后端服务都无法避免遇到需要定时调度任务的场景,为了满足需求,介绍一款 “开发迅速、学习简单、轻量级、易扩展” 的分布式任务调度平台xxl-job。 现已开放源代码并接入多家公司线上产品线,开箱即用。
一、介绍
1.1 主要部分
xxl-job 主要包含2部分:
-
调度中心(xxl-job-admin)
管理调度任务,负责触发调度执行,并且提供web任务管理平台。
运行报表:统计任务的执行状态
执行器管理:注册调度任务执行的AppName, 其由服务IP+端口列表组成
任务管理:在执行器下,设置需要调度的任务
新增任务:
- 基础配置:执行器、任务描述、负责人、任务失败后的报警邮箱
- 调度配置:调度类型(默认CRON)、CRON表达式
- 任务配置:运行模式分BEAN 和 GLUE, JobHandler(执行器服务中声明的Handler)、任务参数(可选)
- 高级配置:路由策略(多个执行服务的路由策略)、子任务ID(任务完成后继续执行的任务ID)、调度过期策略(调度中心错过调度时间的补偿处理策略)、阻塞处理策略、任务超时时间、失败重试次数
调度日志:筛选和查看各定时任务执行的日志
-
执行器
负责接收调度中心的请求并执行任务逻辑。
执行器 就是在 后台服务 中内嵌Server , 来支持 调度中心 的调用,服务地址通过appname归类
使用ip+port来确定执行器地址。
1.2 流程
1.3 架构图
二、使用
Xxl-job对 java 的支持度较好, 同时也提供 RESTful API 服务,从而方便对其他语言的支持。
2.1 java项目(with SpringBoot)
- 在
pom.xml中添加xxl-job-core包
<!--定时任务所需要的jar包 -->
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
<version>2.3.0</version>
</dependency>
- 配置
configuration以及 配置类
在application.yml添加配置,如果使用配置服务例如Spring Cloud Config或Nacos等配置服务时修改对应服务的配置文件即可。
#xxljob 分布式调度配置
xxl-job:
appname: my-local-job-executor
port: 10003
addresses: http://127.0.0.1:8080/xxl-job-admin
accessToken: token
logPath: /data/applogs/xxl-job/jobhandler
logRetentionDays: 30
@Configuration
@ConfigurationProperties(prefix = "xxl-job")
@Component
@Slf4j
@Data
public class XxlJobConfiguration {
private String appname;
private int port;
private String addresses;
private String accessToken;
private String logPath;
private int logRetentionDays;
@Bean(initMethod = "start", destroyMethod = "destroy")
public XxlJobSpringExecutor xxlJobExecutor() {
log.info(">>>>>>>>>>> {} xxl-job config init on address: {}", appname, addresses);
XxlJobSpringExecutor xxlJobExecutor = new XxlJobSpringExecutor();
xxlJobExecutor.setAppname(appname);
xxlJobExecutor.setAdminAddresses(addresses);
xxlJobExecutor.setAccessToken(accessToken);
xxlJobExecutor.setPort(port);
xxlJobExecutor.setLogPath(logPath);
xxlJobExecutor.setLogRetentionDays(logRetentionDays);
return xxlJobExecutor;
}
}
- 添加
JobHandler
@Service
public class DemoJobHandler {
@XxlJob("DemoJobHandler")
public void execute(String params) throws Exception {
....
}
}
- 在
xxl-job-adminweb平台注册执行器和配置任务
新增执行器,填入配置文件中的appname ,名字 和 注册方式选“自动注册”。
新增定时任务,运行方式选在BEAN,JobHandler填写项目中DemoJob``Handler
2.2 通过RESTful API
xxl-job支持RESTful API,已达到跨语言的调用。
API主要包括:
调度中心: 触发任务、中止任务
执行器:注册、查询日志
具体可以查看官方:调度中心/执行器 RESTful API(有些字段已过时,需要查看代码)
下面以Rails项目为例,介绍一下RESTful API的使用过程。
- 执行器(后台应用服务)自动注册 到 调度平台 (可选)
# config/initializers/register_job.rb
JobLogger = Logger.new('log/job.log')
begin
require 'httparty'
url = 'http://127.0.0.1:8080/xxl-job-admin/api/registry' //调度平台注册api
request_body = {}
request_body['registryGroup'] = 'EXECUTOR' // 固定值
request_body['registryKey'] = 'rails-local-executor' // 执行器AppName
request_body['registryValue'] = 'http://192.168.1.20:3000/' // 执行器地址,内置服务跟地址
response = HTTParty.post(url,
headers: { 'Content-Type' => 'application/json', 'XXL-JOB-ACCESS-TOKEN' => 'token' },
body: request_body.to_json,
debug_output: JobLogger)
response_body = JSON.parse(response.body)
if response_body['code'] == 200
JobLogger.info 'Register xxl-job-admin success!'
else
JobLogger.warn "Register xxl-job-amin fail! --> #{response_body}"
end
rescue StandardError
end
- 在调度平台配置执行器地址
推荐将注册方式 设置为 手动录入 可以免去自动注册的麻烦
- 在执行器中实现执行调度任务和查询日志的接口
class JobsController < ApplicationController
skip_before_action :verify_authenticity_token
def run
JobLogger.info "-run-params-------->#{params}"
# {"jobId"=>10, "executorHandler"=>"RestJobHandler", "executorParams"=>"", "executorBlockStrategy"=>"SERIAL_EXECUTION", "executorTimeout"=>0, "logId"=>24, "logDateTime"=>1652199074005, "glueType"=>"BEAN", "glueSource"=>"", "glueUpdatetime"=
# >1652198295000, "broadcastIndex"=>0, "broadcastTotal"=>1, "controller"=>"jobs", "action"=>"run", "job"=>{"jobId"=>10, "executorHandler"=>"RestJobHandler", "executorParams"=>"", "executorBlockStrategy"=>"SERIAL_EXECUTION", "executorTimeout"=>0, "logId"=>2
# 4, "logDateTime"=>1652199074005, "glueType"=>"BEAN", "glueSource"=>"", "glueUpdatetime"=>1652198295000, "broadcastIndex"=>0, "broadcastTotal"=>1}}
JobLogger.info "[#{params[:logId]}]: job start running..."
# [{
# "logId":1, // 本次调度日志ID
# "logDateTim":0, // 本次调度日志时间
# "executeResult":{
# "handleCode": 200, // 200 表示任务执行正常,500表示失败
# "handleMsg": null
# }
# }]
3.times do |index|
JobLogger.info "[#{params[:logId]}]: #{index} working!"
end
JobLogger.info "[#{params[:logId]}]: job finish!"
# callback
url = 'http://127.0.0.1:8080/xxl-job-admin/api/callback'
request_body = []
request_body[0] = {}
request_body[0]['logId'] = params[:logId]
request_body[0]['logDateTim'] = (Time.now.to_f * 1000).to_i
request_body[0]['handleCode'] = 200
request_body[0]['handleMsg'] = nil
response = HTTParty.post(url,
headers: { 'Content-Type' => 'application/json', 'XXL-JOB-ACCESS-TOKEN' => 'token' },
body: request_body.to_json,
debug_output: JobLogger)
response_body = JSON.parse(response.body)
if response_body['code'] == 200
JobLogger.info "[#{params[:logId]}]: run success!"
else
JobLogger.error "[#{params[:logId]}]: run fail!"
end
render json: { code: 200, msg: nil }
end
def beat
render json: { code: 200, msg: nil }
end
def kill; end
def remove; end
def log
# {
# "logDateTim":0, // 本次调度日志时间
# "logId":0, // 本次调度日志ID
# "fromLineNum":0 // 日志开始行号,滚动加载日志
# }
JobLogger.info "-log-params-------->#{params}"
content = []
File.readlines("#{Rails.root}/log/job.log").each do |line|
content << line if line.include?("[#{params[:logId]}]")
end
# {
# "code":200, // 200 表示正常、其他失败
# "msg": null // 错误提示消息
# "content":{
# "fromLineNum":0, // 本次请求,日志开始行数
# "toLineNum":100, // 本次请求,日志结束行号
# "logContent":"xxx", // 本次请求日志内容
# "isEnd":true // 日志是否全部加载完
# }
# }
render json: { code: 200, msg: nil, content: { fromLineNum: params[:fromLineNum], toLineNum: content.size, logContent: content.join, isEnd: true } }
end
end
- 在调度平台添加定时任务
- 启用定时任务,查看调度日志