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版本之后, 任务取消了"任务执行机器"属性, 改为通过任务注册和自动发现的方式, 动态获取远程执行器地址并执行。
AppName:每个执行器机器集群的唯一标示,任务注册以"执行器"为最小粒度进行注册;每个任务通过其绑定的执行器可感知对应的执行器机器列表;注册表:见"xxl_job_registry"表,"执行器"在进行任务注册时将会周期性维护一条注册记录,即机器地址和AppName的绑定关系;"调度中心"从而可以动态感知每个AppName在线的机器列表;执行器注册:任务注册Beat周期默认30s;执行器以一倍Beat进行执行器注册,调度中心以一倍Beat进行动态任务发现;注册信息的失效时间为三倍Beat;执行器注册摘除:执行器销毁时,将会主动上报调度中心并摘除对应的执行器机器信息,提高心跳注册的实时性;
为保证系统"轻量级"并且降低学习部署成本,没有采用Zookeeper作为注册中心,采用DB方式进行任务注册发现。
对执行失败的任务处理
实现类:JobFailMonitorHelper ,处理流程为:
-
查询执行失败的任务。
-
对需要重试的任务进行重试。
-
发送告警通知,当前只支持邮件通知,可以扩展新的通知方式,实现 JobAlarm 接口并注册为Spring的Bean即可。
执行器掉线处理
实现类:JobCompleteHelper 。
此任务是为了解决执行器掉线导致任务一直在运行中的问题。
调度记录停留在 "运行中" 状态超过10min,且对应执行器心跳注册失败不在线,则将本地调度主动标记失败。
日志统计及清理
实现类:JobLogReportHelper ,处理过程:
-
统计 3 天内日志,统计出每天的任务运行数量、任务执行成功数量、任务执行失败数量。
-
清理日志,默认清理 30 天前的日志,每天清理一次。
调度任务
实现类:JobScheduleHelper 。
调度任务有两个线程:
- scheduleThread
此线程做的事:读取要执行的任务,直接触发或将任务放入 ring (时间轮)中。
具体执行过程如下:
-
读取下次执行时间 <= 当前时间 + 5秒的任务,每次最多读取 6000 条任务。
-
循环处理读取出来的每条任务。
-
下次执行时间过期超过 5 秒。
- 如果任务的过期策略是“立即执行一次”,则立即触发任务。
- 刷新任务的下次执行时间。
-
下次执行时间过期小于 5 秒
- 立即触发任务
- 刷新任务的下次执行时间。
- 如果刷新后的下次执行时间在 5 秒内到达,将任务放入 ring 中,再刷新任务的下次执行时间。
-
下次执行时间刚好是当前时间或者在 5 秒内到达
- 将任务放入 ring 中。
- 刷新任务的下次执行时间。
将任务放入 ring 的处理:
先计算任务下次执行时间所在的秒数,再添加到对应秒数的任务 List 中。
- ringThread
此线程做的事:读取 ring (时间轮)中的任务,触发任务执行。
具体执行过程如下:
-
获取当前时间的秒数,读取 ring 中对应秒及前一秒的任务队列中的数据。
这里读取前一秒的任务队列的原因举例说明一下:
当前时间的秒数是5,执行任务触发用了1秒多,进入下一个循环时的秒数已经过了6秒,会等待到第7秒进行处理,这时读取任务队列如果只读取7秒的任务队列的话,6秒的任务就会被跳过没有执行,所以要读取7秒和6秒的任务队列。
-
循环触发读取的每条任务,是在线程池中异步触发的,不会耗费很长时间。
执行器
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