XXL-job原理

3,514 阅读10分钟

1.前言

在传统的定时任务框架有着如下缺陷:

  • 不支持分片任务
  • 不支持生命周期统一管理
  • 不支持集群
  • 不支持失败重试
  • 不支持动态调整
  • 无报警机制
  • 任务数据统计难以实现 传统的定时框架有以上缺陷,如果解决以上缺陷就需要自己进行重新开发,十分麻烦,所以,我们需要分布式调度系统;在分布式任务调度中,xxl-job有着广泛的使用,并且轻量级,很适合小型项目的任务,xxl-job官方文档。

2.xxl-job设计

2.1 定时任务基本概念

在说框架设计之前,先说明一下定时任务的基本概念,定时任务分为三个部分

  • 触发器:什么时候执行任务,cron表达式就可以认为是触发器
  • 执行器:任务在那个机器上执行,那个机器就可以认为是触发器
  • 任务

2.2 xxl-job的演变

由于单机的处理能力是有限的,慢慢的向分布式系统进行演进。那么,分布式调度系统演变过程是怎么样的呢?

image.png
这时候,就发现问题了,任务是只需要执行一次即可,那么怎么确保他们只执行一次?当然,我们可以使用分布式锁来保证只有一个机器来执行这个任务(保证两台机器时间相同),但是分布式锁的三种实现方式都是有问题的。不适合?
使用调度中心? 由于分布式锁不行,那么我们使用一个调度中心行不行?由一个调度中心,来保证那个执行器来执行任务调度。其实是可以的,此时,系统架构图就变成如下:

image.png 此时,一个大致的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),就认为这个任务是慢任务,为了防止这种慢任务影戏系统吞吐量,就将其放在慢线程池中运行; 流程图如下:

image.png

此时,当调用触发任务时,就可以进行基于它的路由规则选取合适的线程池进行执行了。 每一分钟会清空存储慢线程池的Map key存任务id value存延时的次数;

  • JobRegistryHelper.getInstance().start(); 作用: 更新appName对应的IP组,将新增的IP添加到组中,将断线的IP进行清理 (因为我们的执行器是会上下线的。我们需要及时更新当前在线的执行器) 其流程图如下:

image.png
注:xxl-job有心跳机制,客户端会向注册中心发送心跳,xxl-job会把这个心跳信息保存在xxl_job_registry 此时,就能够实现心跳检测机制,保证机器下线能够及时清除,机器上线能够及时发现;

  • JobFailMonitorHelper.getInstance().start(); 作用:一个分布式调度中心需要有重试机制和报警机制,这个线程就是做这个任务的(因为采用的是异步调用,我们不知道这个任务有没有执行成功) 其流程图如下:

image.png
调度失败情况有很多:

  1. 比如无法将调度信息发送给执行器(可能没有存活执行器)
  2. 或者任务的超时时间到了,系统会给执行器一个中断信号
  3. 或者任务在执行的过程中,突然宕机 以上都会导致调度失败,但是都会把失败的情况记录到数据库中,然后,通过对调度日志进行分析来判断是否需要重试和报警。
    JobCompleteHelper.getInstance().start();
    作用:当调度系统调度执行器后,如果这个时候执行器突然宕机,那么,调度系统这边应该怎么感知呢?并且我们还需要知道这个执行器是否任务是否执行成功。 这个线程就是干这个活;
  4. 当调度记录停留在“运行中”状态超过10min,且对应执行器心跳注册失败不在线,则将本地调度主动标识失败;
  5. 由于,采用异步调度,所以调度中心并不知道任务调度是否真正成功,所以,会建立一个日志回调线程池,来接收RPC调用回复,收到执行器在执行这个任务后的处理结果数据,然后,将结果写回到数据表xxl_job_log中。

JobLogReportHelper.getInstance().start();
作用:

  1. 主要是负责清理过期日志的,把一个月前的日志文件进行清理
  2. 做报表,我们xxl-job有一个控制台,比如说有多少任务执行成功,有多少任务执行失败,就由它以每日为维度进行统计,并写入到xxl_job_log_report中
  3. 每一分钟扫描一次 JobScheduleHelper.getInstance().start(); 作用:
  4. 从xxl_job_info表中找出当前时间 + 5s的所有执行器的数据,然后,根据其调度时间判断是立即调度还是加入到时间戳中(如果任务很多的话,在循环过程中有可能导致当前时间已经大于调度时间的情况)
  5. 对加入到时间轮中的数据进行遍历调用

image.png

以上是将5s内的任务加入到时间轮中,那么加入时间轮之后是怎么运行的呢? 其实内部有一个ringThread 线程来负责对该时间轮遍历运行;其流程如下:

image.png
所以,这样就实现了对时间轮中的任务进行调度
思考:任务调度方式有很多种方式,这边为什么要采用时间轮调度呢? 以我看来,任务调度至少有两种方式:

  1. 我可以使用一个线程,key = 调度时间 value = 任务id,每秒去扫描一次,看看有没有任务可以执行,如果可以执行的话,就执行;
  2. 我可以使用延时队列(延时线程池其实都一样),它就是将任务通过执行时间进行堆排序,不断地拿排着的第一个任务的执行时间和当前时间做对比。如果时间到了就执行,再看看这个任务是不是周期性执行的任务来决定是不是要再加入;

那么,上面这两个方案有什么问题呢? 方案一 ,比如说:我现在有1000个任务,5分钟后才需要进行执行,那么这个线程每一秒扫描一次的话,就需要扫描 5 * 60次 才能执行,所以,它的问题就是,对于一些不是最近的任务也需要进行扫描,浪费性能 方案二 ,因为需要维护最小堆,所以插入数据和取出数据时间复杂度为O(logN),如果要取m个数据就为mlogn了 ,如果采用延时队列还有一个问题这个任务是单线程执行,那么如果一个任务执行的时间过久则会影响下一个任务的执行时间(当然任务的run要是异步执行也行,这个可以用线程池来解决)。

很显然,因为我们分布式调度系统调度的任务是比较多的,所以上面两种方案是不行的。当然,平时的任务的话,就用方案二就好了;

那么xxl-job中时间轮是如何解决上面的问题的呢?

  1. 先通过sql 找出最近5s内的数据
  2. 然后再把相应的数据加入到时间轮中 这样扫描次数最多为两次,并且添加数据,删除数据的时间复杂度为O(1);
    其实,时间轮有着广泛的应用,比如rpc框架,kafka等,可能表现形式不太一样,但是思路是差不多的。可以参考这篇文章

3.xxl-job 源码标注

上面分析了xxl-job的原理,为了让文章结构更加清晰,在文中并没有贴出源码,如果,想查看源码的小伙伴,可以下载以下源码,相关标注很清楚 xxl-job备注源码

4.收获

  1. 对xxl-job有了更深刻的了解,xxl-job比较小巧,对小型项目还是比较友好的
  2. 理解分布式思维,像我们的xxl-job是基于mysql对分布式进行管理
  3. 就是涉及到一些具体的设计,比如说各模块解耦,设计思想,还有的话像一些快慢线程池、时间轮、异步调用及其思想,如果后续有这种需求的话,可以参照它的设计来进行实现。

补充

在分布式任务调度系统中,还有阿里云的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。