猿学-延迟任务调度系统—技术选型与设计

时间轮实现

时间轮是一种环形的数据结构,分成多个格。

每个格代表一段时间,时间越短,精度越高。

每个格上用一个链表保存在该格的过期任务。

指针随着时间一格一格转动,并执行相应格子中的到期任务。

名词解释:

时间格:环形结构中用于存放延迟任务的区块

指针:指向当前操作的时间格,代表当前时间

格数:时间轮中时间格的个数

间隔:每个时间格之间的间隔,代表时间轮能达到的精度

总间隔:当前时间轮总间隔,等于格数*间隔,代表时间轮能表达的时间范围

单表时间轮

以上图为例,假设一个格子是1秒,则整个时间轮能表示的时间段为8s, 如果当前指针指向2,此时需要调度一个3s后执行的任务,需要放到第5个格子(2+3)中,指针再转3次就可以执行了。

单表时间轮存在的问题是:

格子的数量有限,所能代表的时间有限,当要存放一个10s后到期的任务怎么办?这会引起时间轮溢出。

有个办法是把轮次信息也保存到时间格链表的任务上。

如果任务要在10s后执行,算出轮次10/8 round等1,格子10%8等于2,所以放入第二格。

检查过期任务时应当只执行round为0的任务,链表中其他任务的round减1。

带轮次单表时间轮存在的问题是:

如果任务的时间跨度很大,数量很大,单层时间轮会造成任务的round很大,单个格子的链表很长,每次检查的量很大,会做很多无效的检查。怎么办?

分层时间轮

过期任务一定是在底层轮中被执行的,其他时间轮中的任务在接近过期时会不断的降级进入低一层的时间轮中。

分层时间轮中每个轮都有自己的格数和间隔设置,当最低层的时间轮转一轮时,高一层的时间轮就转一个格子。

分层时间轮大大增加了可表示的时间范围,同时减少了空间占用。

举个例子:

上图的分层时间轮可表达8 8=512s的时间范围,如果用单表时间轮可能需要512个格子, 而分层时间轮只要8+8+8=24个格子,如果要设计一个时间范围是1天的分层时间轮,三个轮的格子分别用24、60、60即可。

工作原理:

时间轮指针转动有两种方式:

根据自己的间隔转动(秒钟轮1秒转1格;分钟轮1分钟转1格;时钟轮1小时转1格)

通过下层时间轮推动(秒钟轮转1圈,分钟轮转1格;分钟轮转1圈,时钟轮转1格)

指针转到特定格子时有两种处理方式:

如果是底层轮,指针指向格子中链表上的元素均表示过期

如果是其他轮,将格子上的任务移动到精度细一级的时间轮上,比如时钟轮的任务移动到分钟轮上

举个例子:

添加1个5s后执行的任务

算出任务应该放在秒钟轮的第5个格子

在秒钟轮指针进行5次转动后任务会被执行

添加一个50s后执行的任务

算出该任务的延迟时间已经溢出秒钟轮

50/8=6,所以该任务会被保存在分钟轮的第6个格子

在秒钟轮走了6圈(6*8s=48s)之后,分钟轮的指针指向第6个格子

此时该格子中的任务会被降级到秒钟轮,并根据50%8=2,任务会被移动到秒钟轮的第2个格子

在秒钟轮指针又进行2次转动后(50s)任务会被执行

添加一个250s后执行的任务

算出该任务的延迟时间已经溢出分钟轮

250/8/8=3,所以该任务会被保存在时钟轮的第3个格子

在分钟轮走了3圈(3*64s=192s)之后,时钟轮的指针指向第3个格子

此时该格子中的任务会被降级到分钟轮,并根据(250-192)/8=7,任务会被移动到分钟轮的第7个格子

在秒钟轮走了7圈(7*8s=56s)之后,分钟轮的指针指向第7个格子

此时该格子中的任务会被降级到秒钟轮,并根据(250-192-56)=2,任务会被移动到秒钟轮的第2个格子

在秒钟轮指针又进行2次转动后任务会被执行

优点:

高性能(插入任务、删除任务的时间复杂度均为O(1),DelayQueue由于涉及到排序,插入和移除的复杂度是O(logn))

缺点:

数据是保存在内存,需要自己实现持久化

不具备分布式能力,需要自己实现高可用

延迟任务过期时间受时间轮总间隔限制

对于超出范围的任务可放在一个缓冲区中(可用队列、redis或数据库实现),等最高时间轮转到下一格子就从缓冲中取出符合范围的任务落到时间轮中。 

比如:

添加一个600s后执行的任务A

算出该任务的延迟时间已经溢出时间轮

所以任务被保存到缓冲队列中

在时钟轮走了1格之后,会从缓冲队列中取满足范围的任务落到时间轮中

缓冲队列中的所有任务延迟时间均需减去64s,任务A减去64s后是536s,依然大于时间轮范围,所以不会被移出队列

在时钟轮又走了1格之后,任务A减去64s是536-64=472s,在时间轮范围内,会被落入时钟轮

之前的设计(DB/DelayQueue/ZooKeeper)

调度系统提供任务操作接口供业务系统提交任务、取消任务、反馈执行结果等。

针对dubbo调用,将任务抽象成JobCallbackService接口,由业务系统实现并注册成服务。

整体架构

数据库:

负责保存所有的任务数据

内存队列:

实际为DelayQueue,延迟任务精确触发的机制由它保证

只存储未来N分钟内过期且最多1000个任务

ZooKeeper:

管理整个调度集群

存储调度节点信息

存储节点分片信息

主节点:

有新的节点上下线时对数据重新分片

调度节点:

提供dubbo、http接口供业务系统调用,用于提交任务、取消任务、反馈执行结果等

从ZK注册中心获取当前节点的分片信息,再从数据库拉取即将过期的数据放到DelayQueue

调用业务系统注册的回调服务接口,发起调度请求

接收业务系统的反馈结果,更新执行结果,移除任务或发起重试

业务系统:

作为被调度的服务需要实现回调接口JobCallbackService,并注册为dubbo服务提供者

在需要延迟任务的场景调用调度系统接口操作任务

数据库设计

表说明

job_callback_service:服务配置表,配置业务回调服务,包括服务协议、回调服务、重试次数

job_delay_task:延迟任务表,用于存储延迟任务,包括任务分片号、回调服务、调用总次数、失败数、任务状态、回调参数等

job_delay_task_execlog:延迟任务执行表,记录调度系统发起的每一次回调

job_delay_task_backlog:延迟任务调度结果表,记录任务最终状态等信息

主从切换

利用ZooKeeper临时序列节点特性,序号最小的节点为主节点,其他节点为从节点。

主节点监听集群状态,集群状态发生变化时重新分片。

从节点监听序号比它小的兄弟节点,兄弟节点发生变化重新寻找和建立监听关系。

数据分片

任务状态

delay:延迟任务提交后的初始状态

ready:过期时间已到,消息推入就绪队列的状态

running:业务订阅消息,收到消息开始处理的状态

finished:业务处理成功

failed:业务处理失败

主要流程

服务加载

从DB读取服务配置

根据配置动态构造Consumer对象并添加到Spring容器中

提交任务

业务系统通过dubbo或http接口提交任务

判断任务过期时间是否在一个扫描周期内

如果是,

设置分片号(从当前节点所负责的分片随机获取)

添加到内存队列

任务保存到job_delay_task表

如果否,

设置分片号(根据分片总数和随机算法算出分片号)

任务保存到delay_task表

定时器

由一个线程管理

根据配置的扫描间隔设置定时器的执行周期

根据当前时间和扫描间隔算出该时段的过期时间X-Delay

从DB获取过期时间在X-Delay之前的所有任务,并放到DelayQueue

调度任务

由一个线程池管理

所有线程都阻塞在DelayQueue的方法take

take到任务,从DB中获取任务,判断是否存在

如果不在,什么也不做(任务已执行成功或已被删除)

如果存在,判断调用次数是否超过设置

如果不超

调用业务回调服务

从任务中取出调用的服务配置

从容器中获取对应的Consumer对象

异步调用业务回调服务

设置下次重试时间,记录调用日志job_delay_task_execlog

如果超过,将任务转移到job_delay_task_backlog

任务反馈

更新任务调用结果

优点

功能全面,高可用、易伸缩、可重试

缺点

略微复杂

需要将服务配置动态生成为Consumer对象

增加新的服务需要通知所有调度节点刷新

存在一定的耦合性(直接调用业务服务,协议耦合),如果接入系统是thrift协议呢?

需要处理任务的重试

调度系统直接回调业务服务,如果业务服务不可用可能会造成盲目重试,不能很好的控制流量(调度系统不知道业务服务的处理能力)

如果引入MQ,使用MQ来解耦服务调用的协议,保证任务的重试,并由消费方根据自己的处理能力控制流量会不会更好呢?

另一种方案(DB/DelayQueue/ZooKeeper/MQ)

整体架构

数据库设计

主要流程

调度任务

由一个线程池管理

所有线程都阻塞在DelayQueue的take方法

take到任务,从DB中获取任务,判断是否存在

如果不在,什么也不做(任务已执行成功或已被删除)

如果存在,将任务转移到job_delay_task_execlog;往消息队列投递消息

缺点

需要业务系统依赖于MQ

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容