RocketMQ 延时方案分析与总结

政采云技术团队.png

知行.png

一.需求背景 (Background & Motivation)

1.1 概念 (Concept)

  • 定时消息:Producer 将消息发送到消息队列 RocketMQ 版服务端,但并不期望立马投递这条消息,而是推迟到在当前时间点之后的某一个时间投递到 Consumer 进行消费,该消息即定时消息。
  • 延时消息:Producer 将消息发送到消息队列 RocketMQ 版服务端,但并不期望立马投递这条消息,而是延迟一定时间后才投递到 Consumer 进行消费,该消息即延时消息。

定时消息与延时消息在代码配置上存在一些差异,但是最终达到的效果相同:消息在发送到消息队列 RocketMQ 版服务端后并不会立马投递,而是根据消息中的属性延迟固定时间后才投递给消费者。

1.2 解决何种问题 (What problem is this proposal designed to solve?)

目前在RocketMQ(社区版本)的功能中,已经支持了延时消息。 但对于该功能只支持固定粒度的延迟级别(18级), 如:

1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h。
复制代码

虽然这些粒度可以配置,但是还是无法应对广泛的业务场景。

比如定义了 1s 、5s 的 Level,那么用户只能发送 1s 或 5s 的延时消息,不能发送 3s 延迟的消息。
复制代码

对比RocketMQ(阿里云)版本已经支持了 40 天内任意时延的消息。

(注:在本文中将讨论如何在 RocketMQ(社区版本)改造任意时延消息的方案。)

二.目标 (Goals)

在RocketMQ(社区版本)中添加任意时延消息的功能:

  • 精度支持到秒级别

  • 最大支持 30 天的延迟

三.困难点 (Difficulties)

那么任意时延消息的难点在哪?

  • 排序

对于服务端收到的延时消息,需要对投递时间进行排序。在 MQ 中,为了保证可靠性,消息是需要落盘的,且对性能和延迟的要求,决定了在服务端对消息进行排序是完全不可接受的。

  • 消息存储

对于延时消息来说,需要存储最近30天的消息。而 RocketMQ 是基于 WAL,对于高流量的场景下,存储 30 天的消息需要多少服务器。

四.设计思路 (Design)

4.1 原延时方案 (Original scheme)

先来看一下原延时方案的设计:

对于 normal msg 来说:

  • 消息会被顺序写入 commitlog;

  • 在异步 reput 中,通过 DispatchService 生成对应的 consumer queue 索引;

  • 消费者即可以消费对应的消息。

对于 delay msg 来说:

  • 消息在被写入 commitlog 前,会将 topic 设置为 SCHEDULE_TOPIC,并将 real topic 保存在 properties 中,并且将 queueId 设置为 delay level 的值;

  • 在异步 reput中,通过 DispatchService 会生成 SCHEDULE_TOPIC 的 consumer queue,共18个;

  • 在 ScheduleMessageService 中给每个 level 设置定时器,从 ScheduledConsumeQueue 中读取信息。如果 ScheduledConsumeQueue 中的元素已经到时,那么从 CommitLog 中读取消息内容,恢复消息内容写入 CommitLog,再按 normal msg 消费。

方案分析:

  • 优点1:将排序转化为分类

  • 优点2:Level数固定即线程数量固定,开销不大

  • 缺点1:固定Level,不够灵活

  • 缺点2:若延时很长,会导致CommitLog的量很大

4.2 新延时方案 (New scheme)

4.2.1 如何解决排序

我们可以用原方案类似的方式,用分类操作解决排序。

可以使用 TimeWheel:《Hashed and Hierarchical Timing Wheels: Data Structures for the Efficient Implementation of a Timer Facility》

我们只需要将即将发送的消息加载到TimeWheel中,再按时间取出delay msg将其放入real topic中。

这里的“即将发送的消息”可以定义为 30 分钟(timeSliceLength)内的消息,TimeWheel中每一个格子为1秒,那么一个 TimeWheel 为 1800格。

这里我们可以估算一下TimeWheel的内存需求,按每个消息 500B,那么 200万 个消息则需要 1G 的内存。

我们可以根据延迟消息的量,来设置 timeSliceLength 的大小。以下我们将以 timeSliceLength=1800 展开讨论。

4.2.2 如何解决消息存储

将延时消息单独存储至 CommitLog(以下将此称为“DCommitLog”)

那么如何设计 DCommitLog?我们可以将时间段按 timeSliceLength 进行散列,将整个 timeSliceLength 时间内的 delay msg 放入同一个 DCommitLog 中:

# 公式
SliceTime = timestamp / timeSliceLength / 100

# 若delay msg的投递时间为 1641349066282,则带入公式得到
    1641349066282 / 1800 / 1000 = 911860
# 即该消息会被写入 911860 的 DCommitLog 中。
复制代码

4.2.3 延时方案

有了以上基本的概念,我们设计一下整个延时方案:

ScheduleMessageService 主要完成 DispatchMsg 和 TimeWheelTask。

  • DispatchMsg 对 SCHEDULE_TOPIC 按 delayOffset 进行内部消费,并根据 deliveryTime 将消息投递到 real topic、TimeWheel、DCommitLog。
  • TimeWheelTask 则按 currentTime 从 TimeWheel 取出消息投递到 real topic 并提前加载下一个时间片的消息、切换 TimeWheel。这里的 currentTime 是 TimeWheel 所用的,而非现实的时间戳。
  • delayOffsetcurrentTime 需要落盘到 delayOffset.json。

  1. 消息在被写入 commitlog 前,会将 topic 设置为 SCHEDULE_TOPIC,并将 real topic 保存在 properties 中,并且将 queueId 设置为1;
  2. consume with delayOffset:根据 delayOffset 消费 SCHEDULE_TOPIC,得到 delay msg;
  3. 根据 delay msg 计算 deliveryTime,若 deliveryTime < currentTime,即消息已经超时直接将消费放入 real topic,进入7,否则进入4;
  4. 若 deliverySliceTime == currentSliceTime,即消息在本时间片内,将其放入 TimeWheel 和 DCommiLog 中,进入7,否则进入5;
  5. 若 deliverySliceTime == currentSliceTime +1 && nextTimeWheel != null,即消息在下一时间片并且下一时间轮已经加载,则将消息放入 nextTimeWheel 和 DCommitLog,进入7,否则进入6;
  6. 消息在后面的时间片后,直接将其放入 DCommiLog 中;
  7. 根据 currentTime 从 TimeWheel 获取需要投递到 real topic 中的消息集合;
  8. 投递到 real topic;
  9. 若还没有开始加载下一个时间片的数据,未开始加载进入10,已开始则进入11;
  10. 若 currentTime 接近 nextTimeSlice,异步加载下一个时间片 DCommiLog 中的数据到下一个时间轮中,否则12;
  11. 若 currentTime 已经到 nextTimeSlice,则将时间轮切换至下一个时间轮;
  12. 更新 currentTime、delayOffset,每次 currentTime 最多增长1秒,且不能超过 system.currentTimeMillis(),进入2;

4.3 总结 (Summary)

以上流程,分为 DispatchMsg、TimeWheelTask,DispatchMsg 分发 delay msg 将其归类到 DCommitLog 中,TimeWheelTask 则取出 DCommitLog 放入 TimeWheel 中进行时间调度并将 msg 放入 real topic 中。

方案待优化

DCommitLog 会带来随机读写的问题,用 mmp 的同时也会影响 CommitLog 的 PageCache。

可以考虑 normal broker 和 delay broker 分离部署的方式来解决或者调整 DCommitLog 的文件大小优化。

另外因为 TimeWheel 存在在内存中,对于 JVM 的内存需求也会随之上升。

4.3.1 对应的MASTER-SLAVE、DLeger模式需要作何处理?

  • 在 MASTER-SLAVE 中,由于 SLAVE 没有写入权限 TimeWheelTask 则不需要启动,而 SLAVE 不会自动切换 Master,也就不需要生成 DCommitLog;只能重启 master;

  • 在 DLeger 模式中,SLAVE 同样没有写入权限,TimeWheelTask 不需要启动。但是 SLAVE会自动切换 Master,此时需要生成 DCommitLog 用于在容灾恢复后保证 delay msg 可以正常被消息。这里需要将 Matser的delayOffset.json 的数据同步到 Slave 上。

4.3.2 对重试策略的影响

由于重试依赖于原延时方案的固定粒度时间间隔,在使用本方案改造后需要对 sendMessageBack 做额外重试策略的处理。

4.3.3 方案注意事项

1、DCommitLog 会带来随机读写的问题,用 mmp 的同时也会影响 CommitLog 的 PageCache。可以考虑 normal broker 和 delay broker 分离部署的方式来解决或者调整 DCommitLog 的文件大小优化。

2、因为 TimeWheel 存在在内存中,对于 JVM 的内存需求也会随之上升。对于延迟消息使用较高的场景需要特别注意合理设置 JVM 大小。优化:TimeWheel 中的 msg 改为 msg 的 offset,可明显降低 JVM 的内存需求,1G=6250万 条消息

以上就是《RocketMQ延时方案分析与总结》, 希望能给你在工作中起到辅助作用,如有任何问题可以在评论区中提出你的想法和建议。

推荐阅读

Dapr 实战(一)

Dapr 实战(二)

政采云Flutter低成本屏幕适配方案探索

招贤纳士

政采云技术团队(Zero),一个富有激情、创造力和执行力的团队,Base 在风景如画的杭州。团队现有300多名研发小伙伴,既有来自阿里、华为、网易的“老”兵,也有来自浙大、中科大、杭电等校的新人。团队在日常业务开发之外,还分别在云原生、区块链、人工智能、低代码平台、中间件、大数据、物料体系、工程平台、性能体验、可视化等领域进行技术探索和实践,推动并落地了一系列的内部技术产品,持续探索技术的新边界。此外,团队还纷纷投身社区建设,目前已经是 google flutter、scikit-learn、Apache Dubbo、Apache Rocketmq、Apache Pulsar、CNCF Dapr、Apache DolphinScheduler、alibaba Seata 等众多优秀开源社区的贡献者。如果你想改变一直被事折腾,希望开始折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊……如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的技术团队的成长过程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 [email protected]

微信公众号

文章同步发布,政采云技术团队公众号,欢迎关注

政采云技术团队.png

猜你喜欢

转载自juejin.im/post/7088841619515899934