分布式任务 | 青训营笔记

151 阅读11分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 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对象。

组件则具有了

  • 触发器组件,具有不同的触发器实现,用于按照触发器规则,将任务对象交给调度器处理
  • 调度器组件,负责管理多个任务的生命周期和状态转移。start stop pause resume

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等调用 或者执行脚本。而且没有实时性的前提,可以把计算任务交给大数据的计算引擎