分布式任务调度与XXL-JOB
分布式任务调度
理解分布式任务调度,首先拆开理解“分布式”和任务调度“。
任务调度可以理解为“集群中哪些机器什么时候执行什么任务”,任务就是执行的操作,可以对比k8s的Job和CornJob。在单体应用中定时任务还是很容易实现的,但是到了微服务架构和服务分布式时代,服务拆分且多实例,需要“调度”概念来确保任务由具体的某个服务实例执行,否则就会出现多个实例执行同一个任务。
分布式已经是个广泛使用的概念了,在这里用来修饰任务调度平台,要求任务调度平台可以是多实例运行的,且实例之间可以无状态和无限的水平扩展。分布式系统需要具备以下三个特点:
- 分布性:每个部分都可以独立部署,服务之间交互通过网络进行通信,比如:订单服务、商品服务。
- 伸缩性:每个部分都可以集群方式部署,并可针对部分结点进行硬件及软件扩容,具有一定的伸缩能力。
- 高可用:每个部分都可以集群部分,保证高可用。
因此,分布式任务调度就是分布式架构的任务调度平台,任务调度是其功能,分布式是调度平台的架构。
现在林林总总的分布式任务调度框架,包括XXL-JOB,大部分都是在Quartz的基础上进行改进的,Quartz是OpenSymphony开源组织在任务调度领域的一个开源项目,完全基于java实现。作为一个优秀的开源框架,Quartz具有以下特点:强大的调度功能、灵活的应用方式、分布式和集群能力,另外作为spring默认的调度框架,很容易实现与Spring集成,实现灵活可配置的调度功能。
Quartz的组件架构包括:
- Scheduler – 核心调度器,就是任务调度、分配的控制器
- Job – 任务,代表具体要执行的任务,是个接口,里面有默认方法,开发者需要实现该接口,并且业务逻辑写在默认的execute方法中
- JobDetail – 任务描述,描述job的静态消息,是调度器需要的数据,跟Job区分开来,主要是为了一个Job可以在多台机器并行,每个调度器new一个Job的实现类
- Trigger -- 触发器,用于定义任务调度的时间规则
分布式任务调度要解决的一个核心问题是在分布式任务调度平台多实例的情况下,相同的job只被调度执行一次。
Quartz采用的是分布式锁方案,在quartz集群解决方案中有张scheduler_locks,采用了悲观锁的方式对triggers表进行了行加锁,以保证任务同步的正确性。缺点是对于大量的短任务,各个节点都会抢占数据库锁,这样就出现大量的线程等待资源。
setAutoCommit(false)
关闭隐式自动提交事务,启动事务select lock for update
(显式排他锁,其他事务无法进入&无法实现for update
)- 读
db
任务信息 -> 拉任务到内存时间轮 -> 更新db
任务信息 commit
提交事务,同时会释放for update
的排他锁(悲观锁)
xxl-job以Quartz为基础进行优化,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。
对比项 | Quartz | xxl-job |
---|---|---|
依赖 | mysql | mysql,jdk1.7+,maven3.0+ |
集群、弹性扩容 | 多节点,部署,通过竞争数据库锁来保证只有一个节点执行任务 | 使用Quartz基于数据库的分布式功能,服务器超出一定数量会给数据库造成一定的压力 |
任务分片 | 不支持 | 支持 |
管理界面 | 无 | 支持 |
高级功能 | 无 | 弹性扩容,分片广播,故障转移,Rolling实时日志,GLUE(支持在线编辑代码,免发布),任务进度监控,任务依赖,数据加密,邮件报警,运行报表,国际化 |
缺点 | 没有管理界面,以及不支持任务分片等。不适用于分布式场景 | 调度中心通过获取DB锁来保证集群中执行任务的唯一性,如果短任务很多,随着调度中心集群数量增加,那么数据库的锁竞争会比较厉害,性能不好。 |
任务不能重复执行 | 数据库锁 | 使用Quartz基于数据库的分布式功能 |
并行调度 | 调度系统多线程(默认10个线程)触发调度运行,确保调度精确执行,不被堵塞。 | |
失败处理策略 | 调度失败时的处理策略,策略包括:失败告警(默认)、失败重试(界面可配置) | |
动态分片策略 | 分片广播任务以执行器为维度进行分片,支持动态扩容执行器集群从而动态增加分片数量,协同进行业务处理;在进行大数据量业务操作时可显著提升任务处理能力和速度。 执行器集群部署时,任务路由策略选择”分片广播”情况下,一次任务调度将会广播触发对应集群中所有执行器执行一次任务,同时传递分片参数;可根据分片参数开发分片任务; |
XXL-JOB原理和使用
架构与原理
XXL-JOB
是一个轻量级分布式任务调度平台,主打特点是平台化,易部署,开发迅速、学习简单、轻量级、易扩展,代码仍在持续更新中。
调度中心
: 任务调度控制台,平台自身并不承担业务逻辑,只是负责任务的统一管理和调度执行,并且提供任务管理平台执行器
: 负责接收“调度中心”的调度并执行,可直接部署执行器,也可以将执行器集成到现有业务项目中。 通过将任务的调度控制和任务的执行解耦,业务使用只需要关注业务逻辑的开发。
主要特性:
- 简单:支持通过
Web页面
对任务进行CRUD
操作,操作简单,一分钟上手; - 动态:支持_动态修改任务状态、启动/停止任务,以及终止运行中任务,即时生效_;
- 调度中心HA(中心式):调度采用
中心式设计
,调度中心自研调度组件并支持集群部署
,可保证调度中心HA; - 执行器HA(分布式):任务分布式执行,任务"执行器"支持集群部署,可保证任务执行HA;
- 注册中心: 执行器会周期性自动注册任务, 调度中心将会自动发现注册的任务并触发执行。也支持手动录入执行器地址;
- 弹性扩容缩容:一旦有新执行器机器上线或者下线,下次调度时将会重新分配任务;
- 路由策略:执行器集群部署时提供丰富的路由策略,包括:_第一个、最后一个、轮询、随机、一致性HASH、最不经常使用、最近最久未使用、故障转移、忙碌转移_等;
- 故障转移:任务路由策略选择_故障转移_情况下,如果执行器集群中某一台机器故障,将会自动Failover切换到一台正常的执行器发送调度请求。
- 阻塞处理策略:调度过于密集执行器来不及处理时的处理策略,策略包括:单机串行(默认)、丢弃后续调度、覆盖之前调度;
- 任务超时控制:支持_自定义任务超时时间_,任务运行超时将会主动中断任务;
- 任务失败重试:支持_自定义任务失败重试次数_,当任务失败时将会按照预设的失败重试次数主动进行重试;其中分片任务支持分片粒度的失败重试;
- 任务失败警告:默认提供邮件方式失败告警,同时预留扩展接口,可方便的扩展短信、钉钉等告警方式;
- 分片广播任务:执行器集群部署时,任务路由策略选择
分片广播
情况下,一次任务调度将会广播触发集群中所有执行器执行一次任务,可根据分片参数开发分片任务; - 动态分片:分片广播任务以执行器为维度进行分片,支持动态扩容执行器集群从而动态增加分片数量,协同进行业务处理;在进行大数据量业务操作时可显著提升任务处理能力和速度。
- 事件触发:除了
Cron方式
和任务依赖方式
触发任务执行之外,支持基于事件的触发任务方式。调度中心提供触发任务单次执行的API服务,可根据业务事件灵活触发
任务分片广播是xxl-job相比Quartz比较重要的能力提升,可以按照用户自定义分片逻辑进行拆分,分发到集群中不同节点并行执行,提升资源利用效率。适用场景:海量日志统计。
环境搭建与测试
源码下载与环境准备
源码仓库地址为https://github.com/xuxueli/xxl-job
当前xxl-job更新到v3.0.0,依赖Maven3+,Jdk17+和Mysql8.0+,本地使用IDEA直接运行代码,采用云端的Mysql。
初始化数据库
初始化脚本在源码目录的 /doc/db/tables_xxl_job.sql
,将此脚本在MySQL
数据库中执行一遍。
执行完毕,会在MySQL数据库中生成如下 8 张表:
调度中心配置
修改调度中心配置文件
### 调度中心JDBC链接:链接地址请保持和 2.1章节 所创建的调度数据库的地址一致
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=root_pwd
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
### 报警邮箱
spring.mail.host=smtp.qq.com
spring.mail.port=25
spring.mail.username=xxx@qq.com
spring.mail.password=xxx
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true
spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory
### 调度中心通讯TOKEN [选填]:非空时启用;
xxl.job.accessToken=
### 调度中心通讯超时时间[选填],单位秒;默认3s;
xxl.job.timeout=3
### 调度中心国际化配置 [必填]: 默认为 "zh_CN"/中文简体, 可选范围为 "zh_CN"/中文简体, "zh_TC"/中文繁体 and "en"/英文;
xxl.job.i18n=zh_CN
## 调度线程池最大线程配置【必填】
xxl.job.triggerpool.fast.max=200
xxl.job.triggerpool.slow.max=100
### 调度中心日志表数据保存天数 [必填]:过期日志自动清理;限制大于等于7时生效,否则, 如-1,关闭自动清理功能;
xxl.job.logretentiondays=30
启动主程序
调度中心访问地址:http://localhost:8080/xxl-job-admin (该地址执行器将会使用到,作为回调地址)
默认登录账号 “admin/123456”, 登录后运行界面如下图所示。
配置部署“执行器项目”
以源码中的sample-springboot项目为例,配置部署执行器项目。
确认pom文件中引入了 “xxl-job-core” 的maven依赖;
配置application.properties
# web port
server.port=8081
# no web
#spring.main.web-environment=false
# log config
logging.config=classpath:logback.xml
### 调度中心部署根地址 [选填]:如调度中心集群部署存在多个地址则用逗号分隔。执行器将会使用该地址进行"执行器心跳注册"和"任务结果回调";为空则关闭自动注册;
xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin
### 调度中心通讯TOKEN [选填]:非空时启用;
xxl.job.admin.accessToken=default_token
### 调度中心通讯超时时间[选填],单位秒;默认3s;
xxl.job.admin.timeout=3
### 执行器AppName [选填]:执行器心跳注册分组依据;为空则关闭自动注册
xxl.job.executor.appname=xxl-job-executor-sample
### 执行器注册 [选填]:优先使用该配置作为注册地址,为空时使用内嵌服务 ”IP:PORT“ 作为注册地址。从而更灵活的支持容器类型执行器动态IP和动态映射端口问题。
xxl.job.executor.address=
### 执行器IP [选填]:默认为空表示自动获取IP,多网卡时可手动设置指定IP,该IP不会绑定Host仅作为通讯使用;地址信息用于 "执行器注册" 和 "调度中心请求并触发任务";
xxl.job.executor.ip=
### 执行器端口号 [选填]:小于等于0则自动获取;默认端口为9999,单机部署多个执行器时,注意要配置不同执行器端口;
xxl.job.executor.port=9999
### 执行器运行日志文件存储磁盘路径 [选填] :需要对该路径拥有读写权限;为空则使用默认路径;
xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler
### 执行器日志文件保存天数 [选填] : 过期日志自动清理, 限制值大于等于3时生效; 否则, 如-1, 关闭自动清理功能;
xxl.job.executor.logretentiondays=30
执行器组件配置(因xxl-job没有使用spring-boot-starter,需自行将配置类注入到spring容器中。)
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
logger.info(">>>>>>>>>>> xxl-job config init.");
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
xxlJobSpringExecutor.setAppname(appname);
xxlJobSpringExecutor.setIp(ip);
xxlJobSpringExecutor.setPort(port);
xxlJobSpringExecutor.setAccessToken(accessToken);
xxlJobSpringExecutor.setLogPath(logPath);
xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
return xxlJobSpringExecutor;
}
正确配置以上内容后,执行Springboot应用主程序,就能运行执行器。此时打开调度中心,查看执行器就行看到在线的执行器。
这里AppName为xxl-job-executor-sample的执行器已经创建好了,如果是其他执行器,要先新增执行器管理,然后执行器才能注册进来。
执行器集群部署时,几点要求和建议:
- 执行器回调地址(xxl.job.admin.addresses)需要保持一致;执行器根据该配置进行执行器自动注册等操作。
- 同一个执行器集群内AppName(xxl.job.executor.appname)需要保持一致;调度中心根据该配置动态发现不同集群的在线执行器列表。
BEAN模式执行任务
Bean模式任务,支持基于类的开发方式,每个任务对应一个Java类。
- 优点:不限制项目环境,兼容性好。
- 缺点:
- 每个任务需要占用一个Java类,造成类的浪费;
- 不支持自动扫描任务并注入到执行器容器,需要手动注入。
首先在执行器项目中,开发Job方法,如service.jobhandler.SampleXxlJob中的demoJobHandler方法,步骤如下:
- 任务开发:在Spring Bean实例中,开发Job方法;
- 注解配置:为Job方法添加注解 "@XxlJob(value="自定义jobhandler名称", init = "JobHandler初始化方法", destroy = "JobHandler销毁方法")",注解value值对应的是调度中心新建任务的JobHandler属性的值。
- 执行日志:需要通过 "XxlJobHelper.log" 打印执行日志;
- 任务结果:默认任务结果为 "成功" 状态,不需要主动设置;如有诉求,比如设置任务结果为失败,可以通过 "XxlJobHelper.handleFail/handleSuccess" 自主设置任务结果;
其次,在调度中心中新建调度任务,运行模式选中 “BEAN模式”,JobHandler属性填写任务注解“@XxlJob”中定义的值;
测试执行一次任务
查看调度日志,可以发现任务成功调度,查看执行日志,可以查看到log信息。
启动任务
任务设置的是每分钟的00秒执行一次,可以看到任务被准时调度并执行。
GLUE模式执行任务
“GLUE模式”的执行代码托管到调度中心在线维护,相比“Bean模式任务”需要在执行器项目开发部署上线,更加简便轻量;当前支持Java、Shell、Python、PHP、NodeJS、PowerShell。
直接在创建任务,选择之前部署的执行器项目。运行模式选中 “GLUE模式(Java)”,无需设置JobHandler。
选中指定任务,点击该任务右侧“GLUE IDE”按钮,将会前往GLUE任务的Web IDE界面,在该界面支持对任务代码进行开发
package com.xxl.job.service.handler;
import com.xxl.job.core.context.XxlJobHelper;
import com.xxl.job.core.handler.IJobHandler;
import java.util.concurrent.TimeUnit;
public class DemoGlueJobHandler extends IJobHandler {
@Override
public void execute() throws Exception {
XxlJobHelper.log("XXL-JOB, Hello World.");
for (int i = 0; i < 5; i++) {
XxlJobHelper.log("beat at:" + i);
System.out.println("beat at:" + i); //相比bean增加控制台打印,证明代码可以在本地服务中运行
TimeUnit.SECONDS.sleep(2);
}
}
}
任务执行一次,控制台可以打印结果。