分布式任务调度平台xxl-job

1,714 阅读5分钟

后端服务都无法避免遇到需要定时调度任务的场景,为了满足需求,介绍一款 “开发迅速、学习简单、轻量级、易扩展” 的分布式任务调度平台xxl-job。 现已开放源代码并接入多家公司线上产品线,开箱即用。

一、介绍

1.1 主要部分

xxl-job 主要包含2部分:

  1. 调度中心(xxl-job-admin)

管理调度任务,负责触发调度执行,并且提供web任务管理平台。

运行报表:统计任务的执行状态

执行器管理:注册调度任务执行的AppName, 其由服务IP+端口列表组成

任务管理:在执行器下,设置需要调度的任务

新增任务

  1. 基础配置:执行器、任务描述、负责人、任务失败后的报警邮箱
  2. 调度配置:调度类型(默认CRON)、CRON表达式
  3. 任务配置:运行模式分BEAN 和 GLUE, JobHandler(执行器服务中声明的Handler)、任务参数(可选)
  4. 高级配置:路由策略(多个执行服务的路由策略)、子任务ID(任务完成后继续执行的任务ID)、调度过期策略(调度中心错过调度时间的补偿处理策略)、阻塞处理策略、任务超时时间、失败重试次数

调度日志:筛选和查看各定时任务执行的日志

  1. 执行器

负责接收调度中心的请求并执行任务逻辑。

执行器 就是在 后台服务内嵌Server , 来支持 调度中心 的调用,服务地址通过appname归类

使用ip+port来确定执行器地址。

1.2 流程

1.3 架构图

image.png

二、使用

Xxl-job对 java 的支持度较好, 同时也提供 RESTful API 服务,从而方便对其他语言的支持。

2.1 java项目(with SpringBoot)

  1. pom.xml中添加xxl-job-core
 <!--定时任务所需要的jar包 -->
<dependency>
  <groupId>com.xuxueli</groupId>
  <artifactId>xxl-job-core</artifactId>
  <version>2.3.0</version>
</dependency>
  1. 配置configuration 以及 配置类

application.yml添加配置,如果使用配置服务例如Spring Cloud ConfigNacos等配置服务时修改对应服务的配置文件即可。

 #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;
    }
}
  1. 添加 JobHandler
@Service
public class DemoJobHandler {

    @XxlJob("DemoJobHandler")
    public void execute(String params) throws Exception {
        ....
    }
}
  1. xxl-job-adminweb平台注册执行器和配置任务

新增执行器,填入配置文件中的appname ,名字 和 注册方式选“自动注册”。

新增定时任务,运行方式选在BEAN,JobHandler填写项目中DemoJob``Handler

2.2 通过RESTful API

xxl-job支持RESTful API,已达到跨语言的调用。

API主要包括:

调度中心: 触发任务、中止任务

执行器:注册、查询日志

具体可以查看官方:调度中心/执行器 RESTful API(有些字段已过时,需要查看代码)

下面以Rails项目为例,介绍一下RESTful API的使用过程。

  1. 执行器(后台应用服务)自动注册 到 调度平台 (可选)
# 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
  1. 在调度平台配置执行器地址

推荐将注册方式 设置为 手动录入 可以免去自动注册的麻烦

  1. 在执行器中实现执行调度任务和查询日志的接口
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
  1. 在调度平台添加定时任务

  1. 启用定时任务,查看调度日志

四、参考