2017 年 10 月 22 日,又拍云 Open Talk 联合 Spring Cloud 中国社区成功举办了“进击的微服务实战派上海站”。大众点评研发工程师许雪里作了《XXL-JOB :分布式任务调度平台》,以下是分享整理:

一、XXL-JOB 简介

(一)分布式系统对任务调度的几点要求

随着公司推进微服务,我们一些 JOB 之前单体运行的时候,可能开发在某一个项目里面,这个时候我们需要一个平台去维护与开发。在这种场景下对一些任务调度的时候,会有下述我总结出来六点的要求。

1、平台

平台有一个好处,可以把我们 JOB 开发的基础站统一起来,新手学习的时候,付出一些学习成本就可以快速开发;另外我们把所有的之前散落在各个项目中间的 JOB 汇总起来,至于一些相通的业务逻辑,尤其是我们任务出现依赖关系的时候,一些子任务依赖的任务逻辑,可以完全通过流程图的方式进行复用;第三就是自维护和扩展,一旦把这个任务平台抽象成服务时,我们在这个平台进行升级一些功能扩展的时候,就不需要挨个去对接各个业务,直接在平台上扩展就好。

2、HA/集群

这其实是两个方面的。任务调度可以拆成两个词,一个叫调度,一个叫任务。我们把这些平时写的 JOB 中一些调不动的属性抽离出来,其实这就是一个调度中心。但是写 JOB 更重要的一块是要实现一些业务逻辑,这些就是代码的业务,也就是任务。调度中心要保证它的高可用,这样即使调度中心其中一台宕机了,至少保持一台正常地出发调度。针对我们的任务机器,假如说一个大数据量的任务,可能现在三台机器把数据进行分辨处理,突然某一天数据量很大,业务发展很快的情况下,可以很快的对任务机器进行集群,这样面对这些激增的任务数据的时候,可以高效处理。

3、弹性扩容

这是针对我们任务的执行机器,也就是执行器。刚才也讲到在我们业务发展的时候,这个季度我们可能业务量是百万,下个月可能就是千万了,甚至更大的数据,针对这个数量的时候,执行器可以支持快速的或者是线上动态的、弹性的扩容。

4、故障处理

故障处理有 failover、失败告警。假如两点钟你收到这个失败告警的时候,目前默认是通过邮件的策略,以及预留了一些告警的接口。当任务失败了,推送一条消息。假如你看到的情况下,可以快速登录到调度系统,手动触发一次,下一次可能这个 JOB 的业务逻辑已经跑完了,等到八点的时候,老板要这份报表的时候可以正常的交给他。如果你设置了failover,即我们的故障处理的策略,它在任务跑失败的情况下会自动运行,自动重试。

5、阻塞处理

在频率很高任务场景的时候,比如每天 PU 量的计算,可能一个任务要运行一个小时,这时候多次任务调度,可能会产生堆积。任务堆积的时候你需要提供一些策略,创新处理的话可以提供一个队列。另外还有一种处理策略,把之前的 case 掉,重新跑一次 JOB。假如说你认可之前 JOB 跑的业务逻辑的话,也可以选择另外一种,把之前的标记它成功,后面一次标记为失败。

6、高性能

XXL-JOB 对调度的整个环节进行了全异步化,一条任务进行触发的时候,第一步它从触发的调度中心到你执行器的时候,这时候会通过队列的方式。调度中心把信号触发回去之后,立刻入队列,结果立即响应,这是第一个触发的过程。执行器接到信号之后,会走第二步,会自己每个任务隔离的有一个队列,然后挨个的处理你的消息,处理完之后针对单条消息有一个批量回调。回调也有一个队列。在触发和执行这两个地方,XXL-JOB 都进行了缓存和队列的处理。

7、自运维

在写 JOB 的时候,你排查一个现场任务,比如你一个 JOB 怎么样确认它有没有跑成功,以及跑的日志是怎么样子的,如何把当前跑的日志跟其他的日志剥离开。尤其是你跑一个分辨任务的时候,可能插个十片的时候,你怎么确定第五片的 JOB 是跑成功的,而且是跑的日志的每一条数据都记录出来。之前我的做法是上每一台机器 S 登上去,把日志拉下来。但是会发现,假如说一个执行器上面写多个任务的话,它们的任务是耦合在一起的任务 log,这样基本上想去隔离每个任务打的 log 几乎是不可能的。

所以 XXL-JOB 做了另一件事情,第一个版本是基于 logFor 件自研了日志的组件,后面有一些用户他们对 logfor 件不太满意,希望用 logback 这种,所以在这个基础之上做了一个自研的日志组件,通过日志组件把每一个任务,每一次调度的日志单独写小文件。你只要登录到我们的XXL-JOB 调度中心的平台上,就可以看到这个任务它的每一次调度,它触发的状态,触发的时间状态、参数,以及在执行器上执行的时间、状态,以及中间的一些流程。甚至在你的执行过程中,你点进去的时候,可以在远程的拉,每一个任务,每一次触发小日志的脚本,小日志触发日志文件,以轮循的方式,瀑布流的方式,一行一行地加载出来

(二)XXL-JOB 简介

XXL-JOB 是一个轻量级分布式任务调度框架,拥有上面提到的七个特点。它核心原理就是有一个模块叫做调度中心,另外一个模块叫做执行性器,它会把调度和任务执行,隔离成两个部分。这样你的调度模块只需要负责你任务的一些调度属性,触发你的调度信号就行了。你执行模块只需要接入信号,去执行具体的业务逻辑,两者可以各自的进行扩容。

调度中心目前是只有 Quartz,这个我们遇到一些限制,虽然享受着它的一些特性。

image.png

XXL-JOB 架构图 v1.8

上图是 XXL-JOB 1.8 的架构图,首先会维护一些执行器管理的的一些注册 APP name。一个 APP name 对应一个执行器的集群,集群下面机器列表。执行器下面绑定一些任务,每个任务执行的日志信息,还有其他的模块,运营报表,失败告警,任务依赖等属性。这个就是维护代码里面的,整个组成一个调度中心,调度中心通过 Quartz 负责任务触发,每次任务执行的时候,首先会根据这个任务找到对应的执行器,执行器在 APP name 找到注册上来的一些机器的列表,根据一些路由的策略选中一台机器,触发右侧执行器的任务执行。

执行器每个任务都存在一个线程,线程里面有调度队列,调度信息入队列之后,会一个一个地去处理调度队列里面的调度请求。然后这个任务在执行过程中的日志会通过自研的日志组件输出一个一个的日志小文件里。这样你在调度中心界面,它只要执行了就会有一条调度日志,通过调度日志右侧的日志 log,就会看到当前这次调度它实时打印的一些日志信息。

当你的任务最终执行完成的时候,会把你当前的日志执行结果推送回到线程的一个队列,会周期性地把每个任务执行的结果推送到调度中心,在调度中心的 UI 界面上就可以看得到每一条任务的触发情况、执行情况等。

从这个模块我们可以看到它有一个注册服务,每个执行器会指定一个 APP name 。假如说我们抛开对比 SOE,就是服务化的话,一个 APP name 就相当于一个服务,以大的来说就是一个接口或者 serverID。不同的执行器就注册在不同的 APP name 下面,也就是注册在不同的执行器下面。但是每个执行器内嵌的包已经为它实现好了当前执行器统一接口下面的一些实现,这样的话你注册过来的执行器,我知道你的地址,也知道你依赖包所能提供一切的服务,这样就可以用服务的方式请求我的执行器。这种通讯就是 IPC 的方式,是非常方便扩展的。

二、XXL-JOB 特性

以下是 XXL-JOB 的一些特性全览,每一个特性我都是当时写的时候,这是一个演进的过程,我会主要会讲一下执行器和调度中心的物理结构图,以及 HA/集群。

  • 1、简单:支持通过Web页面对任务进行CRUD操作,操作简单,一分钟上手;
  • 2、动态:支持动态修改任务状态、暂停/恢复任务,以及终止运行中任务,即时生效;
  • 3、调度中心HA(中心式):调度采用中心式设计,“调度中心”基于集群Quartz实现并支持集群部署,可保证调度中心HA;
  • 4、执行器HA(分布式):任务分布式执行,任务"执行器"支持集群部署,可保证任务执行HA;
  • 5、注册中心: 执行器会周期性自动注册任务, 调度中心将会自动发现注册的任务并触发执行。同时,也支持手动录入执行器地址;
  • 6、弹性扩容缩容:一旦有新执行器机器上线或者下线,下次调度时将会重新分配任务;
  • 7、路由策略:执行器集群部署时提供丰富的路由策略,包括:第一个、最后一个、轮询、随机、一致性HASH、最不经常使用、最近最久未使用、故障转移、忙碌转移等;
  • 8、故障转移:任务路由策略选择"故障转移"情况下,如果执行器集群中某一台机器故障,将会自动Failover切换到一台正常的执行器发送调度请求。
  • 9、失败处理策略;调度失败时的处理策略,策略包括:失败告警(默认)、失败重试;
  • 10、失败重试:调度中心调度失败且启用"失败重试"策略时,将会自动重试一次;执行器执行失败且回调失败重试状态时,也将会自动重试一次;
  • 11、阻塞处理策略:调度过于密集执行器来不及处理时的处理策略,策略包括:单机串行(默认)、丢弃后续调度、覆盖之前调度;
  • 12、分片广播任务:执行器集群部署时,任务路由策略选择"分片广播"情况下,一次任务调度将会广播触发集群中所有执行器执行一次任务,可根据分片参数开发分片任务;
  • 13、动态分片:分片广播任务以执行器为维度进行分片,支持动态扩容执行器集群从而动态增加分片数量,协同进行业务处理;在进行大数据量业务操作时可显著提升任务处理能力和速度。
  • 14、事件触发:除了"Cron方式"和"任务依赖方式"触发任务执行之外,支持基于事件的触发任务方式。调度中心提供触发任务单次执行的API服务,可根据业务事件灵活触发。
  • 15、任务进度监控:支持实时监控任务进度;
  • 16、Rolling实时日志:支持在线查看调度结果,并且支持以Rolling方式实时查看执行器输出的完整的执行日志;
  • 17、GLUE:提供Web IDE,支持在线开发任务逻辑代码,动态发布,实时编译生效,省略部署上线的过程。支持30个版本的历史版本回溯。
  • 18、脚本任务:支持以GLUE模式开发和运行脚本任务,包括Shell、Python、NodeJS等类型脚本;
  • 19、任务依赖:支持配置子任务依赖,当父任务执行结束且执行成功后将会主动触发一次子任务的执行, 多个子任务用逗号分隔;
  • 20、一致性:“调度中心”通过DB锁保证集群分布式调度的一致性, 一次任务调度只会触发一次执行;
  • 21、自定义任务参数:支持在线配置调度任务入参,即时生效;
  • 22、调度线程池:调度系统多线程触发调度运行,确保调度精确执行,不被堵塞;
  • 23、数据加密:调度中心和执行器之间的通讯进行数据加密,提升调度信息安全性;
  • 24、邮件报警:任务失败时支持邮件报警,支持配置多邮件地址群发报警邮件;
  • 25、推送maven中央仓库: 将会把j最新稳定版推送到maven中央仓库, 方便用户接入和使用;
  • 26、运行报表:支持实时查看运行数据,如任务数量、调度次数、执行器数量等;以及调度报表,如调度日期分布图,调度成功分布图等;
  • 27、全异步:系统底层实现全部异步化,针对密集调度进行流量削峰,理论上支持任意时长任务的运行。

(一)HA/集群

image.png

当我们部署一套 XXL-JOB 的时候,首先我们的调度中心最好部署两台,调度中心会指向同一个 MySQL  实例,这样调度中心一定程度上可以保证它的 HA 。它每次调度的时候会通过一个远程任务代理的请求,触发到远程的执行器。在部署远程执行器的时候,只要把每一个机器指向同一个 APP name,这样每个执行器会以心跳注册的方式向调度中心进行注册,它也是 30 秒注册一次,三次心跳。如果是连续三次心跳中断的话,会把当前的执行器摘除掉。这样调度中心发现三次心跳之内都在存活这个执行器的情况下,会把它视为一个存活的执行器。在下次任务路由任务触发的时候,会当做一个备选项。调度中心和执行器之间的通信也是全异步化的,从任务的触发,到执行,到结果回调,如果可以入队列的话,都通过队列的方式进行。

(二)弹性扩容

image.png

弹性扩容

执行器的弹性扩充,主要是得益于它的注册中心。注册中心其实本质上是一个 API 服务,有三个主要的 API,第一个是心跳注册,假如当前是1、2 两台集群,这时候上线一台 3,3 会在启动的时候立刻向调度中心进行自我的初测。在调度中心找到,比如这个业务线是餐饮,餐饮下面本来有两个执行器,在 30 秒之后调度中心刷新它注册机器列表的时候,就会把餐饮 的执行器给刷出来。然后在下次心跳之后的调度请求,就会将 3 纳入它的路由机器的备选项。

第二个是摘除,摘除还以 3 为例,在机器宕机或者销毁的时候,它会主动把自己摘除掉,让调度中心说我过期了,然后把自己摘除掉;调度中心如果发现这个执行器在三次心跳之内都没有有效的注册,会把它主动过期摘除掉。这就是两种进行机器摘除的方式。

第三是注册机器查询,主要是任务调度的时候筛选机器。我们是一个任务写在一个执行器下,这个执行器可能是集群的。假如有三台机器或者多台,如何去筛选,具体执行本次任务的逻辑是什么?这里提供一些策略。

(三)执行器路由策略

image.png

执行器路由策略

前两个主要是线上,只有两台机器,第一个和最后一个,不是一就是二的情况。后面主要是轮询和随机。如果你对它要求不太高,还有一个一致性策略,你有三台执行器,但是 JOB 可能 1、2、3 层,一个执行器下面可能写了很多个 JOB ,但是执行器的任务数量是一致的,如何把当前的任务每一次调度永远哈希到固定一个执行器下面,一致性这个策略就可以保证这种功能。然后就是AIFU和AIRU这两种。

(四)故障转移 & 忙碌转移

image.png

故障转移 & 忙碌转移

假如执行器集群部署,有三台机器,首先会对机器列表进行自然排序,然后从第 1 台开始,第2 台,直到一次触发成功的时候,会进行触发,之前的会标记失败。在调度轨迹可以看到,第 1 台触发失败,第 2 台触发失败,第 3 台触发成功,最终实现任务触发的成功。

忙碌转移,可能一个 JOB 需要在 3 台执行器上执行,但是不希望已经执行的这台机器还在执行,需要找一台空闲机器执行。这样首先会触发,同样三台机器,会把触发任务执行的信号发过去。机器 1,在忙碌,返回一个状况或者原因标记。这时候它会向机器 2 发送请求,2 返回在忙,然后转向 3 ,3 返回空闲,然后接着这个请求信号进行触发。

(五)分片任务 & 动态分片

image.png

分片任务 & 动态分片

目前是通过分片广播的方式实现分片。我们匹配到执行器以后,可以拿到注册中心所有机器的列表,然后进行自然排序,每一个机器本身就有一个序号,它的总数量也是有序的,可以这两个参数传递到每一个机器。第一个机器传了一个 0-3 ,它就是识别到我是第 1 台机器,而且总共的机器数量是 3 ,根据这两个参数做一次出访,对你的总数字量进行分片。 纯广播类的 JOB 也可以用这种方式,比如动态刷新一些数据的。

接下来介绍动态分片。假如我们现在总数据量有 3 万条数据,任务进行触发了,但是在线的执行器可能是 3 台。当触发的时候会分别向 0、1、2 进行发送请求。这时候 0 接受到的参数就是03,2 就是 13,最后一个是 23。这样根据参数对总数据进行分片,把最大数据拿到,进行处理。这样假如当前有一台新机器上线了,或者机器 4 上线了,在下一次触发的时候,会以新的分片参数作为请求参数进行触发。

(六)阻塞策略 & 失败处理策略

image.png

当我们有一些耗时任务,触发的频率超过它的执行器所执行的那些速度的时候,如上图,红色的触发请求进来,但是前面的还在堆积着执行,这时候怎么办?第一条就是默认的单机串行,会把请求入队列,等前面的执行完了之后,挨个把所有的触发的任务全都执行掉。第二个就是丢弃后续的调度,红色的进来了,发现前面已经有了,或者是当前已经 JOB 运行了,直接把后面的标记失败,不进行后面的执行了。最后一个就是覆盖之前调度的,它发现前面队列里面的数据或者任务执行的情况下,把队列清空,把清空的数据全都标记失败,然后把执行的 JOB 也标记失败,让自己来运行。

失败处理有一个失败告警,你可以默认是邮件告警,只要每个任务配一下,可以备多个。当每一次任务触发,如果是触发失败,执行失败的情况下,会向你的接收人发一封邮件。现在也已经预留了接口,假如你需要短信的方式告警,或者是微信的方式告警,可以自己二次开发扩展。也提了一些失败重试的策略,当你的任务执行失败的时候,选择这个策略,会在当前在线的机器列表里面去寻找下一个机器进行重试。

(七)触发规则

现在提供的触发规则主要有 3 种。第一种是 Cron 表达式,每一个任务需要配一个 Cron 表达式。第二种是任务依赖,你可以为每一个任务配置一个子任务,当副任务执行完成之后,可以触发子任务,这样关联的方式进行触发执行。第三种就是事件触发,其实就是类似于 Mq 的场景,代码里面有一个业务逻辑,触发了一个任务执行。

(八)任务模式

接下来介绍 XXL-JOB 的任务模式。第一个就是 Bean 模式,本质上是 Spring 的一个 Bean,加一个注解之后,会自动被执行器扫描掉,执行器本身上报到执行中心,然后就被找到。

第二个就是 GLUE 模式的 JAVA。GLUE 模式是这样一个模式,假设一个系统是一辆车,上线一个新功能的时候可能拧一个螺丝,每次上螺丝的时候,先把车停下来,把螺丝拧上去;但是 GLUE 好比胶水,上线一个新功能的时候,车还在跑,直接胶水贴上,不管车是停止还是开动。可能会有失败,但是 log 会追踪。你的业务还在跑的情况下,去增加一些业务逻辑,或者是增加一些新的 JOB。

GLUE 模式目前支持 java、shell、python、node JS 这几种模式,它们的开发或者上线,全部都在 web UI 上提供了 wed IDE 进行开发的,无需打包上线。因为源码写在了调度中心,所以它也提供了一些基本的版本回溯。

GLUE java 的这种模式,是通过 GLUE Class loader 加载源码的方式,加载源码可以注入 spring 当中其他的一些 server 组件,很方便的接触 spring 其他服务。你修改的时候,下次任务执行,会是你当前这份源码重新实例化,注入一些新的服务进行执行。

(九)执行日志 & Rolling Log

image.png

任务日志和 Rolling Log ,上图可以看到是一条一条的日志,其实是对应到任务触发的每一条,它的执行的历史。第一个红框主要是它触发的时间、状态,以及触发的参数。黑框如果可以看得到,当前触发选择一些当前的机器列表和路由策略。右侧的是任务执行一侧的日志信息,在执行器上执行的时间,以及它执行的状态,以及最终执行时候中间的执行 log。

(十)构建方式

构建方式比较简单,所有的依赖只有一个 JDK 和 Mysql ,在 Mysql 里面初始化你的脚本,第二步编辑你的调度中心,第三步是编译部署执行器。提供了几种 Sample 的项目,有Spring Boot,还有 Spring 外包方式的,上面三步部署完之后,就是可以开发的一个JobHandler。

OT官网二维码.png