分布式调度平台XXL-JOB原理分析

一、简介

1.1、调度框架/工具

1.2、XXL-JOB介绍
基本信息

  • 大众点评一个叫许雪里的程序员开发的一个任务调度框架,所以项目名叫XXL-JOB,嗯……
  • 使用广泛,目前已有超过400+公司接入
  • 项目地址:Gitee地址开源社区

术语解释

  • 调度中心:服务端 & 管理后台,负责管理调度信息及发出调度请求
  • 执行器:可以简单理解为一个Group,由多个具有相同名称的执行器节点组成
  • 执行器节点:任务的实际执行节点,客户端,负责接收调度请求并执行任务逻辑
  • 任务:具体的执行逻辑,可以有多种实现方式,如Java、Shell、Python等,任务都需要绑定到某个具体的执行器上
  • 调度:调度中心发起的一次远程任务调度,任务调度与任务执行解耦,调度后马上返回
  • 执行:执行器接收到调度后本地异步执行,执行完成后异步通知调度中心执行结果

框架特性
XXL-JOB是轻量级易扩展高可用开箱即用分布式 任务调度框架

  • 轻量级:环境依赖Maven3+,Jdk1.8+,Mysql5.7+;整个框架只有调度中心和执行器
  • 分布式:调度中心和执行器均可以集群部署
  • 易扩展:可以随时增减调度中心和执行器部署节点
  • 高可用:在分布式部署模式下,可以容忍部分调度中心节点宕机;至于执行器嘛,当然要看具体的业务逻辑是否支持幂等或事务方式执行
  • 开箱即用:Java实现,调度中心项目经过简单配置后即可直接部署运行,执行器的使用也比较简单
  • 任务调度:只负责任务的管理和调度,不与具体的任务执行耦合,保证了调度系统的可控度与稳定性

功能特点

  • 触发策略(什么时候执行):Cron触发、固定间隔触发、固定延时触发、API(事件)触发、手动触发、父子任务触发
  • 路由策略(由谁执行):第一个、最后一个、随机、一致性HASH、忙碌转移等
  • 执行控制(如何执行):超时控制,失败重试,失败告警,分片执行,阻塞策略,过期策略等
  • 管理方面:进度监控,运行报表,用户管理,权限管理
  • GLUE模式:支持Java Bean、Shell、Python等方式执行
  • ……

1.3、XXL-JOB的使用
调度中心部署

  • 下载源码,最新版本为2.3.0,在xxl-job-admin模块下修改application.properties文件配置参数,主要是MySQL的连接信息,其它视情况调整或添加
  • 手动执行xxl-job/doc/db/table_xxl_job.sql内容,创建相关数据库表
  • 打开管理页面:http://localhost:8080/xxl-job-admin,默认登录账号 “admin/123456”,登录后主页面如下:在这里插入图片描述
    执行器接入
  • 引入core包依赖
<dependency>
    <groupId>com.xuxueli</groupId>
    <artifactId>xxl-job-core</artifactId>
    <version>2.3.0</version>
</dependency>
  • 添加配置信息
### 调度中心地址,与上面保持一致即可
xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin
### access token,需要与admin的token保持一致,可以都为空
xxl.job.accessToken=
 
### 执行器名称,用于管理后台添加执行器,对应AppName
xxl.job.executor.appname=executor-sample
### 执行器地址,ip:port,与下面的配置二选一
xxl.job.executor.address=
### 执行器ip,为空时调度中心自动获取,建议为空
xxl.job.executor.ip=
### 执行器端口
xxl.job.executor.port=9997
 
### 日志保存路径
xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler
### 日志保存天数
xxl.job.executor.logretentiondays=30
  • 配置启动类
@Configuration
public class XxlJobConfig {
    @Value("${xxl.job.admin.addresses}")
    private String adminAddresses;
 
    @Value("${xxl.job.accessToken}")
    private String accessToken;
 
    @Value("${xxl.job.executor.appname}")
    private String appname;
 
    @Value("${xxl.job.executor.address}")
    private String address;
 
    @Value("${xxl.job.executor.ip}")
    private String ip;
 
    @Value("${xxl.job.executor.port}")
    private int port;
 
    @Value("${xxl.job.executor.logpath}")
    private String logPath;
 
    @Value("${xxl.job.executor.logretentiondays}")
    private int logRetentionDays;
 
    @Bean
    public XxlJobSpringExecutor xxlJobExecutor() {
        logger.info(">>>>>>>>>>> xxl-job config init.");
        XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
        xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
        xxlJobSpringExecutor.setAppname(appname);
        xxlJobSpringExecutor.setAddress(address);
        xxlJobSpringExecutor.setIp(ip);
        xxlJobSpringExecutor.setPort(port);
        xxlJobSpringExecutor.setAccessToken(accessToken);
        xxlJobSpringExecutor.setLogPath(logPath);
        xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
 
        return xxlJobSpringExecutor;
    }
}
  • 任务实现类
@Component
public class SampleXxlJob {
   @XxlJob("demoJobHandler") // demoJobHandler为任务名称
   public void demoJobHandler() throws Exception {
       XxlJobHelper.log("XXL-JOB, Hello World.");  // 通过该方法记录的日志在调度中心可以查看

       for (int i = 0; i < 5; i++) {
           XxlJobHelper.log("beat at:" + i);
           TimeUnit.SECONDS.sleep(2);
       }
       // 默认会返回执行成功
   }
}
  • 启动客户端服务

添加任务并执行

  • 【执行器管理】中新增执行器,AppName为客户端配置的执行器名词,注册方式建议选自动注册
  • 【任务管理】中新增任务,选择刚添加的执行器,允许模式选择Bean,JobHandler填@XxlJob中指定的名称,其它参数视情况填写
  • 【任务管理】中刚才添加的任务,操作下拉框选择【执行一次】,调度中心向执行器发起一次调度
  • 【调度日志】中查看任务调度结果及执行结果

二、XXL-JOB原理分析

2.1、一些问题
在开始进行分析前,你可能会有如下一些疑问,我们先来看一下,希望后面的分析能帮助你解开这些疑惑。

使用相关

  • 同一个任务可以并行执行吗?
  • 调度中心可以看到任务的执行过程和结果吗?
  • 可以中断执行中的任务吗?
  • 任务执行失败了怎么办?
  • 执行中的任务宕机了怎么办?
  • 子任务是如何被调度的?
  • 任务计算量很大,如何提高执行效率?

实现相关

  • 调度中心在集群模式下是如何进行调度的(谁来调度,如何防止重复调度,如何进行负载均衡)?
  • 如何选择执行器节点?
  • 最大可以支持到多少并发调度?
  • 调度是准时触发的吗,精度能达到多少,如何实现?
  • 调度中心重启后,未调度的定时任务怎么办?
  • 调度日志和运行日志分别保存在哪?

2.2、整体架构输入图片说明
官网架构图↑↑↑↑↑↑
在这里插入图片描述
上一张为官网架构图,重在描述整体功能模块划分
下一张为笔者重新梳理的架构图,补充了一些实现细节,两张图结合着看可以加深理解。以下说明均针对第二张图

系统组成

  • 调度中心:维护执行器(节点注册及注销),维护调度任务,发起调度及重试,失败告警,统计执行信息,人员及权限管理
  • 执行器:向调度中心注册,根据调度请求执行任务,记录执行日志,返回调度及执行结果
  • MySQL:保存所有任务信息、调度记录等重要信息
  • 文件系统:保存任务执行日志及回调失败日志

2.3、数据模型

模型名称说明
xxl_job_group执行器包含执行器名称,执行器地址列表(可调度地址集合)以及地址注册方式
xxl_job_registry执行器节点注册信息执行器节点注册信息,客户端启动后每隔30s向调度中心发送心跳更新注册时间,调度中心将近期(30s*3)存活的执行器节点ip更新到执行器地址列表
xxl_job_info任务信息包含了任务本身相关的所有信息,以及上一次、下一次执行时间、关联子任务
xxl_job_logglueGLUE源代码GLUE源代码及修改记录
xxl_job_log任务调度记录包括调度状态任务执行状态等,失败任务处理线程(见上图)会根据这两个状态以及重试次数决定是否重新发起调度及**失败告警
xxl_job_log_report任务统计信息按天维度统计任务执行情况
xxl_job_lock锁表多个调度中心争抢调度权限时,通过对该表特定数据加锁(select *** for update)实现
xxl_job_user用户信息管理后台的用户信息及权限配置

2.4、后台线程与线程池

类型名称说明
调度中心线程执行器监控线程JobRegistryHelper中实现
定期移除失效(最近更新时间超过30s*3)的执行器节点
调度计划线程JobScheduleHelper中实现
生成待调度任务;先对xxl_job_lock加锁获取全局调度权限,读取近期待调度的任务列表,放入待调度任务环
触发调度线程JobScheduleHelper中实现
消费待调度任务;从任务环中取出当前的调度任务列表,通过调度线程池进行异步调度
失败任务监控线程JobCompleteHelper中实现
定期查找失败(执行时间>10min,且执行器不在线,如宕机情况)的任务并更新状态
失败任务处理线程JobFailMonitorHelper中实现
定期处理失败的任务,根据调度状态及任务执行状态判断,操作包括重新发起调度及告警
调度报告统计线程JobLogReportHelper中实现
定期统计近3天的调度报告,包括成功数、失败数、总任务数等
线程池执行器节点注册与注销线程池JobRegistryHelper中实现
负责接受执行器节点的注册与注销请求
执行回调处理线程池JobCompleteHelper中实现
处理执行器的执行结果回调,记录执行日志;执行成功后如果有关联子任务,则将子任务加入调度线程池
调度线程池JobTriggerPoolHelper中实现
负责异步发起远程调度,调度后可快速返回
内部分快和慢两个调度线程池,当某个类型任务近期调度耗时过高时会自动使用慢线程池进行调度以免影响其它任务调度
执行器线程任务线程JobThread,负责执行某一类具体的任务,持有一个JobHandler(具体的任务执行逻辑或由此生成的代理)及任务参数阻塞队列,从队列中依次获取参数执行
执行器节点注册与注销线程ExecutorRegistryThread中实现
定期将自己注册到所有调度中心,应用下线时会自动注销,避免接受新的调度任务
任务执行结果回调线程TriggerCallbackThread中实现
任务执行结束后将结果放入回调阻塞队列,回调线程将结果告知调度中心
回调重试线程TriggerCallbackThread中实现
结果回调失败后会将信息写入本地文件,由重试线程发起重试
日志文件清理线程JobLogFileCleanThread中实现
本地日志按天归档,线程根据配置删除过期文件

2.5、交互说明

请求方向功能说明
执行器 → 调度中心节点注册循环更新执行器注册信息
节点注销优雅关闭,防止无效调度
执行结果回调本地任务执行结束(成功或者失败)或获取结果超时后告知调度中心执行结果。
需要注意的是,获取结果超时不一定代表某次任务执行失败,所以最好通过业务日志判断最终执行情况。
宕机情况下不会再发起回调。
调度中心 → 执行器存活探测检查执行器当前是否在线,用于故障转移路由策略
空闲探测检查执行器当前类型的任务是否空闲,用于忙碌转移路由策略
任务调度请求执行器执行某任务1次。
执行器为每种类型的任务分配一个执行线程,该线程维护了一个待执行任务参数阻塞队列,当执行任务到达时,实际上只是将本次任务参数放入该阻塞队列等待异步执行,见上图“任务 i 线程”部分。
任务中止请求执行器中止当前正在执行的任务,阻塞队列里的待执行任务也将会被丢弃并回调调度中心;当然是否能真正停止执行取决于程序是否响应了中断
执行日志请求请求执行器返回某次任务的执行日志,该日志保存在执行器节点所在物理机文件系统中

2.6、核心流程
在这里插入图片描述
说明:

  • 整体交互流程及异步线程/线程池过多,上图并没有完全准确和完整的描述整个过程,但核心流程大部分已经包含在内了;
  • 橙色线框表示涉及到异步“队列”,虚线箭头表示异步调度;
  • 上图最核心的地方在于如何准确的筛选出待调度任务,以及准时的发起异步调度,下面详细说明。

待调度任务筛选
为了能准时的发起调度,不可能每次都实时从数据库中捞取待调度的任务,XXL-JOB的设计思想是把将要被调度的任务提前捞取出来放入队列,然后用另外一个线程准实时(每秒轮询)扫描并发起调度。
在这里插入图片描述
大致的筛选及调度逻辑如上图所示

  • 调度中心每隔5s捞取下一次执行时间在未来5s内的任务列表(图中黄色+绿色+蓝色区域)
  • 对于过去5s前的任务,根据该任务是否需要进行补偿决定是否立即触发调度1次(放入调度线程池)
  • 对于过去5s内的任务,立即触发调度1次(放入调度线程池)
  • 对于未来5s内的任务,以当前秒数[0~59]为key放入待调度任务环等待调度

待调度任务”环”
在这里插入图片描述

说是“环”实际上是一个以**当前秒数为key的map结构**,value为该时间段内需要被调度的任务列表。

调度计划线程每隔5s捞取待调度任务放入“环”中,触发调度线程每秒取出当前3s内的任务进行异步调度,该操作为小数据量的纯内存操作,延迟极低。

**绿色部分**为当前秒数的任务;**黄色部分**为过去2s内的任务,大部分情况下都为空,这里是为了防止被遗漏做的冗余设计;**蓝色部分**为将要被调度的任务。

调度线程池

所有未过期的任务都会放入线程池进行异步调度,为了保证异步调度的效率和稳定性,这里设计了**快、慢两个线程池**,如果1min内某一类任务的调度耗时超过500ms的次数超过10次则会切换到慢线程池。
调度耗时高可能的原因有网络延迟/断开、执行器负载高/无响应、以及分片广播任务等。
并发量:按官方的说法,每次异步调度耗时10ms左右(基本为一次请求的网络开销),默认快线程池大小为200(最小配置),所以最大支持并发量为20000。

三、思考总结

3.1、问题回顾
使用相关

  • Q:同一个任务可以并行执行吗?

  • A:在不同执行器节点下可以并行执行,同一个节点内只能串行(阻塞队列实现)

  • Q:调度中心可以实时查看任务的执行过程和结果吗?

  • A:可以,前提是任务实时记录了进度信息

  • Q:可以中断执行中的任务吗?

  • A:可以,对于当前执行中的任务只是“软”中断,具体取决于程序是否响应了中断;阻塞队列里的剩余批次任务会被丢弃

  • Q:任务执行失败了怎么办?

  • A:根据重试策略忽略本次失败或重新发起调度

  • Q:执行中的任务宕机了怎么办?

  • A:执行器会检测到“丢失”任务,然后根据重试策略忽略本次失败或重新发起调度

  • Q:子任务是如何被调度的?

  • A:在父任务执行完成并回调给调度中心后,会“立即”(线程池)触发1次子任务调度

  • Q:任务计算量很大,如何提高执行效率?

  • A:分片广播任务,将执行逻辑分散在不同的执行器节点中

实现相关

  • Q:调度中心在集群模式下是如何进行调度的(谁来调度,如何防止重复调度,如何进行负载均衡)?

  • A:通过时钟校准(等待至整秒)加mysql分布式锁公平获取调度权限

  • Q:如何选择执行器节点?

  • A:执行器节点提供了存活探测及忙碌探测接口,可以根据执行器当前状态选择多种路由策略

  • Q:最大可以支持到多少并发调度?

  • A:不考虑网络异常,以默认200个最大线程数计算,最高可支持到2w并发量

  • Q:调度是准时触发的吗,精度能达到多少?

  • A:目前只支持到秒级精度,调度延迟毫秒级

  • Q:调度中心宕机/重启后,未调度的定时任务怎么办?

  • A:已过期的任务按补偿策略进行处理,未过期的由新节点正常调度

  • Q:调度日志和运行日志分别保存在哪?

  • A:调度日志由调度中心保存在数据库,执行日志由执行器保存在本地磁盘

3.2、设计总结

**简洁架构**:整个框架只有调度中心和执行器两种角色,调度中心无状态,节点之间无感知,依赖的中间件也只有mysql

**异步化**:大量使用独立线程、线程池,将长流程分段解耦,如:执行器节点注册与任务地址更新,调度计划与触发调度,调度与执行等等

**轻量级**:调度中心只负责管理任务与发起调度,不参与具体的执行过程,保证了高性能与稳定性

**负载均衡**:通过时钟校准与分布式锁保证调度中心公平抢占调度权限,提供多种路由策略实现执行器负载均衡

**线程池隔离**:核心调度线程分为快慢两个线程池,防止某个慢调度任务影响整个调度中心

**精准触发**:待调度任务提前加载,精准(小批量数据纯内存操作)触发调度

3.3、思考

  • 当前的调度时间精度只到秒级,如何支持毫秒级精度调度?提示:如果精度集中控制,会极大增加调度中心的负担,可以考虑在执行器实现
  • 如何实现调度的灰度功能?
  • 加锁后调度中心节点宕机,其它节点能再次获取到锁并执行调度任务吗?

四、参考资料

  • 源码:https://gitee.com/xuxueli0323/xxl-job
  • 官方社区:https://www.xuxueli.com/xxl-job/
  • https://www.cnblogs.com/caison/p/11641161.html
  • https://www.cnblogs.com/davidwang456/p/9057839.html
  • https://www.cnblogs.com/ssslinppp/p/12485273.html
  • 1
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值