参考 分布式任务调度平台xxl-job
源码下载地址 https://github.com/xuxueli/xxl-job/releases
一、需求背景
以往系统中的周期性任务执行,一般有以下几种方式
- 引入quartz or spring-task ,每一个任务对应编写一个执行类,配置调度规则执行
- 使用系统的执行器,比如centos的crontab,每一个任务对应编写一个执行类,并在crontab 配置调度规则
此时,又会出现以下的问题
- 任务出现异常,需增加报警功能
- 任务出现阻塞,比如上一次任务没执行完,下次任务会排队等待,以此累积
- 任务超时,需自己主动中断任务
- 单机任务调度下,系统出现异常,比如内存爆掉,磁盘空间不够用
- 任务失败,需hardcode 指定次数重试
- 在没有配日志可视化的前提下,查看日志需要去服务器上敲命令,面临rm -rf *的风险
- ...
二、什么是xxl-job
XXL-JOB是一个轻量级分布式任务调度平台,支持通过 Web 页面对任务进行 CRUD 操作,支持动态修改任务状态、暂停/恢复任务,以及终止运行中任务,支持在线配置调度任务入参和在线查看调度结果。
它的特点有:
平台:
调度中心式设计
- 解耦
- 用DB实现注册中心,实现轻量级部署
- 弹性扩、缩容
- 故障转移:执行器集群某台机器出现故障,能够自动切换正常的执行器
- 分片广播:且支持动态分片,提升任务运行效率
- 路由策略:执行器集群部署时提供丰富的路由策略,包括:第一个、最后一个、轮询、随机、一致性HASH、最不经常使用、最近最久未使用、故障转移、忙碌转移等;
-
全异步化
:异步调度和异步执行。
任务:
任务分布式执行
- 简单:支持通过web对任务进行crud
- 灵活:动态修改任务状态,自定义失败重试次数等,且即时生效
- 告警:任务失败,支持邮件告警
三、概念说明
任务
调度任务,系统角色中的最小单元
- 可通过webide(GLUE模式) 或者ide(BEAN模式)自定义任务内容
- 支持多种路由策略和阻塞处理策略的选择
- 支持自定义失败重试次数和任务超时时间
- 支持任务失败告警
- 支持动态修改任务状态和各个配置策略的修改
调度中心
统一管理任务调度平台上的调度任务,负责触发调度执行,并提供可视化平台管理
执行器
主要负责接收“调度中心”的调度并执行
四、安装启动
下载
https://github.com/xuxueli/xxl-job
环境要求
- Maven3+
- JDK1.7+
- MYSQL5.6+
入门
-
初始化数据库
sql脚本在源码内,用navicat工具导入即可
doc/db/tables_xxl_job.sql
-
调度数据库配置说明
- xxl_job_lock:任务调度锁表;
- xxl_job_group:执行器信息表,维护任务执行器信息;
- xxl_job_info:调度扩展信息表: 用于保存XXL-JOB调度任务的扩展信息,如任务分组、任务名、机器地址、执行器、执行入参和报警邮件等等;
- xxl_job_log:调度日志表: 用于保存XXL-JOB任务调度的历史信息,如调度结果、执行结果、调度入参、调度机器和执行器等等;
- xxl_job_logglue:任务GLUE日志:用于保存GLUE更新历史,用于支持GLUE的版本回溯功能;
- xxl_job_registry:执行器注册表,维护在线的执行器和调度中心机器地址信息;
- xxl_job_user:系统用户表;
-
项目结构
xxl-job-admin:调度中心
xxl-job-core:公共依赖
xxl-job-executor-samples:执行器Sample示例(选择合适的版本执行器,可直接使用,也可以参考其并将现有项目改造成执行器)
:xxl-job-executor-sample-springboot:Springboot版本,通过Springboot管理执行器,推荐这种方式;
:xxl-job-executor-sample-spring:Spring版本,通过Spring容器管理执行器,比较通用;
:xxl-job-executor-sample-frameless:无框架版本;
:xxl-job-executor-sample-jfinal:JFinal版本,通过JFinal管理执行器;
:xxl-job-executor-sample-nutz:Nutz版本,通过Nutz管理执行器;
-
配置部署“调度中心”(xxl-job-admin)
更改配置
### web server.port=8080 server.context-path=/xxl-job-admin ### resources spring.mvc.static-path-pattern=/static/** spring.resources.static-locations=classpath:/static/ ### freemarker spring.freemarker.templateLoaderPath=classpath:/templates/ spring.freemarker.suffix=.ftl spring.freemarker.charset=UTF-8 spring.freemarker.request-context-attribute=request spring.freemarker.settings.number_format=0.########## ### mybatis mybatis.mapper-locations=classpath:/mybatis-mapper/*Mapper.xml ### xxl-job, datasource spring.datasource.url=jdbc:mysql://127.0.0.1:3306/xxl_job?Unicode=true&characterEncoding=UTF-8 spring.datasource.username=root spring.datasource.password=123456 spring.datasource.driver-class-name=com.mysql.jdbc.Driver spring.datasource.type=org.apache.tomcat.jdbc.pool.DataSource spring.datasource.tomcat.max-wait=10000 spring.datasource.tomcat.max-active=30 spring.datasource.tomcat.test-on-borrow=true spring.datasource.tomcat.validation-query=SELECT 1 spring.datasource.tomcat.validation-interval=30000 ### xxl-job email spring.mail.host=smtp.qq.com spring.mail.port=25 [email protected] 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 ### xxl-job, access token xxl.job.accessToken= ### xxl-job, i18n (default empty as chinese, "en" as english) xxl.job.i18n=
部署项目
方式一:项目打包部署
打包xxl-job-admin
-
调度中心访问地址:http://localhost:8080/xxl-job-admin (该地址执行器将会使用到,作为回调地址)
默认登录账号:admin 默认登录密码:123456
方式二:Docker镜像搭建
- 下载镜像
docker pull xuxueli/xxl-job-admin
-
创建容器
由于官方镜像的配置文件(application.properties)是默认的,这里需要通过 "PARAMS" 指定,参数格式 RAMS="--key=value --key2=value2" 去修改;
docker run -e PARAMS="--spring.datasource.url=jdbc:mysql://127.0.0.1:3306/xxl_job? Unicode=true&characterEncoding=UTF-8" -p 8082:8080 -v /tmp:/data/applogs --name xxl-job-admin -d xuxueli/xxl-job-admin
-
运行
mvn clean package docker build -t xuxueli/xxl-job-admin ./xxl-job-admin docker run --name xxl-job-admin -p 8080:8080 -d xuxueli/xxl-job-admin
调度中心集群(可选 )
调度中心支持集群部署,提升调度系统容灾和可用性。
调度中心集群部署时,几点要求和建议:
- DB配置保持一致;
- 登陆账号配置保持一致;
- 集群机器时钟保持一致(单机集群忽视);
- 建议:推荐通过nginx为调度中心集群做负载均衡,分配域名。调度中心访问、执行器回调配置、调用API服务等操作均通过该域名进行。
-
配置部署“执行器项目”(xxl-job-executor-sample)
执行器配置application.properties
# web port server.port=8081 # log config logging.config=classpath:logback.xml ### 调度中心部署跟地址 [选填]:如调度中心集群部署存在多个地址则用逗号分隔。执行器将会使用该地址进行"执行器心跳注册"和"任务结果回调";为空则关闭自动注册 xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin ### 执行器AppName [选填]:执行器心跳注册分组依据;为空则关闭自动注册 xxl.job.executor.appname=xxl-job-executor-sample ### 执行器IP [选填]:默认为空表示自动获取IP,多网卡时可手动设置指定IP,该IP不会绑定Host仅作为通讯实用;地址信息用于 "执行器注册" 和 "调度中心请求并触发任务" xxl.job.executor.ip= ### 执行器端口号 [选填]:小于等于0则自动获取;默认端口为9999,单机部署多个执行器时,注意要配置不同执行器端口 xxl.job.executor.port=9999 ### 执行器通讯TOKEN [选填]:非空时启用 xxl.job.accessToken= ### 执行器运行日志文件存储磁盘路径 [选填] :需要对该路径拥有读写权限;为空则使用默认路径 xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler ### 执行器日志保存天数 [选填] :值大于3时生效,启用执行器Log文件定期清理功能,否则不生效 xxl.job.executor.logretentiondays=-1
部署项目
官方提供了多种执行器的示例项目,这里默认使用springboot。
执行器集群(可选)
执行器支持集群部署,提升调度系统可用性,同时提升任务处理能力。
执行器集群部署时,几点要求和建议:
- 执行器回调地址(xxl.job.admin.addresses)需要保持一致;执行器根据该配置进行执行器自动注册等操作。
- 同一个执行器集群内AppName(xxl.job.executor.appname)需要保持一致;调度中心根据该配置动态发现不同集群的在线执行器列表。
五、开发
任务运行模式主要有Bean模式和Gule模式(理解为脚本)
BEAN模式
在该模式下,任务的具体实现逻辑是 以JobHandler的形式存在于“执行器项目”中。
- 步骤一:新建执行器项目
这里只要copy 项目xxl-job-executor-sample-springboot
即可
- 步骤二:修改执行器appname
logging:
config: "classpath:logback.xml"
server:
port: 8081
xxl:
job:
accessToken: ''
admin:
addresses: http://127.0.0.1:8080/xxl-job-admin
executor:
appname: xxl-job-executor-test
ip: ''
logpath: /data/applogs/xxl-job/jobhandler
logretentiondays: -1
port: 9999
- 步骤三:新建一个任务Handler:TestJobHandler
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.handler.IJobHandler;
import com.xxl.job.core.handler.annotation.JobHandler;
import com.xxl.job.core.log.XxlJobLogger;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* Title:lyy-modular-saas-api
* Desc: 运行模式为Bean模式
* 开发步骤:
* 1、继承"IJobHandler":“com.xxl.job.core.handler.IJobHandler”;
* 2、注册到Spring容器:添加“@Component”注解,被Spring容器扫描为Bean实例;
* 3、注册到执行器工厂:添加“@JobHandler(value="自定义jobhandler名称")”注解,注解value值对应的是调度中心新建任务的JobHandler属性的值。
* 4、执行日志:需要通过 "XxlJobLogger.log" 打印执行日志;
* @author Jerry
* @version 1.0
* @since 2019/8/8
*/
@JobHandler(value = "testJobHandler")
@Component
public class TestJobHandler extends IJobHandler {
@Override
public ReturnT execute(String s) throws Exception {
XxlJobLogger.log("################# start to job test");
for (int i = 0; i < 5; i++) {
XxlJobLogger.log("beat at:" + i);
TimeUnit.SECONDS.sleep(2);
}
XxlJobLogger.log("################# end to job test");
return SUCCESS;
}
}
- 步骤四:执行器管理,新增执行器
- 步骤五:任务管理,新建调度任务
- 步骤六:执行任务,并查看日志
可以看到,这里成功执行了先前自定义的调度任务,并输出了log,结合代码分析,任务执行的log通过XxlJobLogger.log
记录并写到日志表内,调度中心后台读取日志表得到详细的log
GLUE模式
任务以源码方式维护在调度中心,支持通过Web IDE在线更新,实时编译和生效,因此不需要指定JobHandler
拿GULE模式(JAVA)来解释,每一个GLUE模式的代码,在“执行器”接受到调度请求时,会通过Groovy类加载器加载出代码,并实例化成Java对象,同时注入此代码中声明的Spring服务(这里要确保代码中所引用的类或服务在“执行器项目中存在”),接着调用该对象的execute方法,执行具体的任务逻辑。
-
步骤一:任务管理,新建调度任务
-
步骤二:点击操作,选择GLUE IDE,进入Web Ide界面
任务执行失败-邮件告警
- 步骤一:在之前
3.3.1 更改配置
中,有相关邮件报警的发送方配置,配置即可
### 这里是用腾讯企业邮箱测试
spring.mail.host=smtp.exmail.qq.com
spring.mail.port=465
[email protected]
spring.mail.password=123456
- 步骤二:在
任务管理,新建调度任务
中,配置接收告警邮件的邮箱,多个用“,”隔开即可
相关的处理逻辑在调度中心的JobFailMonitorHelper
类中
分片广播&动态分片
执行器集群部署时,任务路由策略选择"分片广播"情况下,一次任务调度将会广播触发对应集群中所有执行器执行一次任务,同时系统自动传递分片参数;可根据分片参数开发分片任务。
使用场景:
分片任务场景:10个执行器的集群来处理10w条数据,每台机器只需要处理1w条数据,耗时降低10倍;
广播任务场景:广播执行器机器运行shell脚本、广播集群节点进行缓存更新等
-
步骤一:执行器集群部署,暂设置为2个节点
执行器管理可以看到:一个APP有两个机器地址
-
步骤二:新增or更新任务,更改路由策略为分片广播
-
步骤三:编写业务代码
// 分片参数 ShardingUtil.ShardingVO shardingVO = ShardingUtil.getShardingVo(); log.info("分片参数:当前分片序号 = {}, 总分片数 = {}", shardingVO.getIndex(), shardingVO.getTotal()); XxlJobLogger.log("分片参数1:当前分片序号 = {}, 总分片数 = {}", shardingVO.getIndex(), shardingVO.getTotal()); // 业务逻辑 // ...
-
步骤四:执行任务,查看运行日志
# 节点1 2019-08-09 09:21:38 [com.xxl.job.executor.service.jobhandler.ShardingJobHandler#execute]-[30]-[Thread-14] 分片参数:当前分片序号 = 0, 总分片数 = 2 2019-08-09 09:21:38 [com.xxl.job.executor.service.jobhandler.ShardingJobHandler#execute]-[35]-[Thread-14] 第 0 片, 命中分片开始处理 2019-08-09 09:21:38 [com.xxl.job.executor.service.jobhandler.ShardingJobHandler#execute]-[37]-[Thread-14] 第 1 片, 忽略 # 节点2 2019-08-09 09:21:38 [com.lyyopen.saas.service.jobhandler.ShardingJobHandler#execute]-[29]-[Thread-16] 分片参数1:当前分片序号 = 1, 总分片数 = 2 2019-08-09 09:21:38 [com.lyyopen.saas.service.jobhandler.ShardingJobHandler#execute]-[36]-[Thread-16] 第 0 片, 忽略 2019-08-09 09:21:38 [com.lyyopen.saas.service.jobhandler.ShardingJobHandler#execute]-[34]-[Thread-16] 第 1 片, 命中分片开始处理
可以看到一个集群的两个节点都收到了分片调度请求,业务逻辑即可针对对应的分片序号进行业务逻辑分片处理!
六、架构设计
设计思想
实现 调度+任务
两者解耦
调度行为在调度中心(admin后台),负责发起调度请求
任务抽象化为一个个JobHandler,交由“
执行器
”统一管理,“执行器
”负责接收调度请求并找到对应的JobHandler,执行具体的业务逻辑(execute)
系统组成
- 调度模块(调度中心): 负责管理调度信息,按照调度配置发出调度请求,自身不承担业务代码。调度系统与任务解耦,提高了系统可用性和稳定性,同时调度系统性能不再受限于任务模块; 支持可视化、简单且动态的管理调度信息,包括任务新建,更新,删除,GLUE开发和任务报警等,所有上述操作都会实时生效,同时支持监控调度结果以及执行日志,支持执行器Failover。
- 执行模块(执行器): 负责接收调度请求并执行任务逻辑。任务模块专注于任务的执行等操作,开发和维护更加简单和高效; 接收“调度中心”的执行请求、终止请求和日志请求等。
架构图
架构特性
-
解耦
调度中心
通过类rpc的调用模式,调用执行器
暴露的远程服务;详见日志远程调用源码:
com.xxl.rpc.remoting.invoker.reference.XxlRpcReferenceBean#getObject
-
线程池
调度采用线程池方式实现,避免用单线程出现阻塞而导致任务调度延迟;
-
并行调度
调度模块采用并行机制,而调度传递到
执行器
则是串行执行 多种路由策略
-
过期处理策略
过期5s内立即触发一次,过期超5s则忽略
-
日志回调服务
“执行器”
在收到调度请求并执行任务后,会将任务的执行结果回调通知给调度中心,调度中心再对应更新日志表。执行器触发回调线程处理源码:
com.xxl.job.core.thread.TriggerCallbackThread#doCallback
调度中心接受回调源码:
com.xxl.job.admin.service.impl.AdminBizImpl#callback(com.xxl.job.core.biz.model.HandleCallbackParam)
-
任务集群
执行器如若集群部署,调度中心将会感知到在线的所有执行器
-
全异步化
- 异步调度:调度中心每次任务触发时仅发送一次调度请求,该调度请求首先推送“异步调度队列”,然后异步推送给远程执行器
- 异步执行:执行器会将请求存入“异步执行队列”并且立即响应调度中心,异步运行。
-
轻量级设计
XXL-JOB调度中心中每个JOB逻辑非常 “轻”,在全异步化的基础上,单个JOB一次运行平均耗时基本在 "10ms" 之内(基本为一次请求的网络开销);因此,可以保证使用有限的线程支撑大量的JOB并发运行
通讯流程分析
-
调度中心
向执行器
发送http调度请求: “执行器”中接收请求的服务,实际上是一台内嵌Server,默认端口9999; -
执行器
执行任务逻辑 -
执行器
http回调调度中心
调度结果:调度中心
中接收回调的服务,是针对执行器
开放一套API服务;