背景概述
业务场景中经常有一些场景需要使用定时任务,比如:
- 时间驱动的场景:某个时间点发送优惠券,发送短信,取消未支付订单等等。
- 批量处理数据:批量统计上个月的账单,全量同步商品数据等等。
- 固定频率的场景:每隔一定时间需要执行一次。
传统的定时任务实现方案,比如Timer,Quartz等都或多或少存在一些问题:
- 不支持集群高可用,没有监控、故障告警等。
- 没有统一管理平台,不支持统计和追踪各个服务节点任务调度的结果等
- 不支持分布式任务调度:同一个服务多个实例的任务存在互斥时,需要统一的调度。
ElasticJob
elastic-job 是由当当网基于quartz 二次开发之后的分布式调度解决方案 , 由两个相对独立的子项目Elastic-Job-Lite和Elastic-Job-Cloud组成 。
ElasticJob-Lite定位是无中心化的分布式定时调度框架,采用zookeeper实现分布式协调,实现任务高可用以及分片。
ElasticJob-Cloud提供资源治理、应用分发以及进程隔离等功能。
ElasticJob-Lite | ElasticJob-Cloud | |
---|---|---|
无中心化 | 是 | 否 |
资源分配 | 不支持 | 支持 |
作业模式 | 常驻 | 常驻 瞬时 |
部署依赖 | ZooKeeper | ZooKeeper Mesos |
实现原理
1. 作业启动
2. 作业执行
缺点
elasticjob是无中心化的,通过ZooKeeper的选举机制选举出主服务器。如果主服务器挂了,会重新选举新的主服务器。
因此elasticjob具有良好的扩展性和可用性,但是使用和运维有一定的复杂。
XXL-JOB
xxl-job就是一个中心化管理系统,系统主要通过MySQL管理定时任务信息,通过DB锁保证集群分布式调度的一致性。虽然扩展执行器会增大DB的压力,但是实际上大部分公司任务数,执行器并不多。
当到了定时任务的触发时间,就把任务信息从db中拉进内存,对任务执行器发起触发请求。这个任务执行器,既可以是bean、groovy脚本、python脚本等,也可以是外部的http接口。
相比起当当网开源的elastic-job-lite(基于zookeeper作为协调器的“无中心”架构),这种中心化管理的系统轻量级、上手快、易于维护。
架构图
- 调度模块(调度中心): 负责管理调度信息,按照调度配置发出调度请求,自身不承担业务代码。调度系统与任务解耦,提高了系统可用性和稳定性,同时调度系统性能不再受限于任务模块; 支持可视化、简单且动态的管理调度信息,包括任务新建,更新,删除,GLUE开发和任务报警等,所有上述操作都会实时生效,同时支持监控调度结果以及执行日志,支持执行器Failover。
- 执行模块(执行器): 负责接收调度请求并执行任务逻辑。任务模块专注于任务的执行等操作,开发和维护更加简单和高效; 接收“调度中心”的执行请求、终止请求和日志请求等。
执行步骤
- 任务执行器根据配置的调度中心的地址,自动注册到调度中心。
- 达到任务触发条件,调度中心下发任务。
- 执行器基于线程池执行任务,并把执行结果放入内存队列中、把执行日志写入日志文件中。
- 执行器的回调线程消费内存队列中的执行结果,主动上报给调度中心。
- 当用户在调度中心查看任务日志,调度中心请求任务执行器,任务执行器读取任务日志文件并返回日志详情。
定时触发任务是如何实现的?:使用时间轮实现
2.1.0版本前核心调度模块都是基于quartz框架,2.1.0版本开始自研调度组件,移除quartz依赖 ,使用时间轮调度。
- xxl_job_info表记录定时任务信息,trigger_next_time(Long)字段记录下一次触发的时间点
- 任务时间被修改 或者任务触发后,计算下一次触发时间戳并更新trigger_next_time字段
定时执行任务逻辑:
定时任务scheduleThread:不断从db把5秒内要执行的任务读出,立即触发 / 放到时间轮等待触发,并更新trigger_next_time
- 获取当前时间now
- 查询数据库中trigger_next_time在距now 5秒内的任务
- (0)对到达now时间后的任务(超出now 5秒外):
- 直接跳过不执行;
- 重置trigger_next_time
- (1)对到达now时间后的任务(超出now 5秒内):
- 开线程执行触发逻辑;
- 若任务下一次触发时间是在5秒内,则放到时间轮内(Map<Integer, List<Integer>> 秒数(1-60) => 任务id列表);
- 重置trigger_next_time
- (2)对未到达now时间的任务:
- 直接放到时间轮内;
- 重置trigger_next_time
定时任务ringThread:时间轮实现到点触发任务
- 时间轮数据结构:Map<Integer, List<Integer>> key是秒数(1-60) ,value是任务id列表
- 获取当前时间秒数,从时间轮内移出当前秒数前2个秒数(避免处理耗时太长,跨过刻度,向前校验一个刻度)的任务列表id,触发任务;
如何避免集群中的多个服务器同时调度任务?
当xxl-job应用本身集群部署(实现高可用HA)时,通过mysql悲观锁实现分布式锁(for update语句)
- setAutoCommit(false)关闭隐式自动提交事务,启动事务
- select lock for update(显式排他锁,其他事务无法进入&无法实现for update)
- 读db任务信息 -> 拉任务到内存时间轮 -> 更新db任务信息
- commit提交事务,同时会释放for update的排他锁(悲观锁)
任务执行器注册中心是如何实现的?
xxl-job添加执行器到任务调度中心有两种方式
(1)客户端执行器自动将名称和机器地址注册到任务调度中心,任务调度中心对外提供注册地址/api用来接受任务执行器注册的相关服务器信息
(2)在任务调度中心手动录入执行器名称和相关的机器地址,使用db表xxl_job_group记录下执行器的信息:执行器AppName、执行器名称title、执行器地址列表address_list(多地址逗号分隔)
如何实现任务执行器的路由?
执行器集群部署时提供丰富的路由策略,包括:第一个、最后一个、轮询、随机、一致性HASH、最不经常使用、最近最久未使用、故障转移、忙碌转移等;
- 第一个、最后一个、轮询、随机:都是简单读address_list即可
- 一致性HASH:TreeSet实现一致性hash算法
- 最不经常使用、最近最久未使用:HashMap、LinkedHashMap
- 故障转移:遍历address_list获取address时,逐个检查该address的心跳(请求返回状态);只有心跳正常的address才返回使用
- 忙碌转移:遍历address_list获取address时,逐个检查该address是否忙碌(请求返回状态);只有状态为idle的address才返回使用
如何实现任务分片、并行执行?
- 拉出任务的执行机器列表,逐个设置index / total,把index / total分发到任务执行器
- 任务执行器可根据index / total参数开发分片任务
问题
高频调度的执行时间比较长的任务
一般建议指定到单独一台主机上并保证在单机上任务不会并发执行来解决。
定时任务中依赖任务
1)任务依赖不支持环,只支持DAG;
如:A->B->(C,D)->E 其中CD并行,其余串行
2)下游任务只支持上游所有任务都成功并调度时间到了,才执行任务;
如:JobA只有在Job1,Job2,Job3都执行完,并且时间到了才能执行。
3)不支持有不同调度周期的任务存在依赖关系
如:A->B B的前置任务为A, A的调度周期为每15分钟调度一次, B为每天早上1点调度,该任务不建议分布式调度中心执行。
很难判断前置任务是成功还是失败;建议把A任务拆分为两个任务,一个为B对前置任务A1,一个为每15分钟执行一次(调度时间过滤掉A1)的任务
任务重复执行
JobA依赖Job1,Job2,Job3执行,同时JobA3点也会调度执行,在3点左右时,Job3执行完后会执行JobA,同时cron调度也会执行JobA,在这种情况怎么保证JobA只被执行一次。
解决办法:在JobA执行前需要把JobA的状态修改为正在执行中,此时,通过update where jobId = #{jobId} and status=#{未开始执行} 方法执行更新,如果更新记录为1的,任务可以进行执行,如果更新记录为0,抛弃该任务的执行。
怎么判断任务该不该执行
条件一:1点钟Job1执行完了,开始找后置任务JobA,JobA是否该执行?怎么判断?
JobA不该执行,前置任务Job2,Job3 都没开始执行,Job1不能执行;
条件二:3点钟Job3执行完了,开始找后置任务JobA,JobA是否该执行?怎么判断?
JobA不该执行,前置任务Job1,Job2,Job3 都执行完了,但是Cron时间还没到,Job1不能执行;
条件三:3点15分调度器开始调度,JobA是否该执行,怎么判断?
JobA该执行,前置任务Job1,Job2,Job3 都执行完了,Cron时间也到了;
判断任务是否执行的逻辑: 如果JobA执行时,需要判断Job1,Job2,Job3是否执行,下面拿Job1为例
假设Job1的历史任务都是正常执行成功的。
情况1: 2019-06-26 00:30:00(today)时,Job1的上一次执行成功时间为2019-06-25:01:00:00 (lastDay),下一次执行时间为:2019-06-26 01:00:00(nextDay).
情况2: 2019-06-26 01:30:00时,Job1的上一次执行成功时间为2019-06-26:01:00:00,下一次执行时间为:2019-06-27 01:00:00.
任务失败了怎么办?
任务失败应该同时执行带依赖执行和不带依赖执行,由页面配置控制。如果页面配置执行任务有参数,参数需要传递给依赖任务。
参考:
ElasticJob官网文档:http://shardingsphere.apache.org/elasticjob/
Elastic-job 介绍与使用:https://www.jianshu.com/p/4dc449cdeb67
LTS源码地址:https://github.com/ltsopensource/light-task-scheduler
sia-task源码地址:https://gitee.com/mirrors/sia-task?hmsr=aladdin1e6
XXL-JOB源码地址:https://github.com/xuxueli/xxl-job
3千字带你搞懂XXL-JOB任务调度平台:https://baijiahao.baidu.com/s?id=1681035278234207562&wfr=spider&for=pc
XXL-JOB(4) 原理分析:http://www.heartthinkdo.com/?p=3181
分布式任务调度系统xxl-job小结:https://zhuanlan.zhihu.com/p/91862341
xxl问题汇总:https://www.cnblogs.com/smileIce/p/11156412.html
xxl-job原理:https://blog.csdn.net/yanghuangsanguo/article/details/90701592