1.前言
在传统的定时任务框架有着如下缺陷:
- 不支持分片任务
- 不支持生命周期统一管理
- 不支持集群
- 不支持失败重试
- 不支持动态调整
- 无报警机制
- 任务数据统计难以实现 传统的定时框架有以上缺陷,如果解决以上缺陷就需要自己进行重新开发,十分麻烦,所以,我们需要分布式调度系统;在分布式任务调度中,xxl-job有着广泛的使用,并且轻量级,很适合小型项目的任务,xxl-job官方文档。
2.xxl-job设计
2.1 定时任务基本概念
在说框架设计之前,先说明一下定时任务的基本概念,定时任务分为三个部分
- 触发器:什么时候执行任务,cron表达式就可以认为是触发器
- 执行器:任务在那个机器上执行,那个机器就可以认为是触发器
- 任务
2.2 xxl-job的演变
由于单机的处理能力是有限的,慢慢的向分布式系统进行演进。那么,分布式调度系统演变过程是怎么样的呢?
这时候,就发现问题了,任务是只需要执行一次即可,那么怎么确保他们只执行一次?当然,我们可以使用分布式锁来保证只有一个机器来执行这个任务(保证两台机器时间相同),但是分布式锁的三种实现方式都是有问题的。不适合?
使用调度中心?
由于分布式锁不行,那么我们使用一个调度中心行不行?由一个调度中心,来保证那个执行器来执行任务调度。其实是可以的,此时,系统架构图就变成如下:
此时,一个大致的xxl-job框架就出来了,首先web层先获取触发器、任务信息、执行器相关信息存入到mysql中,然后,找出当前要执行的任务,通过负载均衡算法找到对应的执行器,通过RPC框架,调度该任务,如何失败再根据重试次数和报警机制看看要不要重试和报警。
以上是实现了分布式调度的功能,但是,我们还需要保证调度中心高可用,那么,调度中心高可用是怎么实现的呢?
当有两个调度中心时,就会通过分布式锁,看谁拿到了这个分布式锁,再去数据库查询那个触发器将要触发,到达触发时间的任务进行调度。
设计思想
将调度行为抽象形成“调度中心” 公共平台,而平台本身只负责发起调度请求,不负责业务逻辑。
将任务抽象成分散的JobHandler,交由“执行器”统一管理,“执行器”负责接收调度请求并执行对应的JobHandler中的业务逻辑。
所以,调度和任务两部分可以相互解耦,提供系统整体稳定性和扩展性;
2.3 xxl-job 实现细节
以上是xxl-job架构方面的理解,当架构设计好以后,我们需要关注的就是细节了。为了提供系统的吞吐量,系统展开了一些设计,启动代码如下:
public void init() throws Exception {
// init i18n
initI18n();
// 1. 创建快慢线程池
JobTriggerPoolHelper.toStart();
// 2. 注册线程池启动
JobRegistryHelper.getInstance().start();
// 3. 失败监控线程池启动
JobFailMonitorHelper.getInstance().start();
// 4. admin 对数据进行监控 如果运行时间超过10min 并且没有心跳就设置为失败
JobCompleteHelper.getInstance().start();
// 5. 报表相关,保存任务执行的日志,并且生成日结报表,清理过期日志
JobLogReportHelper.getInstance().start();
//6. 调度调度任务相关
JobScheduleHelper.getInstance().start();
logger.info(">>>>>>>>> init xxl-job admin success.");
}
简单的说:
JobTriggerPoolHelper.toStart();作用: 创建两个线程池,一个线程池为快线程池,另一个线程池为慢线程池;
设计思路如下:在1分钟内,如果一个任务有10次提交时间花费大于500ms(虽然是异步执行,但是由于网络原因,可能会大于500ms),就认为这个任务是慢任务,为了防止这种慢任务影戏系统吞吐量,就将其放在慢线程池中运行; 流程图如下:
此时,当调用触发任务时,就可以进行基于它的路由规则选取合适的线程池进行执行了。 每一分钟会清空存储慢线程池的Map key存任务id value存延时的次数;
JobRegistryHelper.getInstance().start();作用: 更新appName对应的IP组,将新增的IP添加到组中,将断线的IP进行清理 (因为我们的执行器是会上下线的。我们需要及时更新当前在线的执行器) 其流程图如下:
注:xxl-job有心跳机制,客户端会向注册中心发送心跳,xxl-job会把这个心跳信息保存在xxl_job_registry
此时,就能够实现心跳检测机制,保证机器下线能够及时清除,机器上线能够及时发现;
JobFailMonitorHelper.getInstance().start();作用:一个分布式调度中心需要有重试机制和报警机制,这个线程就是做这个任务的(因为采用的是异步调用,我们不知道这个任务有没有执行成功) 其流程图如下:
调度失败情况有很多:
- 比如无法将调度信息发送给执行器(可能没有存活执行器)
- 或者任务的超时时间到了,系统会给执行器一个中断信号
- 或者任务在执行的过程中,突然宕机
以上都会导致调度失败,但是都会把失败的情况记录到数据库中,然后,通过对调度日志进行分析来判断是否需要重试和报警。
JobCompleteHelper.getInstance().start();
作用:当调度系统调度执行器后,如果这个时候执行器突然宕机,那么,调度系统这边应该怎么感知呢?并且我们还需要知道这个执行器是否任务是否执行成功。 这个线程就是干这个活; - 当调度记录停留在“运行中”状态超过10min,且对应执行器心跳注册失败不在线,则将本地调度主动标识失败;
- 由于,采用异步调度,所以调度中心并不知道任务调度是否真正成功,所以,会建立一个日志回调线程池,来接收RPC调用回复,收到执行器在执行这个任务后的处理结果数据,然后,将结果写回到数据表xxl_job_log中。
JobLogReportHelper.getInstance().start();
作用:
- 主要是负责清理过期日志的,把一个月前的日志文件进行清理
- 做报表,我们xxl-job有一个控制台,比如说有多少任务执行成功,有多少任务执行失败,就由它以每日为维度进行统计,并写入到xxl_job_log_report中
- 每一分钟扫描一次
JobScheduleHelper.getInstance().start();作用: - 从xxl_job_info表中找出当前时间 + 5s的所有执行器的数据,然后,根据其调度时间判断是立即调度还是加入到时间戳中(如果任务很多的话,在循环过程中有可能导致当前时间已经大于调度时间的情况)
- 对加入到时间轮中的数据进行遍历调用
以上是将5s内的任务加入到时间轮中,那么加入时间轮之后是怎么运行的呢? 其实内部有一个ringThread 线程来负责对该时间轮遍历运行;其流程如下:
所以,这样就实现了对时间轮中的任务进行调度
思考:任务调度方式有很多种方式,这边为什么要采用时间轮调度呢?
以我看来,任务调度至少有两种方式:
- 我可以使用一个线程,key = 调度时间 value = 任务id,每秒去扫描一次,看看有没有任务可以执行,如果可以执行的话,就执行;
- 我可以使用延时队列(延时线程池其实都一样),它就是将任务通过执行时间进行堆排序,不断地拿排着的第一个任务的执行时间和当前时间做对比。如果时间到了就执行,再看看这个任务是不是周期性执行的任务来决定是不是要再加入;
那么,上面这两个方案有什么问题呢? 方案一 ,比如说:我现在有1000个任务,5分钟后才需要进行执行,那么这个线程每一秒扫描一次的话,就需要扫描 5 * 60次 才能执行,所以,它的问题就是,对于一些不是最近的任务也需要进行扫描,浪费性能 方案二 ,因为需要维护最小堆,所以插入数据和取出数据时间复杂度为O(logN),如果要取m个数据就为mlogn了 ,如果采用延时队列还有一个问题这个任务是单线程执行,那么如果一个任务执行的时间过久则会影响下一个任务的执行时间(当然任务的run要是异步执行也行,这个可以用线程池来解决)。
很显然,因为我们分布式调度系统调度的任务是比较多的,所以上面两种方案是不行的。当然,平时的任务的话,就用方案二就好了;
那么xxl-job中时间轮是如何解决上面的问题的呢?
- 先通过sql 找出最近5s内的数据
- 然后再把相应的数据加入到时间轮中
这样扫描次数最多为两次,并且添加数据,删除数据的时间复杂度为O(1);
其实,时间轮有着广泛的应用,比如rpc框架,kafka等,可能表现形式不太一样,但是思路是差不多的。可以参考这篇文章
3.xxl-job 源码标注
上面分析了xxl-job的原理,为了让文章结构更加清晰,在文中并没有贴出源码,如果,想查看源码的小伙伴,可以下载以下源码,相关标注很清楚 xxl-job备注源码
4.收获
- 对xxl-job有了更深刻的了解,xxl-job比较小巧,对小型项目还是比较友好的
- 理解分布式思维,像我们的xxl-job是基于mysql对分布式进行管理
- 就是涉及到一些具体的设计,比如说各模块解耦,设计思想,还有的话像一些快慢线程池、时间轮、异步调用及其思想,如果后续有这种需求的话,可以参照它的设计来进行实现。
补充
在分布式任务调度系统中,还有阿里云的SchedulerX也在公测中,官网如下:SchedulerX 和xxl-job相比,主要有如下优点:
- xxl-job需要自己进行部署,而xxl-job把这部分工作直接交给阿里云了。
- xxl-job 只支持cron 而对于cron来说只能支持被60分钟整除的轮询,像每40分钟执行一次这种调度是不行的,(他会在40分钟和整点的时候进行执行,因为0%40=0 而取余为0就会执行,会导致出错) 而SchedulerX支持Second delay 支持(1-60s间隔的秒级轮询)
- SchedulerX 支持工作流,(调度不传入时间,一个job工作完毕后,另一个job再开始工作)
- SchedulerX 支持重刷数据 (需要把过去一段时间的任务重新执行一遍)
- XXL-job 支持静态分片(统计当前执行器数量,发送总的执行器数量和当前执行器的位置)SchedulerX还支持MapReduce动态分片 网格分片等功能
总结:如果,只是简单使用下功能,没有用到这些特性可以使用xxl-job 如果说为了稳定性,并且调度任务比较多,也用到了高级特性,那就可以使用SchedulerX。