原创不易,转载请注明出处
前言
延时音讯(定时音讯)指的在 分布式异步音讯场景 下,生产端发送一条音讯,心愿在指定延时或者指定工夫点被生产端生产到,而不是立即被生产。
延时音讯实用的业务场景十分的宽泛,在分布式系统环境下,延时音讯的性能个别会在下沉到中间件层,通常是 MQ 中内置这个性能或者内聚成一个公共根底服务。
本文旨在探讨常见延时音讯的实现计划以及方案设计的优缺点。
实现计划
1. 基于内部存储实现的计划
这里探讨的内部存储指的是在 MQ 自身自带的存储以外又引入的其余的存储系统。
基于内部存储的计划实质上都是一个套路,将 MQ 和 延时模块 辨别开来,延时音讯模块是一个独立的服务 / 过程。延时音讯先保留到其余存储介质中,而后在音讯到期时再投递到 MQ。当然还有一些细节性的设计,比方音讯进入的延时音讯模块时曾经到期则间接投递这类的逻辑,这里不展开讨论。
下述计划不同的是,采纳了不同的存储系统。
基于 数据库(如 MySQL)
基于关系型数据库(如 MySQL)延时音讯表的形式来实现。
CREATE TABLE `delay_msg` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`delivery_time` DATETIME NOT NULL COMMENT '投递工夫',
`payloads` blob COMMENT '音讯内容',
PRIMARY KEY (`id`),
KEY `time_index` (`delivery_time`)
)
通过定时线程定时扫描到期的音讯,而后进行投递。定时线程的扫描距离实践上就是你延时音讯的最小工夫精度。
长处:
- 实现简略;
毛病:
- B+Tree 索引不适宜音讯场景的大量写入;
基于 RocksDB
RocksDB 的计划其实就是在上述计划上抉择了比拟适合的存储介质。
RocksDB 在笔者之前的文章中有聊过,LSM 树更适宜大量写入的场景。滴滴开源的 DDMQ 中的延时音讯模块 Chronos 就是采纳了这个计划。
DDMQ 这个我的项目简略来说就是在 RocketMQ 里面加了一层对立的代理层,在这个代理层就能够做一些性能维度的扩大。延时音讯的逻辑就是代理层实现了对延时音讯的转发,如果是延时音讯,会先投递到 RocketMQ 中 Chronos 专用的 topic 中。延时音讯模块 Chronos 生产失去延时音讯转储到 RocksDB,前面就是相似的逻辑了,定时扫描到期的音讯,而后往 RocketMQ 中投递。
这个计划诚实说是一个比拟重的计划。因为基于 RocksDB 来实现的话,从数据可用性的角度思考,你还须要本人去解决多正本的数据同步等逻辑。
长处:
- RocksDB LSM 树很适宜音讯场景的大量写入;
毛病:
- 实现计划较重,如果你采纳这个计划,须要本人实现 RocksDB 的数据容灾逻辑;
基于 Redis
再来聊聊 Redis 的计划。上面放一个比较完善的计划。
本计划来源于:https://www.cnblogs.com/lylif…
- Messages Pool 所有的延时音讯寄存,构造为 KV 构造,key 为音讯 ID,value 为一个具体的 message(这里抉择 Redis Hash 构造次要是因为 hash 构造能存储较大的数据量,数据较多时候会进行渐进式 rehash 扩容,并且对于 HSET 和 HGET 命令来说工夫复杂度都是 O(1))
- Delayed Queue 是 16 个有序队列(队列反对程度扩大),构造为 ZSET,value 为 messages pool 中音讯 ID,score 为过期工夫(分为多个队列是为了进步扫描的速度)
- Worker 代表解决线程,通过定时工作扫描 Delayed Queue 中到期的音讯
这个计划选用 Redis 存储在我看来有几点思考,
- Redis ZSET 很适宜实现延时队列
- 性能问题,尽管 ZSET 插入是一个 O(logn) 的操作,然而 Redis 基于内存操作,并且外部做了很多性能方面的优化。
然而这个计划其实也有须要斟酌的中央,上述计划通过创立多个 Delayed Queue 来满足对于并发性能的要求,但这也带来了多个 Delayed Queue 如何在多个节点状况下平均调配,并且很可能呈现到期音讯并发反复解决的状况,是否要引入分布式锁之类的并发管制设计?
在量不大的场景下,上述计划的架构其实能够堕落成主从架构,只容许主节点来解决工作,从节点只做容灾备份。实现难度更低更可控。
定时线程查看的缺点与改良
上述几个计划中,都通过线程定时扫描的计划来获取到期的音讯。
定时线程的计划在音讯量较少的时候,会浪费资源,在音讯量十分多的时候,又会呈现因为扫描距离设置不合理导致延时工夫不精确的问题。能够借助 JDK Timer 类中的思维,通过 wait-notify 来节俭 CPU 资源。
获取中最近的延时音讯,而后 wait(执行工夫 - 以后工夫),这样就不须要浪费资源达到工夫时会主动响应,如果有新的音讯进入,并且比咱们期待的音讯还要小,那么间接 notify 唤醒,从新获取这个更小的音讯,而后又 wait,如此循环。
2. 开源 MQ 中的实现计划
再来讲讲目前自带延时音讯性能的开源 MQ,它们是如何实现的
RocketMQ
RocketMQ 开源版本反对延时音讯,然而只反对 18 个 Level 的延时,并不反对任意工夫。只不过这个 Level 在 RocketMQ 中能够自定义的,所幸来说对一般业务算是够用的。默认值为“1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h”,18 个 level。
艰深的讲,设定了延时 Level 的音讯会被暂存在名为 SCHEDULE_TOPIC_XXXX
的 topic 中,并依据 level 存入特定的 queue,queueId = delayTimeLevel – 1,即一个 queue 只存雷同延时的音讯,保障具备雷同发送延时的音讯可能程序生产。broker 会调度地生产 SCHEDULE_TOPIC_XXXX,将音讯写入实在的 topic。
上面是整个实现计划的示意图,红色代表投递延时音讯,紫色代表定时调度到期的延时音讯:
长处:
- Level 数固定,每个 Level 有本人的定时器,开销不大
- 将 Level 雷同的音讯放入到同一个 Queue 中,保障了同一 Level 音讯的程序性;不同 Level 放到不同的 Queue 中,保障了投递的工夫准确性;
- 通过只反对固定的 Level,将不同延时音讯的排序变成了固定 Level Topic 的追加写操作
毛病:
- Level 配置的批改代价太大,固定 Level 不灵便
- CommitLog 会因为延时音讯的存在变得很大
Pulsar
Pulsar 反对“任意工夫”的延时音讯,但实现形式和 RocketMQ 不同。
艰深的讲,Pulsar 的延时音讯会间接进入到客户端发送指定的 Topic 中,而后在堆外内存中创立一个基于工夫的优先级队列,来保护延时音讯的索引信息。延时工夫最短的会放在头上,工夫越长越靠后。在进行生产逻辑时候,再判断是否有到期须要投递的音讯,如果有就从队列外面拿出,依据延时音讯的索引查问到对应的音讯进行生产。
如果节点解体,在这个 broker 节点上的 Topics 会转移到其余可用的 broker 上,下面提到的这个优先级队列也会被重建。
上面是 Pulsar 公众号中对于 Pulsar 延时音讯的示意图。
乍一看会感觉这个计划其实非常简单,还能反对任意工夫的音讯。然而这个计划有几个比拟大的问题
- 内存开销: 保护延时音讯索引的队列是放在堆外内存中的,并且这个队列是以订阅组(Kafka 中的生产组)为维度的,比方你这个 Topic 有 N 个订阅组,那么如果你这个 Topic 应用了延时音讯,就会创立 N 个 队列;并且随着延时音讯的增多,时间跨度的减少,每个队列的内存占用也会回升。(是的,在这个计划下,反对任意的延时音讯反而有可能让这个缺点更重大)
- 故障转移之后延时音讯索引队列的重建工夫开销: 对于跨度工夫长的大规模延时音讯,重建工夫可能会到小时级别。(摘自 Pulsar 官网公众号文章)
- 存储开销:延时音讯的时间跨度会影响到 Pulsar 中曾经生产的音讯数据的空间回收。打个比方,你的 Topic 如果业务上要求反对一个月跨度的延时音讯,而后你发了一个延时一个月的音讯,那么你这个 Topic 中底层的存储就会保留整整一个月的音讯数据,即便这一个月中 99% 的失常音讯都曾经生产了。
对于后面第一点和第二点的问题,社区也设计了解决方案,在队列中退出工夫分区,Broker 只加载以后较近的工夫片的队列到内存,其余工夫片分区长久化磁盘,示例图如下图所示:
然而目前,这个计划并没有对应的实现版本。能够在理论应用时,规定只能应用较小时间跨度的延时音讯,来缩小前两点缺点的影响。另外,因为内存中存的并不是延时音讯的全量数据,只是索引,所以可能要积压上百万条延时音讯才可能对内存造成显著影响,从这个角度来看,官网临时没有欠缺前两个问题也能够了解了。
至于第三个问题,预计是比拟难解决的,须要在数据存储层将延时音讯和失常音讯辨别开来,独自存储延时音讯。
QMQ
QMQ 提供任意工夫的延时 / 定时音讯,你能够指定音讯在将来两年内 (可配置) 任意工夫内投递。
把 QMQ 放到最初,是因为我感觉 QMQ 是目前开源 MQ 中延时音讯设计最正当的。外面设计的外围简略来说就是 多级工夫轮 + 延时加载 + 延时音讯独自磁盘存储。
如果对工夫轮不相熟的能够浏览笔者的这篇文章 从 Kafka 看工夫轮算法设计
QMQ 的延时 / 定时音讯应用的是两层 hash wheel 来实现的。第一层位于磁盘上,每个小时为一个刻度 (默认为一个小时一个刻度,能够依据理论状况在配置里进行调整),每个刻度会生成一个日志文件(schedule log),因为 QMQ 反对两年内的延时音讯(默认反对两年内,能够进行配置批改),则最多会生成 2 366 24 = 17568 个文件(如果须要反对的最大延时工夫更短,则生成的文件更少)。 第二层在内存中,当音讯的投递工夫行将到来的时候,会将这个小时的音讯索引 (索引包含音讯在 schedule log 中的 offset 和 size) 从磁盘文件加载到内存中的 hash wheel 上,内存中的 hash wheel 则是以 500ms 为一个刻度。
总结一下设计上的亮点:
- 工夫轮算法适宜延时 / 定时音讯的场景,省去延时音讯的排序,插入删除操作都是 O(1) 的工夫复杂度;
- 通过多级工夫轮设计,反对了超大时间跨度的延时音讯;
- 通过延时加载,内存中只会有最近要生产的音讯,更久的延时音讯会被存储在磁盘中,对内存敌对;
- 延时音讯独自存储(schedule log),不会影响到失常音讯的空间回收;
总结
本文汇总了目前业界常见的延时音讯计划,并且探讨了各个计划的优缺点。心愿对读者有所启发。
参考
- http://blog.itpub.net/3155560…
- https://www.cnblogs.com/hzmar…
- https://mp.weixin.qq.com/s/_w…
- https://github.com/qunarcorp/…
- https://github.com/apache/roc…