分布式任务调度平台 XXL-JOB 源码学习

153 阅读6分钟

XXL-JOB 是一个分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。

官网:www.xuxueli.com/xxl-job/

github:github.com/xuxueli/xxl…

使用场景:应用程序启动了定时任务,比如每天00:00:00执行任务处理数据,如果此应用程序部署了多个实例,就需要使用 XXL 等这类分布式任务调度平台,使得 00:00:00 时只选择一个应用程序实例执行定时任务。

架构设计

将调度行为抽象形成“调度中心”公共平台,而平台自身并不承担业务逻辑,“调度中心”负责发起调度请求。

将任务抽象成分散的JobHandler,交由“执行器”统一管理,“执行器”负责接收调度请求并执行对应的JobHandler中业务逻辑。

因此,“调度”和“任务”两部分可以相互解耦,提高系统整体稳定性和扩展性。

(此部分摘自官网)

调度中心

调度中心的主要代码在 xxl-job-admin 模块,部分代码在 xxl-job-core 模块。

调度中心任务启动入口在 com.xxl.job.admin.core.conf.XxlJobAdminConfig#afterPropertiesSet ,此方法是实现 InitializingBean 接口的,在 Bean 属性设置完成后被调用。

任务初始化在 XxlJobScheduler#init 方法。

trigger 线程池初始化

实现类:JobTriggerPoolHelper 。

将调度线程池隔离,拆分为"Fast"和"Slow"两个线程池,1分钟窗口期内任务耗时达500ms超过10次,该窗口期内判定为慢任务,慢任务自动降级进入"Slow"线程池,避免耗尽调度线程,提高系统稳定性。

执行器注册处理任务

实现类:JobRegistryHelper ,实现任务注册, 任务自动发现功能。

(下面这部分描述摘自官网)

自v1.5版本之后, 任务取消了"任务执行机器"属性, 改为通过任务注册和自动发现的方式, 动态获取远程执行器地址并执行。

  1. AppName:每个执行器机器集群的唯一标示,任务注册以"执行器"为最小粒度进行注册;每个任务通过其绑定的执行器可感知对应的执行器机器列表;
  2. 注册表:见"xxl_job_registry"表,"执行器"在进行任务注册时将会周期性维护一条注册记录,即机器地址和AppName的绑定关系;"调度中心"从而可以动态感知每个AppName在线的机器列表;
  3. 执行器注册:任务注册Beat周期默认30s;执行器以一倍Beat进行执行器注册,调度中心以一倍Beat进行动态任务发现;注册信息的失效时间为三倍Beat;
  4. 执行器注册摘除:执行器销毁时,将会主动上报调度中心并摘除对应的执行器机器信息,提高心跳注册的实时性;

为保证系统"轻量级"并且降低学习部署成本,没有采用Zookeeper作为注册中心,采用DB方式进行任务注册发现。

对执行失败的任务处理

实现类:JobFailMonitorHelper ,处理流程为:

  1. 查询执行失败的任务。

  2. 对需要重试的任务进行重试。

  3. 发送告警通知,当前只支持邮件通知,可以扩展新的通知方式,实现 JobAlarm 接口并注册为Spring的Bean即可。

执行器掉线处理

实现类:JobCompleteHelper 。

此任务是为了解决执行器掉线导致任务一直在运行中的问题。

调度记录停留在 "运行中" 状态超过10min,且对应执行器心跳注册失败不在线,则将本地调度主动标记失败。

日志统计及清理

实现类:JobLogReportHelper ,处理过程:

  1. 统计 3 天内日志,统计出每天的任务运行数量、任务执行成功数量、任务执行失败数量。

  2. 清理日志,默认清理 30 天前的日志,每天清理一次。

调度任务

实现类:JobScheduleHelper 。

调度任务有两个线程:

  • scheduleThread

此线程做的事:读取要执行的任务,直接触发或将任务放入 ring (时间轮)中。

具体执行过程如下:

  1. 读取下次执行时间 <= 当前时间 + 5秒的任务,每次最多读取 6000 条任务。

  2. 循环处理读取出来的每条任务。

  3. 下次执行时间过期超过 5 秒。

    1. 如果任务的过期策略是“立即执行一次”,则立即触发任务。
    2. 刷新任务的下次执行时间。
  4. 下次执行时间过期小于 5 秒

    1. 立即触发任务
    2. 刷新任务的下次执行时间。
    3. 如果刷新后的下次执行时间在 5 秒内到达,将任务放入 ring 中,再刷新任务的下次执行时间。
  5. 下次执行时间刚好是当前时间或者在 5 秒内到达

    1. 将任务放入 ring 中。
    2. 刷新任务的下次执行时间。

将任务放入 ring 的处理:

先计算任务下次执行时间所在的秒数,再添加到对应秒数的任务 List 中。

  • ringThread

此线程做的事:读取 ring (时间轮)中的任务,触发任务执行。

具体执行过程如下:

  1. 获取当前时间的秒数,读取 ring 中对应秒及前一秒的任务队列中的数据。

    这里读取前一秒的任务队列的原因举例说明一下:

    当前时间的秒数是5,执行任务触发用了1秒多,进入下一个循环时的秒数已经过了6秒,会等待到第7秒进行处理,这时读取任务队列如果只读取7秒的任务队列的话,6秒的任务就会被跳过没有执行,所以要读取7秒和6秒的任务队列。

  2. 循环触发读取的每条任务,是在线程池中异步触发的,不会耗费很长时间。

执行器

xxl-job-executor-sample-frameless 模块是不使用 Spring Boot 框架的执行器代码,不常用。重点分析使用 Spring Boot 框架的执行器代码,即 xxl-job-executor-sample-springboot 模块。

执行器初始化

com.xxl.job.executor.core.config.XxlJobConfig#xxlJobExecutor

此方法向 Spring 注册了一个 Bean:XxlJobSpringExecutor。

注意调度中心和执行器通讯的端口是 xxl.job.executor.port , server.port 是原有业务接口使用的端口。

com.xxl.job.core.executor.impl.XxlJobSpringExecutor 实现了 Spring 框架的 SmartInitializingSingleton 接口,在 Spring 所有单例 Bean 创建完成后会回调 afterSingletonsInstantiated 方法。

其中 initJobHandlerMethodRepository 是初始化任务处理器名称和对应方法的对应关系,

例如任务处理器名称“demoJobHandler”对应的处理方法是 SampleXxlJob 的 demoJobHandler() 方法。

super.start() 启动执行器任务:

任务执行线程启动:XxlJobExecutor#registJobThread