从定时任务调度到分布式任务调度集群
从定时任务到分布式任务调度
最近项目里面使用到了分布式任务调度,因此这篇文章准备从定时任务调度到分布式任务调度集群方面的内容做下总结。重点还是说明清楚具体的应用场景和解决的关键问题。
定时任务调度
定时任务作业大家在软件开发过程中经常会遇到。比如常说的ETL作业定时执行,或者对于一个API接口服务,我们需要每10分钟去轮询调用一次以获取增量数据信息。
简单来说就是定时任务本身不是基于外部请求触发的,而是基于定时器Schedule触发的。到了规定的时间间隔就定时触发,没有具体的请求方,定时任务执行也不需要实时返回信息给最终的客户端或用户。
如果采用微服务框架,你可以通过SpringBoot提供的Schedule注解等方面快速地实现一个定时任务调度,去定时执行某些任务或作业,然后达成一个Jar部署到中间件容器中。
分布式调度集群
还是用前面的例子,当我们面对一个集群的时候存在哪些变化?
前面谈到的这个Jar包是否可以和业务系统本身的功能实现包一起部署到类似Tomcat等中间件容器中。
如果我们通过类似HAProxy或Ngnix来实现简单的负载均衡,对于业务功能实现本身的请求负载没有问题。但是定时任务调度不是业务请求发起的,这个时候定时任务调度程序同时部署到多个Tomcat中间件节点中,比如一个有3个集群节点。
那么这个时候就出现了一个问题。
3个集群节点中的调度程序都有定时器在跑,那么每10分钟可能都触发对API接口服务的定时调用,即每10分钟间隔下,实际API接口服务却被调用了3次。如果10个集群节点,那么API接口调用就被调用了10次。
现在在这种集群部署的场景下就出现了问题。
实际上我们的需求是虽然部署了3个集群节点,但是到了10分钟的定时任务触达点的时候,应该只有集群中的一个节点去执行API接口调用任务。如果这个节点出现错误,那么再转移到其它节点去继续执行。
简单来说就是调度请求和调度任务执行应该分离。
调度请求应该有统一的调度中心进行管理,并基于负载算法等确定将该调度请求分配到那个作业中心去执行,这样就不会出现多个集群节点同时去执行一个调度任务。
分布式请求集群和分布式调度集群
如果是有实际的外部用户并发访问请求作为输入,那么一般对应常说的负载均衡,通过负载均衡来实现请求的分发。在这种请求下只要中间件容器节点本身无状态,那么中间件节点本身并不需要做大的改变或调整,可以做到弹性水平扩展。
但是实际情况确实一般的节点本身并不是无状态,而是存在一些本地的配置文件需要时刻保持状态同步,多个节点中的类似配置文件这些数据的一致性。
因此从简单的负载均衡发展到了分布式集群架构。
典型的分布式集群架构类似Eureka,Etcd,Zookeeper等各种开源实现。其核心的一个作用就是需要进行各个分布式节点的任务协调,一致性保证,状态同步等。
如果调度请求和调度任务没有分离,那么基于分布式集群思路来实现分布式任务调度,就需要引入类似分布式锁机制来确保不出现同一个调度任务多节点执行。
对于前面集群3个节点的例子。
当任何一个节点启动了API接口的定时调度任务后,实际就对这个任务进行了锁定,其它2个节点无法再去执行,但是其它两个节点可以监听A节点,当A节点执行完成或出现故障的时候,那么B节点就可以继续去发起任务的执行。
因此你可以看到在分布式任务调度集群实现的时候,首先要考虑是采用开源的分布式调度组件通过调度请求和调度任务执行分离方式来实现,还是通过类似Zookeeper分布式集群的分布式锁方式来实现。一个典型的分布式任务调度场景
我们来举一个典型的场景进行说明。
比如当前我们要实现一个分布式系统中的中间件日志文件的采集处理,并结构化入库这个操作。集群里面有20台服务器,每个Server都会在log文件目录下产生大量的.log日志文件,任务调度程序需要去采集这些日志文件并将数据结构化后写入到数据库。
可以看到这个任务没有外部请求发起,而是类似于一种通过任务调度系统定时机制进行发起。符合分布式任务调度场景。
业务系统集群20个节点,每个节点如果100个日志文件,那么实际就有2000个任务作业需要处理,这个时候日志采集入库我们配置4台服务器组成分布式任务调度集群,通过调度中心将2000个任务处理请求分配到4个任务处理节点去执行。
调度中心实现任务请求和执行分离
第一个要做的就是调度中心和任务中心分离,即任务请求和任务执行分离。基于上面的例子实际就是有2000个任务作业请求,这个我们需要提前进行计算,将任务请求计算出来后放到缓存或任务请求队列中。
其次才是调度中心将2000个任务请求平均分配到三个任务处理节点去执行。
任务处理节点实际上已经是处理具体的任务请求。比如一条具体的任务作业请求已经是:
Server1的/log目录下的aaa.log文件进行采集和解析处理。
调度中心持久化存储机制
分布式调度应该具备数据持久化功能,不论是用本地配置文件,还是结构化数据或缓存数据库都可以。通过数据库来对前面谈到的调度任务配置信息,任务处理请求,任务处理状态,错误日志信息等进行持久化存储。同时任务调度涉及到的一些分布式锁机制也通过DB数据库机制来完成。
分布式容错机制
在这里看下分布式容错机制。
如果进行的是一个调度任务执行失败,比如aaa.log日志解析任务失败。那么这个时候任务返回失败状态并重新返回到任务请求池。下次该任务重新分配到任务处理节点进行执行。也就是说在A节点处理失败,那么下次可能重新分配到B节点重新执行。
第二种场景是任务处理节点A宕机。那么这个时候节点A处理的所有正在执行中的任务全部出现异常,这些任务同样终止并回到任务请求队列,通过调度中心重新进分配,将任务请求分配到B和C节点进行执行。
调度中心需要时刻监听A节点状态,当A节点恢复后新的调度任务能够重新分配到A节点。
文件采集和文件解析入库分离
如果是上面的业务场景,还可以将日志文件采集和日志文件解析入库两个动作进行分离,即形成不同的调度任务请求。这样可以减少整个长周期的处理事务。
日志文件采集到任务处理节点后,采集类任务即完成。这个时候可以再触发生成一个日志解析入库任务到任务请求池队列。调度中心再对解析任务进行重新分配。
节点弹性动态扩容
任务处理节点应该做到可以动态伸缩,即可以动态加入新的任务处理节点,在节点增加进入后新的调度处理任务能够分配到新节点去执行。
当然整个集群节点还可以进行分组或分片,形成多个调度集群,但是调度中心保留一个调度中心,即调度中心可以将多个分片集群纳入统一管理。
多线程任务调度
这个应该是分布式调度系统的基础能力,比如上面例子实际获取文件列表,文件采集,文件解析可以拆分为不同的线程池。而对于文件采集任务本身又可以启动多个线程同时执行。比如节点A可以一次同时采集和传输5个文件。开源框架xxl-job介绍
在我们实际的项目中,通过最终的必选最终选择了采用xxl-job来做分布式任务调度集群。xxl-job是一个分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。
该开源项目是大众点评员工徐雪里于2015年发布的分布式任务调度平台,当前已经有上100家企业在使用,而且整体版本更新及时,社区也偏活跃,这个也是我们选择该开源项目的一个重要原因。
xxl-job就是一个中心化管理系统,系统主要通过MySQL管理各种定时任务信息,当到了定时任务的触发时间,就把任务信息从db中拉进内存,对任务执行器发起触发请求。这个任务执行器,既可以是bean、groovy脚本、python脚本等,也可以是外部的http接口。
架构设计
该分布式调度框架核心设计思想是将调度行为抽象形成"调度中心"公共平台,而平台自身并不承担业务逻辑,"调度中心"负责发起调度请求。同时将任务抽象成分散的JobHandler,交由"执行器"统一管理,"执行器"负责接收调度请求并执行对应的JobHandler中业务逻辑。
因此,"调度"和"任务"两部分可以相互解耦,提高系统整体稳定性和扩展性;
系统组成
从上面架构图可以看到,整个系统分为了调度中心和执行器,同时两个部分内容解耦,执行器节点本身可以进行灵活扩展。
调度模块(调度中心):负责管理调度信息,按照调度配置发出调度请求,自身不承担业务代码。调度系统与任务解耦,提高了系统可用性和稳定性,同时调度系统性能不再受限于任务模块;
支持可视化、简单且动态的管理调度信息,包括任务新建,更新,删除,GLUE开发和任务报警等,所有上述操作都会实时生效,同时支持监控调度结果以及执行日志,支持执行器Failover。
执行模块(执行器):负责接收调度请求并执行任务逻辑。任务模块专注于任务的执行等操作,开发和维护更加简单和高效;接收"调度中心"的执行请求、终止请求和日志请求等。
具体的使用手册请参考:
https://www.xuxueli.com/xxl-job/