无论是互联网应用或者企业级应用,都充斥着大量的批处理任务。我们常常需要一些任务调度系统来帮助解决问题。随着微服务化架构的逐步演进,单体架构逐渐演变为分布式、微服务架构。在此背景下,很多原先的任务调度平台已经不能满足业务系统的需求,于是出现了一些基于分布式的任务调度平台。
在实际业务开发过程中,很多时候我们无可避免地需要使用一些定时任务来解决问题。通常我们会有多种解决方案:使用 Crontab 或 SpringCron (当然这种情况可能机器很少而且任务简单又不是很多的情况下)。然而,当应用复杂度升高、定时任务数量增多且任务之间产生依赖关系时,Crontab 进行定时任务的管理配置就会非常混乱,严重影响工作效率。这时就会产生一系列问题:
随着互联网的发展,分布式服务架构势越来越流行。相应的也需要一个分布式任务调度系统来管理分布式架构中的定时任务。
当垂直应用越来越多,应用之间交互也会越来越复杂,通常我们采用分布式或者微服务架构,将核心业务抽取出来,形成单独的服务。一个独立的微服务群体逐渐形成稳定的服务中心,使得业务应用能更快地响应多变的市场需求。
此时,用于提高业务复用及整合的分布式服务框架成为关键。同时,由于服务独立,一般能做到定时任务独立的情况,任务的更改对于整体系统的影响小之又小。通常我们会采用任务与调度分离的方式(如上图所示),任务的执行逻辑无需关注调度与编排,同时可以保证执行器和调度的高可用,易于开发和维护。
在分布式服务架构的基础上,由于独立业务的数量可能很多,此时如果定时任务单独在该服务中实现,很可能会出现难以管理的情况,且避免不了由于定时任务的更改而导致的业务重启。因此,一个独立的分布式任务调度系统是很必要的,可以用来全局统筹管理所有的定时任务。同时,将任务的配置单独抽离出来,作为该分布式任务调度系统的功能,就能做到定时任务的更改不影响任何业务,也不影响整个系统:
SIA是宜信公司基础开发平台Simple is Awesome的简称,SIA-TASK(微服务任务调度平台)是其中的一项重要产品,SIA-TASK契合当前微服务架构模式,具有跨平台、可编排、高可用、无侵入、一致性、异步并行、动态扩展、实时监控等特点。
开源地址: https://github.com/siaorg/sia-task
我们先对比市场上主流的开源分布式任务调度框架,分析其优缺点,然后再介绍我们的技术选型。
下面我们简单对比下 SIA-TASK 与这些任务调度框架:
任务编排 | 任务分片 | 跨平台 | 高可用 | 故障转移 | 实时监控 | |
---|---|---|---|---|---|---|
SIA-TASK | √ | √ | √ | √ | √ | √ |
Quartz | × | × | .NET | √ | × | API监控 |
TBSchedule | × | √ | × | √ | √ | √ |
Elastic-Job | × | √ | × | √ | √ | √ |
Saturn | × | √ | √ | √ | √ | √ |
Antares | √ | √ | × | √ | √ | √ |
Uncode-Schedule | × | × | × | √ | √ | √ |
XXL-JOB | 子任务依赖 | √ | × | √ | √ | √ |
可以发现,这些调度框架基本上都支持高可用、故障转移与实时监控等功能,但是对于任务编排、任务分片与跨平台等功能的支持各有侧重点。SIA-TASK 将全面支持这些功能。
SIA-TASK借鉴微服务设计思想,获取分布在每个执行器节点上的任务(Task)元数据,进行汇报,上传注册中心。利用在线可编辑方式支持任务在线编排、动态修改任务时钟;使用 Http 协议作为交互传输协议。数据交互格式统一使用Json。用户通过编排器(下文会做介绍)进行操作,触发事件,调度器接收事件,由调度中心进行时钟解析,执行任务流程,进行任务通知。
SIA-TASK 采用任务和调度分离的方式,业务的执行任务逻辑和调度逻辑完全分离。系统组成共涉及以下几个核心概念:
SIA-TASK 可以分为三大模块(调度中心、编排中心和执行器)、两大组件(持久化存储和注册中心)。这三大模块和两大组件的作用如下:
SIA-TASK 使用 SpringBoot 体系作为架构选型,基于Quartz及Zookeeper进行二次开发,支持相应的特性功能,SIA-TASK 的逻辑架构图如下图所示:
任务调度中心负责任务调度,管理调度信息,按照调度配置发出调度请求,自身不承担业务代码。调度系统与任务解耦,提高了系统可用性和稳定性,同时调度系统性能不再受限于任务模块;支持可视化、简单且动态地管理调度信息,包括任务新建,更新,删除和任务报警等,所有上述操作都会实时生效,同时支持监控调度结果以及执行日志,支持执行器故障恢复。
任务编排中心是分布式调度中心支持在线任务模型编排的组件;依托于UI可进行web端任务编排。
我们可以通过上述基础模型来编排一些复杂的调度模型,例如:
SIA-TASK的UI编排界面:
编排结束后查看task的编排信息如下图所示:
同时,编排中心还提供首页统计数据查看、调度监控、Job管理、Task管理以及日志管理功能。
负责接收调度请求并执行任务逻辑。任务模块专注于任务的执行等操作,开发和维护更加简单和高效;
执行器支持两种类型:
(1) 如果使用 sia-task-hunter,支持SpringBoot项目和Spring项目, 引入 sia-task-hunter,任务(Task)抓取客户端。合规的HTTP接口(称之为Task)任务会自动被抓取并上传注册中心;
(2) 如果不使用 sia-task-hunter,只需提供任务可调用的HTTP接口,此时需要业务手动录入,且自行控制该任务的并发调用控制。
分布式框架采用Zookeeper作为注册中心。
(1) 任务注册
调度中心和执行集群都以Zookeeper作为注册中心,所有数据以节点及节点内容的形式注册,通过定时汇报主机状态保持存活在Zookeeper上。
(2) 元数据存储
注册中心不仅仅提供注册服务,并且存储每个执行器的信息(包括执行器实例信息,执行器上传的Task元数据,以及任务运行时的一些临时状态数据)。
(3) 事件发布
基于Zookeeper事件推送机制,进行任务的发布,通过平衡算法保证调度器任务抢占的分布均衡。
(4) 负载均衡
保证调度器获取执行Job的个数均衡,避免单一节点压力。
这里采用MySQL作为数据持久化解决方案。
除了Task动态元数据保存在注册中心之外,其他相关的元数据都存入MySQL,包括但不限于:手动录入的Task、配置的Job信息、编排的Task依赖信息、调度日志、业务人员操作日志、Task执行日志等。
(1) 用户可以通过UI进行Job创建。可以选择Job类型,设置预警邮箱,设置Job描述。然后为创建的Job进行任务Task编排。
(2) Job创建完毕并且设置Task编排关系后可进行任务发布,通过UI对相应的Job进行操作(激活,执行一次,停止以及删除操作)。
(3) 用户的Task任务可以是通过抓取器抓取的,亦可以使用UI手动创建。
(1) Job创建完成之后,可以选择激活触发定时任务;
(2) Job到达预订时间后,调度中心触发Job,然后按照预定的Task编排逻辑通过http通知Task执行器进行执行,并异步监听任务执行结果;
(3) 若执行结果成功,则判断是否存在后置Task,若存在,则继续下一次调度,若不存在,则说明该Job执行完毕,结束本次调用;若执行结果失败,则触发故障恢复策略:立即停止、忽略本次失败、多次尝试、转到其它执行器执行。
Job在整个生命周期内存在四种状态,分别是:已停止(NULL)、准备中(READY)、开始运行(RUNNING)、异常停止(STOP),状态流转及流转条件如下图所示。
SIA-TASK 的物理网络拓扑图如下所示:
SIA-TASK 的模块间交互设计思路:
(1) 通过编排中心创建Task任务或通过Hunter自动抓取,并将 Task 信息异步保存到DB;创建Job并激活,在zookeeper中创建JobKey。
(2) 调度中心会监听zookeeper中JobKey创建事件,然后抢占创建的Job,抢占成功后加入quartz定时任务,当时间到达即触发Job运行。调度中心异步调用执行器服务执行Job中的 Task (可能存在多个 Task ,遵循 Task 失败策略),并将结果返回到调度中心。
(3) 将Job执行状态随时在zookeeper上更改,通过编排中心的查询接口可以进行查询。
(4) Job执行结束后,等待下一次执行。
编排中心可以与DB和zookeeper进行数据交互,其主要功能可分为三方面:
编排中心首页监控展示如下:
调度中心主要与DB、ZK和执行器进行交互,其主要功能可分为以下几个方面:
执行器可以与ZK和调度中心进行交互,其主要功能可分为两个方面:
执行器 Task示例:
@OnlineTask(description = "在线任务示例",enableSerial=true) @RequestMapping(value = "/example", method = { RequestMethod.POST }, produces = "application/json;charset=UTF-8") @CrossOrigin(methods = { RequestMethod.POST }, origins = "*") @ResponseBody public String example(@RequestBody String json) { /** * TODO:客户端业务逻辑处理 */ Map<String, String> info = new HashMap<String, String>(); info.put("status", "success"); info.put("result", "as you need"); return JSONHelper.toString(info); }
由此可见,任务 Task 编写非常简单。
分布式服务一般都要考虑高可用方案,同样 SIA-TASK 为了保证高可用,针对不同的服务组件进行了不同维度增强。
SIA-TASK 通过前后端分离、服务拆分等措施实现了编排中心的高可用。当集群中某实例失效后,不会影响集群的其它实例,因此无需特殊操作即可使用集群中其它的可用编排中心。
如果调度中心集群中的某个实例节点服务宕机后,这个实例节点上的所有Job会平滑迁移到集群中可用的实例上,不会造成定时任务的执行缺失,同时,当崩溃后的实例修复成功重新接入该集群时,会继续抢占Job提供服务。
调度采用线程池方式实现,避免单线程因阻塞而引起任务调度延迟。程池里的线程数,默认值是10,当执行任务会并发执行多个耗时任务时,要根据业务特点选择线程池的大小。
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool org.quartz.threadPool.threadCount = 60 org.quartz.threadPool.threadPriority = 5 org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = true
SIA-TASK 根据quartz自身提供的threadPool再次进行线程池的利用。进行线程池重新定义,针对每个Job去分配一个独有的线程池。线程池的大小可根据Job自身编排的 Task 个数的大小进行动态伸缩,从而保证每个Job的调度线程完全独立,不在会因为编排 Task 个数的陡增而耗尽线程资源。同时提供线程池资源的回收逻辑,在Job进行永久性终止时回收为期分配的线程池资源。
public static ExecutorService getExecutorService(String JobKey) { ExecutorService exec = executorPool.get(JobKey); if (exec == null) { LOGGER.info(Constants.LOG_PREFIX + "Initialize thread pool for running Jobs,Job is {}",JobKey); exec = Executors.newCachedThreadPool(); executorPool.putIfAbsent(JobKey, exec); exec = executorPool.get(JobKey); } return exec; }
SIA-TASK 针对Job的整个调度生命周期进行全面跟踪,利用AOP进行日志增强,调度中心每触发一次Job调度就会进行日志记录。同时针对Job编排的 Task 执行也会进行记录任务日志。
日志分为Job日志和 Task 日志:
public interface RestTemplate { /** * 异步Post方法 * @param request * @param responseType * @param uriVariables * @param <T> * @return */ <T> ListenableFuture<ResponseEntity<T>> postAsyncForEntity(Request request, Class<T> responseType, Object... uriVariables); }
SIA-TASK 从物理资源角度设计了调度资源池,出于一些特殊情况的考量我们针对调度器进行了池化;调度器可以通过不同的操作进行状态的转变,从而进行能力的转化。
至此对微服务任务调度平台 SIA-TASK 做了一个简要的介绍,包括设计背景、架构设计以及产品组件功能与特性。微服务任务调度平台 SIA-TASK 基本上解决了当前的业务需求,提供简单高效的编排调度服务。SIA-TASK 会持续迭代,提供更为完善的服务。之后也会提供相关技术文档和使用文档。
开源地址: https://github.com/siaorg/sia-task
作者:毛正卫/李鹏飞/梁鑫
原文首发:SpringCloud社区