ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

如何实现一个任务调度系统?

如何实现一个任务调度系统? 阅读一篇「定时任务框架选型」的文章时一位网友的留言电到了我我看过那么多所谓的教程大部分都是教“如何使用工具”的没有多少是教“如何制作工具”的能教“如何仿制工具”的都已经是凤毛麟角中国 软件行业缺的是真正可以“制作工具”的程序员而绝对不缺那些“使用工具”的程序员...... ”这个业界最不需要的就是“会使用XX工具的工程师”而是“有创造力的软件工程师”业界所有的饭碗本质就是“有创造力的软件工程师”提供出来的啊写这篇文章想和大家从头到脚说说任务调度希望大家读完之后能够理解实现一个任务调度系统的核心逻辑。1 QuartzQuartz是一款Java开源任务调度框架也是很多Java工程师接触任务调度的起点。下图显示了任务调度的整体流程Quartz的核心是三个组件。任务Job 用于表示被调度的任务触发器Trigger 定义调度时间的元素即按照什么时间规则去执行任务。一个Job可以被多个Trigger关联但是一个Trigger 只能关联一个Job调度器 工厂类创建Scheduler根据触发器定义的时间规则调度任务。上图代码中Quartz 的JobStore是RAMJobStoreTrigger 和 Job 存储在内存中。执行任务调度的核心类是QuartzSchedulerThread。调度线程从JobStore中获取需要执行的的触发器列表并修改触发器的状态Fire触发器修改触发器信息(下次执行触发器的时间以及触发器状态并存储起来。最后创建具体的执行任务对象通过worker线程池执行任务。接下来再聊聊 Quartz 的集群部署方案。Quartz的集群部署方案需要针对不同的数据库类型(MySQL , ORACLE) 在数据库实例上创建Quartz表JobStore是:JobStoreSupport。这种方案是分布式的没有负责集中管理的节点而是利用数据库行级锁的方式来实现集群环境下的并发控制。scheduler实例在集群模式下首先获取{0}LOCKS表中的行锁Mysql 获取行锁的语句{0}会替换为配置文件默认配置的QRTZ_。sched_name为应用集群的实例名lock_name就是行级锁名。Quartz主要有两个行级锁触发器访问锁 (TRIGGER_ACCESS) 和 状态访问锁STATE_ACCESS。这个架构解决了任务的分布式调度问题同一个任务只能有一个节点运行其他节点将不执行任务当碰到大量短任务时各个节点频繁的竞争数据库锁节点越多性能就会越差。2 分布式锁模式Quartz的集群模式可以水平扩展也可以分布式调度但需要业务方在数据库中添加对应的表有一定的强侵入性。有不少研发同学为了避免这种侵入性也探索出分布式锁模式。业务场景电商项目用户下单后一段时间没有付款系统就会在超时后关闭该订单。通常我们会做一个定时任务每两分钟来检查前半小时的订单将没有付款的订单列表查询出来然后对订单中的商品进行库存的恢复然后将该订单设置为无效。我们使用Spring Schedule的方式做一个定时任务。Scheduled(cron 0 */2 * * * ? ) public void doTask() { log.info(定时任务启动); //执行关闭订单的操作 orderService.closeExpireUnpayOrders(); log.info(定时任务结束); }在单服务器运行正常考虑到高可用业务量激增架构会演进成集群模式在同一时刻有多个服务执行一个定时任务有可能会导致业务紊乱。解决方案是在任务执行的时候使用Redis 分布式锁来解决这类问题。Scheduled(cron 0 */2 * * * ? ) public void doTask() { log.info(定时任务启动); String lockName closeExpireUnpayOrdersLock; RedisLock redisLock redisClient.getLock(lockName); //尝试加锁最多等待3秒上锁以后5分钟自动解锁 boolean locked redisLock.tryLock(3, 300, TimeUnit.SECONDS); if(!locked){ log.info(没有获得分布式锁:{} , lockName); return; } try{ //执行关闭订单的操作 orderService.closeExpireUnpayOrders(); } finally { redisLock.unlock(); } log.info(定时任务结束); }Redis的读写性能极好分布式锁也比Quartz数据库行级锁更轻量级。当然Redis锁也可以替换成Zookeeper锁也是同样的机制。在小型项目中使用定时任务框架Quartz/Spring Schedule和 分布式锁redis/zookeeper有不错的效果。但是呢我们可以发现这种组合有两个问题定时任务在分布式场景下有空跑的情况而且任务也无法做到分片要想手工触发任务必须添加额外的代码才能完成。3 ElasticJob-Lite 框架ElasticJob-Lite 定位为轻量级无中心化解决方案使用 jar 的形式提供分布式任务的协调服务。应用内部定义任务类实现SimpleJob接口编写自己任务的实际业务流程即可。public class MyElasticJob implements SimpleJob { Override public void execute(ShardingContext context) { switch (context.getShardingItem()) { case 0: // do something by sharding item 0 break; case 1: // do something by sharding item 1 break; case 2: // do something by sharding item 2 break; // case n: ... } } }举例应用A有五个任务需要执行分别是ABCDE。任务E需要分成四个子任务应用部署在两台机器上。应用A在启动后 5个任务通过 Zookeeper 协调后被分配到两台机器上通过Quartz Scheduler 分开执行不同的任务。ElasticJob 从本质上来讲 底层任务调度还是通过 Quartz 相比Redis分布式锁 或者 Quartz 分布式部署 它的优势在于可以依赖 Zookeeper 这个大杀器 将任务通过负载均衡算法分配给应用内的 Quartz Scheduler容器。从使用者的角度来讲是非常简单易用的。但从架构来看调度器和执行器依然在同一个应用方JVM内而且容器在启动后依然需要做负载均衡。应用假如频繁的重启不断的去选主对分片做负载均衡这些都是相对比较重的操作。ElasticJob 的控制台通过读取注册中心数据展现作业状态更新注册中心数据修改全局任务配置。从一个任务调度平台的角度来看控制台功能还是偏孱弱的。4 中心化流派中心化的原理是把调度和任务执行隔离成两个部分调度中心和执行器。调度中心模块只需要负责任务调度属性触发调度命令。执行器接收调度命令去执行具体的业务逻辑而且两者都可以进行分布式扩容。4.1 MQ模式先谈谈我在艺龙促销团队接触的第一种中心化架构。调度中心依赖Quartz集群模式当任务调度时候发送消息到RabbitMQ 。业务应用收到任务消息后消费任务信息。这种模型充分利用了MQ解耦的特性调度中心发送任务应用方作为执行器的角色接收任务并执行。但这种设计强依赖消息队列可扩展性和功能系统负载都和消息队列有极大的关联。这种架构设计需要架构师对消息队列非常熟悉。4.2 XXL-JOBXXL-JOB 是一个分布式任务调度平台其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线开箱即用。xxl-job 2.3.0架构图我们重点剖析下架构图 ▍ 网络通讯 server-worker 模型调度中心和执行器 两个模块之间通讯是 server-worker 模式。调度中心本身就是一个SpringBoot 工程启动会监听8080端口。执行器启动后会启动内置服务 EmbedServer 监听9994端口。这样双方都可以给对方发送命令。那调度中心如何知道执行器的地址信息呢 上图中执行器会定时发送注册命令 这样调度中心就可以获取在线的执行器列表。通过执行器列表就可以根据任务配置的路由策略选择节点执行任务。常见的路由策略有如下三种随机节点执行选择集群中一个可用的执行节点执行调度任务。适用场景离线订单结算。广播执行在集群中所有的执行节点分发调度任务并执行。适用场景批量更新应用本地缓存。分片执行按照用户自定义分片逻辑进行拆分分发到集群中不同节点并行执行提升资源利用效率。适用场景海量日志统计。▍ 调度器调度器是任务调度系统里面非常核心的组件。XXL-JOB 的早期版本是依赖Quartz。但在v2.1.0版本中完全去掉了Quartz的依赖原来需要创建的 Quartz表也替换成了自研的表。核心的调度类是JobTriggerPoolHelper。调用start方法后会启动两个线程scheduleThread 和 ringThread 。首先 scheduleThread 会定时从数据库加载需要调度的任务这里从本质上还是基于数据库行锁保证同时只有一个调度中心节点触发任务调度。Connection conn XxlJobAdminConfig.getAdminConfig() .getDataSource().getConnection(); connAutoCommit conn.getAutoCommit(); conn.setAutoCommit(false); preparedStatement conn.prepareStatement( select * from xxl_job_lock where lock_name schedule_lock for update); preparedStatement.execute(); # 触发任务调度 (伪代码) for (XxlJobInfo jobInfo: scheduleList) { // 省略代码 } # 事务提交 conn.commit();调度线程会根据任务的「下次触发时间」采取不同的动作已过期的任务需要立刻执行的直接放入线程池中触发执行 五秒内需要执行的任务放到 ringData 对象里。ringThread 启动后定时从 ringData 对象里获取需要执行的任务列表 放入到线程池中触发执行。5 自研在巨人的肩膀上2018年我有一段自研任务调度系统的经历。背景是兼容技术团队自研的RPC框架技术团队不需要修改代码RPC注解方法可以托管在任务调度系统中直接当做一个任务来执行。自研过程中研读了XXL-JOB 源码同时从阿里云分布式任务调度 SchedulerX 吸取了很多营养。SchedulerX 1.0 架构图Schedulerx-console 是任务调度的控制台用于创建、管理定时任务。负责数据的创建、修改和查询。在产品内部与 schedulerx server 交互。Schedulerx-server 是任务调度的服务端是 Scheduler的核心组件。负责客户端任务的调度触发以及任务执行状态的监测。Schedulerx-client 是任务调度的客户端。每个接入客户端的应用进程就是一个的 Worker。Worker 负责与 Schedulerx-server 建立通信让 schedulerx-server发现客户端的机器。并向schedulerx-server注册当前应用所在的分组这样 schedulerx-server才能向客户端定时触发任务。我们模仿了SchedulerX的模块架构设计如下图我选择了 RocketMQ 源码的通讯模块 remoting 作为自研调度系统的通讯框架。基于如下两点我对业界大名鼎鼎的 Dubbo不熟悉而remoting我已经做了多个轮子我相信自己可以搞定在阅读 SchedulerX 1.0 client 源码中发现 SchedulerX 的通讯框架和RocketMQ Remoting很多地方都很类似。它的源码里有现成的工程实现完全就是一个宝藏。我将 RocketMQ remoting 模块去掉名字服务代码做了一定程度的定制。在RocketMQ的remoting里服务端采用 Processor 模式。调度中心需要注册两个处理器回调结果处理器CallBackProcessor和心跳处理器HeartBeatProcessor 。执行器需要注册触发任务处理器TriggerTaskProcessor 。public void registerProcessor( int requestCode, NettyRequestProcessor processor, ExecutorService executor);处理器的接口public interface NettyRequestProcessor { RemotingCommand processRequest( ChannelHandlerContext ctx, RemotingCommand request) throws Exception; boolean rejectRequest(); }对于通讯框架来讲我并不需要关注通讯细节只需要实现处理器接口即可。以触发任务处理器TriggerTaskProcessor举例搞定网络通讯后调度器如何设计 最终我还是选择了Quartz 集群模式。主要是基于以下几点原因调度量不大的情况下 Quartz 集群模式足够稳定而且可以兼容原来的XXL-JOB任务使用时间轮的话本身没有足够的实践经验担心出问题。另外如何让任务通过不同的调度服务schedule-server触发 需要有一个协调器。于是想到Zookeeper。但这样的话又引入了新的组件。研发周期不能太长想快点出成果。自研版的调度服务花费一个半月上线了。系统运行非常稳定研发团队接入也很顺畅。调度量也不大 四个月总共接近4000万到5000万之间的调度量。坦率的讲自研版的瓶颈我的脑海里经常能看到。数据量大我可以搞定分库分表但 Quartz 集群基于行级锁的模式 注定上限不会太高。为了解除心中的困惑我写一个轮子DEMO看看可否work去掉外置的注册中心调度服务schedule-server管理会话引入zookeeper通过zk协调调度服务。但是HA机制很粗糙相当于一个任务调度服务运行另一个服务standbyQuartz 替换成时间轮 参考Dubbo里的时间轮源码。这个Demo版本在开发环境可以运行但有很多细节需要优化仅仅是个玩具并没有机会运行到生产环境。最近读阿里云的一篇文章《如何通过任务调度实现百万规则报警》SchedulerX2.0 高可用架构见下图文章提到每个应用都会做三备份通过 zk 抢锁一主两备如果某台 Server 挂了会进行 failover由其他 Server 接管调度任务。这次自研任务调度系统从架构来讲并不复杂实现了XXL-JOB的核心功能也兼容了技术团队的RPC框架但并没有实现工作流以及mapreduce分片。SchedulerX 在升级到2.0之后基于全新的Akka 架构这种架构号称实现高性能工作流引擎实现进程间通信减少网络通讯代码。在我调研的开源任务调度系统中PowerJob也是基于Akka 架构同时也实现了工作流和MapReduce执行模式。我对PowerJob非常感兴趣也会在学习实践后输出相关文章敬请期待。6 技术选型首先我们将任务调度开源产品和商业产品 SchedulerX 放在一起生成一张对照表Quartz 和 ElasticJob从本质上还是属于框架的层面。中心化产品从架构上来讲更加清晰调度层面更灵活可以支持更复杂的调度mapreduce动态分片工作流。XXL-JOB 从产品层面已经做到极简开箱即用调度模式可以满足大部分研发团队的需求。简单易用 能打所以非常受大家欢迎。其实每个技术团队的技术储备不尽相同面对的场景也不一样所以技术选型并不能一概而论。不管是使用哪种技术在编写任务业务代码时还是需要注意两点幂等。当任务被重复执行的时候或者分布式锁失效的时候程序依然可以输出正确的结果任务不跑了千万别惊慌。查看调度日志JVM层面使用Jstack命令查看堆栈网络通讯要添加超时时间 一般能解决大部分问题。
返回列表