写点什么

不咋简单的定时任务设计

作者:Quincy
  • 2022-11-26
    北京
  • 本文字数:3687 字

    阅读完需:约 12 分钟

不咋简单的定时任务设计

背景

定时任务在我们日常开发中是一个常见的场景。

当下的分布式定时任务的开源项目也较多,每种定时器都有着它的一些优势和不足,但无论哪种本质上是为了实现业务的目的而去实施,因此鉴于在工作中有过相关的开发,以及取得不错的效果,想把自己的一些东西沉淀出来分享给大家,特写了这篇文章。

本文也将主要介绍相关的一些定时器设计的一些方案和具体架构流程。

本文将主要符合业务场景为:时间粒度最小为秒级,针对于某些大人群的一些定时任务。

整体架构

定时器做的细致情况下,功能层面可以分为两个大体模块:执行器和调度器。

我们在刚开始改进定时器的时候,弃用延时队列和抢占式调度,采用类似 master/slave 结构,应用时间轮触发调度,采用调度策略、任务池、队列来保证任务执行顺序的可控性。

被动调用或者主动轮询是触发定时器的常见手段,尤其对于一些外部任务或者存储任务。

模块图

当我们确定用调度器和执行器这种结构去实现定时器的时候,我们需要确定下两者的功能定位和边界。

调度器来负责任务的拆分、执行器的管理、调度策略的安排,以及分配某个任务给某个执行器在特定时间和策略去执行该任务。

执行器主要接受调度器分配的任务,通过等待队列,执行队列和结果队列来组织任务的有序执行。同时具备相关回调,rpc 通信,日志记录,心跳上报等功能。

我们将需要定时执行的任务存储在 db 内,然后通过轮询机制判断任务是否到达该执行的预备时间,进行提前加载任务以及任务的拆分,将拆分后的任务预先加载到内存中,等待调度器去调度。

泳道图

整个架构流程图如下:

该架构是一个比较丰富的架构体系,从任务的提前拆分,到调度器对最小粒度任务的调度,再到执行器根据调度策略的执行,最终到日志的落库,是一个较为完善的架构体系,并且也在生产中得到了应用,目前服务的量级相当可观,但是实现也很麻烦。

本架构细致的拆分基本可以为各个环节提供性能优化空间,实现后可以满足大部分的定时场景。其中的每一个环节在不同业务场景下都具备一些较为深挖的点,后续有时间也会发布相关内容进行补充。

功能模块

调度器

当我们注册一个任务后,我们将这个任务放入到任务单元,等待被执行。

此时我们采用轮询方式去轮询 mysql 进行任务的提前获取任务,进行任务拆分,任务单元预加载到 redis 或者某个内存服务中。

这个内存服务的数据结构就是一个比较特别的点,我们可以实现的方式很多中,比如采用时间轮主动去触发,或者采用一个 redis 数据结构,执行器去轮询,两种方式各有优弊,大家需要根据自身的实际情况去做选择和实现。

时间轮

时间轮算是一个常见的处理定时任务的结构,我们可以用一个时间轮也可以用多个时间轮来实现定时调度,当然针对于一些简单场景,轮询 redis 去触发调度也是一种较为常见的手段,总之明确一个环节的作用,具体的实现方式能有很多种,没有最优的,只有最适合的。

本次设计呢,我们将简单的用一个一级时间轮来进行任务的调度,由于任务会提前进行拆分和加载,我们真正执行的任务是拆分过后的任务,将其放入到时间轮中,由时间轮去触发,时间轮只负责触发不会去处理业务逻辑,保证时间轮实现的纯粹。

我们根据业务需求将时间轮按秒级别,分 60 片,做一个环形,每次到点的时候就将该秒级槽内对应时间段的任务队列进行取出,根据评分权重均衡分配给执行器按照调度策略去执行。

为了保证任务得到均等的执行,每个槽内的任务将会在同一时间得到均等的被选取的机会,执行的优先级由执行器去执行,但是选择的优先级和均等的机会由调度器去执行操作。

这个时间轮的实现成本其实要做好的话还是有成本的,尤其解决单点瓶颈,这块是需要开发者们根据实际情况去实现。

当然还有另一种简单常见方式,那就是去轮询了。

redis 轮询

我们可以将任务存储在一系列 redis key 下,通过执行器去抢占式调用,每个执行器启用一个协程去调度。

这种方式将会降低调度器的作用,把一些执行策略放在执行器自己去执行,这种方式的数据结构,可以给大家一个参考如下:

整个轮询步骤可以分为:

1、拆分 job execution unit 最小执行单元;

2、redis 同步 unit,并以 job-execution-id 和 unit 以及时间构成一个 kv 关系;

3、执行器每秒轮询 redis 到时需要执行的 job-execution-id,然后从 job-execution-id 下的队列里依次取;

4、需要执行的 unit,并修改 unit 状态;

5、执行器将 unit 放入到执行队列里;

6、执行器从执行队列里抽取任务执行,若三次取出的数据为空,则将该队列删除;

7、执行成功或者失败后,修改 unit 状态。

当然为了让抢占式更加公平,我们也可以使用令牌桶机制,让其提前获取任务,或者采用范围时间打点的方式来保障这个抢占是流量均衡的。但这个方式个人觉得收益可能并不高,主要适应在大规模资源分布不均匀情况下,大家可以根据自身的一些情况去实现不同。

调度策略

执行器注册时,以 CPU、任务数量、内存等多维度指标,为执行器做一个综合计算值;

获取任务时,也给任务的优先级和场景做一个综合评分;

值高的执行器优先分配值高的任务,执行器优先级和任务优先级做一个匹配契合度,层级判断,来保证任务分发的合理性和平均性。

另外调度器是否是需要进行多机群的高可用,其实本质上去实现这套东西,就是采用分布式一致性的解法,本人也比较懒,这类通信方式,可以借鉴 etcd、zookeeper 的实现,后续感兴趣的话,等抽时间再分享一下下。

执行器

当调度器设计完成后,我们就会想着怎么去执行调度器的任务计划了。

时间轮执行

当调度器为时间轮的时候,执行器的功能本质上是可以做一些简化,只需要评估自身情况去执行调度器分配的任务即可,当资源不够的时候,即使上报,让调度器知晓后,评分降低后,下一波任务将会减少或者停止发放。

执行器实时上报自身信息到调度器,调度器根据评分决定哪些任务分配去该执行器去执行,评分较高的执行器得到的任务越多,同时调度器选择执行任务的时候,也将保证公平均等原则,保证每个业务线上的任务得到执行的机会是均等,保证均等这一步骤由调度器去执行,这样执行器只需要关心两件事,一是上报自身数据,二是执行调度器下发的任务。

当拿到任务后,执行器将根据自身的任务池、等待队列、执行队列、任务优先级等保证任务的公平执行的机会。

轮询执行

当调度器为 redis 结构,需要执行器去轮询的时候,这时候,我们就是要执行器本身增加一些轮询机制和策略,比如下面这个场景案列。

当任务 A 1 万个任务分片,任务 B 1000 个任务分片,任务 A 先于任务 B 执行,所有任务首先都存入到 redis,下游消费速度是 1000 任务分片/s,任务执行队列的长度为 1000;

执行器轮询 redis,当任务 A 触发执行的时候,此时只存在任务 A,从任务 A 里去取 1000 个任务分片,拿到后存入任务的执行队列;

任务从执行队列里拿取任务去执行;

下次轮询的时候,会根据执行队列里剩余的空间,去 redis 去取任务,比如剩下 200 任务分片空间,就去 redis 拿取 200 个任务分片;

当 B 触发执行时,gjob 机器 A 上还可以执行 500 个任务分片;

下次轮询会平均地从任务 B 队列里面取出 250 个任务和任务 A 队列里面抽取出 250 个任务,并存入到执行队列

这样保证下游消费速度的同时,也保证了均衡。

执行结构

整个执行器的执行,我们可以增加任务池的概念,可以方便管控和调度任务的执行,执行队列、等待队列可以更细粒度限速定时任务的执行。


首先执行器从调度器里分配到执行任务的时候,先放到本地任务池,根据调度策略,在任务池里按照时间(时间靠后的先取)、优先级(优先级高的先取)批次取出任务,若满足执行条件放入到执行队列,若不满足执行条件,满足等待条件,放入到等待队列里,等待队列会首先放入到执行队列里,以备去执行。

执行器从队列里拿到所需要执行的任务后,就去根据任务类型去执行,执行结果放入到结果队列。

从结果队列里抽取任务结果进行出队,并回调给调度器汇报执行结果,调度器根据执行结果更新任务状态或者启用失败尝试策略。

另外对于定时任务,我们还需要要考虑下游的消费情况,很多时候下游消费不过来会造成阻塞状态,针对于这种情况,我们每次执行的时候,需要对一些优先级高的任务进行均等执行机会,而不需要一直等待前面阻塞的任务,对于下游消费过慢情况,可以考虑任务的暂缓以均速去下发,同时防止内存的膨胀,这点上可以深挖的点也蛮多的,读者可以根据自身工作情况去研究,想必也有不少收获。

后言

好了,本人比较懒,文章就先写到这了,写得比较宏观,很多点的代码设计都可以深挖,甚至也有很多不同的架构方式都是很赞的。开源东西过多,鄙人就起个抛砖作用,有问题可以留言一起交流。

其实业内很多定时器都大同小异,也有各自的优缺点,关键还是要最适合自己的,简单场景 crontab 就可以直接去做,也可以把人家开源东西直接拿来用都是可行的,但学习别人思想也是很重要的点。

时间轮的方式算是一种比较正规较重的实现方式,可以参考 kafka 的实现,根据自身业务去做适应改变。

而轮询 redis 是一种常见的、实现起来较为轻的方式,想必很多厂内部的定时器也是基于这种方式,这种方式个人觉得比较精妙的地方在于开发者针对于业务对 key 的定义,好的数据结构能让定时器的性能在简单的架构下得到较高的提升。


没有最完美的架构,只有更适合当下业务场景的方案。

在工作中思考,在思考中沉淀,慢慢生活。

只写原创,净化环境


发布于: 刚刚阅读数: 5
用户头像

Quincy

关注

路过岁月 2018-08-01 加入

gopher

评论

发布
暂无评论
不咋简单的定时任务设计_crontab_Quincy_InfoQ写作社区