这是我参与「第五届青训营 」伴学笔记创作活动的第 10 天
1. 前言
业务场景
- 超高用户规模 ==> 单体下似乎比较困难
- 超高存储容量
- 超高QPS
架构设计 4S原则
在这样的业务场景下,如何实现 任务调度?
2. 发展历程
2.1 windows批处理任务
定时10分钟后关机:
shutdown命令。
2.2 windows计划调度
应用:每天12点自动打卡。
运行python脚本模拟登录,模拟发送打卡包。
在windows中,一个定时任务被描述为
元信息:任务名,任务描述等
触发器:触发器负责定时将任务交给调度器执行
操作:任务执行时,执行的内容是什么
2.3 linux crontab
使用
cron指令进行linux下的系统任务安排。特点:
- 有自己的一套语法,不同于windows的图形化创建,通过编辑配置完成任务创建。
缺点:
- 仅在linux平台下有效
- 仅支持单机调度
2.4 编程语言级别
java的
Timer:由于java基于共享内存模型,所以Timer实际上是一个信号发射器,通过定时发送信号触发事件。而go的
ticker基于 CSP模型,定时向管道内写入数据,有一个读的goroutine就可以做到按时执行。
进一步的,为每个任务创建一个线程,任务执行后销毁,开销和额外负担比较大。
再优化就是线程池,复用连接以达到节省资源。
通过编程语言实现任务调度,相当于在OS层上再加了一层,效果就是对用户屏蔽了操作系统的API。
- 可以达到跨平台。
- 引入了学习成本,简洁性下降。
- 同样的,只能基于单机调度。
2.5 任务调度Quartz
Quartz是一个单机任务调度的框架。这个框架对模型进行了建模,并且也完成了模块化的定制。
- 任务被描述为JobDetail对象。
组件则具有了
- 触发器组件,具有不同的触发器实现,用于按照触发器规则,将任务对象交给调度器处理
- 调度器组件,负责管理多个任务的生命周期和状态转移。
startstoppauseresume
2.6 分布式定时任务
调度器和执行器都成为了分布式集群,使得任务的管理变得平台化:没有中心化的任务管理组件,而是去中心化,每个结点都具有管理能力,结点之间通过rpc等手段进行通信。
2.6.1 什么是分布式定时任务
这里的平台很模糊,其实我们用的美团外卖就是平台,我们作为平台的客户端,商家作为服务端,而骑手具有较为自由的方式。也就是一个订单不通过集中式的调度,而是类似点对点的。
不是说我们把订单请求发给 骑手中心,然后骑手中心进行中心调度,为订单分配骑手。而是骑手主动接取订单。也就是骑手本身也参与进了订单的调度。这自然是去中心化的。
分布式任务的分类
-
按触发时机
- 定时任务
- 延时任务
- 周期任务
-
按执行方式
-
单机任务 比如执行一个小脚本,定时发布打卡情况
-
广播任务 比如统一执行的操作,如日志清理
-
Map任务 Map任务主要在于分,把大的计算任务进行拆分,往往不要求对各个结果进行整合 比如定时的信息推送 优惠券发放等
-
MapReduce任务 需要对子任务的结果进行汇总 比如定时的统计任务等。
分布式定时任务最大的特点就是,充分利用了当前的机器资源。
在调度器层面,可以对任务进行拆分。这也是当今多线程计算的一种思想,即fork-join。在大数据领域就是map-reduce。
在进程层面,fork-join指的是可以拆分的大任务,先进行fork成不同的小任务,并行在不同的cpu核心,最后进行join同步和聚合结果。
而map-reduce则是基于分布式架构的一个模型,先在不同的计算节点对任务进行map,然后将小任务交给其他的计算节点,最后reduce结点直接处理各结点的结果,并合并。
fork-join把任务交给同一主机的线程,为的是充分利用同一主机的CPU资源。而map-reduce把任务交给分布式体系中的不同主机,为的是充分利用计算资源。
-
在春节季卡活动中,对于用户信息的扫描与汇总,就是一个拆分后合并的任务模型,因此采取MapReduce任务。==> 可以理解为任务体是既有输入又有输出的业务逻辑
而定时开奖,相当于定时发放奖品,不需要对结果进行汇总。 ==> 可以认为任务体只有输入,没有输出的业务逻辑。
3. 实现原理
3.1 架构(组件层)
这是谈论架构的组件层面。
体现了模块化的调度,采取模块化主要是为了业务垂直分割和解耦。
3.1.1 数据流(组件调用链)
既然把业务垂直分割为不同模块,那么在完成整个业务逻辑时,务必涉及组件的调用。
既然涉及组件间调用,就要满足一定的可观测性和稳定性。
可观测性是观测是否发生错误。稳定性则是在发生错误时,具备一定恢复能力。
3.1.2 功能架构(组件功能)
在这些功能中,只要实现一些基本功能,这个框架就可以投入使用。
我们也可以面向功能编程,在已有的框架上进行功能拓展,以达到更方便,简洁,安全的使用。
同时,评估一个框架的优越性,也可以比对该框架实现了哪些功能。
3.2 控制台(模型建立)
在分布式任务调度中,应该怎么模型任务的模型呢?
我们不仅要建立任务对象的模型,还要建立模型之间的联系,这个很像创建数据表。
在编程语言中,这些实体(Job,JonInstance等)被描述为一个个结构体。而在数据库中,则描述为一张张表,表之间通过主外键建立关联。
3.2.1 任务元数据
// 伪代码
type Job struct {
jobName string
jobInfo string
jobType string // 任务类型(可以用一个枚举表示)
state string // 任务状态(可以枚举表示)
trigger Trigger // 调度时机
doSth func() // 执行行为
}
3.2.2 任务实例
任务本质就是一个例程,因此任务调度,有点类似线程调度,涉及到复杂的状态转移。
在涉及任务调度系统时,要清晰的设计好。
3.3 触发器
3.3.1 触发器 -- 任务存储结构
面向需求设计。
触发器的任务时:在合适的时机,实例化出JobInstance交给调度器调度
方案一:定时扫描 + 延时队列
通过扫描DB中的任务元数据,通过触发器的processor,创建JobInstance。Job的Metadata中存放任务的创建时机,processor会读出这个时机,然后加入延时MQ。并通过上面的那一套状态机模型,修改任务状态。
方案二:时间轮
这个时间轮的设计,自然是面向需求的设计。就是因为有了批量调度任务的需求,才有了这个数据结构。
数据结构存在一个演变过程。因为用最基本的数据结构,总能完成复杂的操作。
演变数据结构,通过演变数据结构的定义,简化操作的实现。
在分布式任务调度系统中,我们只需要对任务队列做两个操作
- 查:trigger定期扫描任务队列,扫到待执行的任务[频繁]。如果有必要,可以删除只执行一次的任务
- 改:通过状态转移,修改任务的state属性[频繁]
链表是不是最适合的数据结构?
查询复杂度O(n),修改复杂度O(1)
缺点:任务太多怎么办?链表结构膨胀。
小根堆是不是最适合的数据结构
查询复杂度O(1),修改复杂度O(logn) ==> 需要自行实现能修改结点的小根堆,建立结点索引和数组索引的map,定义一个修改方法。
缺点:同样,如果时间跨度比较不均匀,任务又很多,同样涉及到结构爆炸的情况。
使用时间轮
这个时间轮很像一个hash表,可以根据定时任务的执行时间,直接插入对应的插槽。
而且时间轮是一个循环数组,不用担心存储内容过于庞大的风险。
这个实现就可以很多了。
首先trigger遍历任务列表,把他们按照待执行时间,插入时间轮slot中。
然后trigger的processor每隔1s,将该slot中的任务交给调度器执行(只是把任务send给scheduler,因此时间差不会很大)
读完一圈,下一次读取就从头读取。这是trigger中scanner的一种实现。
可以为每个任务分配一个count字段,如果一个任务要等60min35s执行,那么其count=60,slot=35,假设processor转一圈60s,那么转60圈后,才会把该任务发给scheduler
如果不够用,可以多增加几个时间轮
比如一个任务是1h30min50s执行,在时轮转过1时,所有这里的任务,都会降级到分轮中,同理,当秒轮中的任务被转过时,才会真正发给scheduler执行。
3.3.2 触发器 -- 高可用
任何分布式系统都需要考虑组件的高可用。
为了避免单点故障,就需要引入集群,然而引入集群,就会导致各种问题。
如果是存储系统的集群,可能导致数据不一致,对于消息队列或者定时任务系统,则可能导致业务代码重复执行。
为了避免执行的互斥,我们可以采取数据库行锁或者分布式锁。
阿里云的scheduleX采取了基于zookeeper的分布式锁,保证了性能和安全性。
3.4 调度器
3.4.1 资源来源
这里指的是调度器工作在什么计算资源上。
如果调度器和业务逻辑共享计算资源,那么务必会造成和业务逻辑的耦合性,而且调度任务往往具有高并发,集中调度性,而不和业务逻辑隔离,可能会导致在线服务出事故。好处就是,调度器调度业务代码时,可以进行ipc,也就是进程间通信
如果调度器和业务逻辑隔离,定时任务平台提供机器资源(也就是调度器作为单独部署的主机,集群分布在分布式定时任务平台)。由于和业务逻辑解耦,因此不会影响业务逻辑,也方便扩容,缺点就是,需要业务逻辑提供接口,执行rpc调度,在网络同欣赏会折损一定效率。
3.4.2 资源调度 -- 节点选择
调度器主要负责把从trigger发来的jobInstance调度到不同的计算节点上执行。这里scheduler的实现,可以是根据job元信息的jobType,选择执行模型,然后把任务交给执行器执行。
3.4.2 任务调度 -- 任务分片
实际上就是mapreduce任务的处理。
3.4.3 高级特性 -- 任务编排
定时任务之间也有可能具有依赖性。
可以人为定义拓扑排序图来保证任务执行的先后顺序。
在实现上,不同于多线程的执行模型,即同步机制,在分布式集群中进行同步需要引入分布式锁等
3.4.3 高级特性 -- 高可用
无状态设计:采取无状态设计,可以保证MapReduce的正确性。如果scheduler保存状态,无比需要维护不必要的依赖关系。
MQ:采取MQ机制,可以保证trigger的任务分派的可用性。
3.5 执行器
执行器负责执行调度中心发来的任务对象(任务对象中绑定一个函数,可以直接执行)。并适当的记录日志,然后将状态上报给调度中心。
执行器会先注册到调度中心。调度中心不仅负责任务调度,还具有服务发现功能,注册中心负责负载均衡。同时根据执行器的返回结果,判断是否执行成功,如果不成功,将把任务调度给其他的执行器。
这都是分布式集群正确性的保障。
4. 业务应用
术业有专攻。
对于单纯的延时信息,或者定时信息,可以交给消息队列来完成。但是对于重复执行的任务,就要交给分布式定时任务框架来完成了。trigger负责任务的状态转移。
如果是单纯的计算任务,不涉及rpc等调用 或者执行脚本。而且没有实时性的前提,可以把计算任务交给大数据的计算引擎