RocketMQ 5 的延迟队列实现

RocketMQ 5 的延迟队列于之前的版本比,支持任意时刻的延迟。 我们来看看它是如何实现的。

主干流程


1. 当一个属于 topic A 的消息保存至 commitlog 之前,会被各种钩子修改,其中一个钩子即检查消息是否有延迟属性。如果属于延迟消息,会修改消息原本目标 topic 为 rmq_sys_wheel_timer, queueId 改为 0。因为只有本地 broker 会消费这个 queue ,所以这个延迟消息的 queue 只有一个。  

2. 在 broker 启动时,即会启动 TimeMessageStore。而后者在启动时,会启动多个ServiceThread。其中,TimeEnqueueGetService 负责从延迟拉取消息,并维护好当前的拉取进度(currQueueOffset)。拉取出来的消息封装为 TimerRequest 后,均进入 enqueuePutQueue 队列。

3. TimerEnqueuePutService 负责从 enqueuePutQueue 获取 TimerRequest,并对其进行过期判断。如果消息的 delayTime 小于 currWriteTimeMs,表明这个延迟消息已经过期,放入时间轮已经没有必要。在这个情况下,消息会绕开时间轮,直接进入 dequeuePutQueue。若消息 delayTime 大于 currWriteTimeMs,则将消息放入时间轮。 同时,这个serivce 负责推进 commitQueueOffset 和 currWriteTimeMs。

4. TimerDequeueGetService,  将到期的消息从时间轮中取出并进行分组,分为待删除组和正常组。然后按组进入 dequeuePutQueue队列。 没错,延迟消息在还没有最终落到原 topic 之前,都可以后悔,详细过程后表。同时,这个service 负责推进 currReadTimeMs。

5. TimerDequeueGetMessageService,将未删除的消息放入dequeuePutQueue。

6. TimerDequeuePutMessageService,最终将消息从 dequeuePutQueue 取出并放入消息原本属于的 topic 和 queue 中,自此延时过程结束。


时间轮的实现

时间轮的的优点以及一般实现方式:分层的也好,记圈的也好,已不是什么神秘的事了。但 RocketMQ 的实现比较特别,可以来一探究竟。  

基于文件实现的时间轮

时间轮由 slot 组成,一个时间轮默认有 7 * 24 * 3600 * 2 个 slot,即两周的秒数,每个 slot 固定长度 32 字节。 因此一个时间轮是一个大小约为37M的文件。每一个 slot 的格式如下:


由 slot 的格式可以看到,每一个 slot 存放了一个延迟消息的 offset 列表。这里的 offset,并不是 consume queue 的 offset,而是 TimerLog 的 offset,而 TimerLog 的一个单元为固定长度 52 字节,格式如下:


所以,timewheel 的存储逻辑上如下:


时间如何推进

1. 在主干流程中的第三步,会推进 currWriteTimeMs 往前走:

     if (currWriteTimeMs < formatTimeMs(System.currentTimeMillis())) {
            currWriteTimeMs = formatTimeMs(System.currentTimeMillis());
        }


currWriteTimeMs 表示当前时刻 (时间轮 tick time 的整倍数)。如果消息的延迟时间小于 currWriteTimeMs,那么消息不会进入时间轮,因为这个消息已经到期,直接进入原 topic 即可。

2. currReadTimeMs 表示当前时间轮处理的时刻,这个时刻不能大于 currWriteTimeMs。所以时间轮上的 slot 可以分为三部分,如图

总之,TimerEnqueuePutService 负责轮询延迟队列并往时间轮中添加消息,TimerDequeueGetService 负责轮询时间轮并把到期消息取出处理。

延迟消息的滚动

这里的时间轮,默认的 slot 数量为 2周的秒数。如果延迟时间超过2周,那么就会产生 overflow,消息会插入到一个不正确的 slot 中。如何处理超长的延迟消息呢? 这里的过程如下:

举例:
有一个需要延迟15天的消息,走到第三步时。发现15天大于 配置的 timerRoleWindowSlot (10*24*3600, 即10天)。于是 消息在打上 needRoll 的标示后,按延迟10天的时间进入时间轮。
10天过去后,该消息被取出。在第六步时发现了消息的 needRoll 标示并送回至 rmq_sys_wheel_timer 队列(而非该消息的原队列)。此后该消息再一次开启他的时间轮之旅,只是在第三步时,延迟时间已经只剩5天了,没有超出 timerRoleWindowSlot 的设置。 因此走上了开篇介绍的主干流程。

延迟消息的删除

待续

时间轮的持久化

待续
TimerCheckPoint 的结构:

时间轮的高可用

RocketMq 的高可用依赖于主从复制或者通过 DLedger 实现的 Raft 一致性协议。这里的 rmq_sys_wheel_timer  也不例外。
此外,在主干流程的第二步,即 TimerEnqueueGetService 开始工作前,就会判断当前 broker 是否为 master,只有在 master 的情况下才会往 enqueuePutQueue 中放置消息,后续流程才能继续。而 slave 的 broker,则会定期向 master 请求同步 TimerCheckPoint。
在 ReplicasManager (当使用主从复制时) 和 DLedgerRoleChangeHandler (当使用raft时) 类的的 handleSlaveSynchronize 方法中,如果 broker 角色是 slave,会启动一个间隔3秒的定时任务,向 master 不断轮询最新的 lastReadTimeMs 和 masterTimerQueueOffset,尽量保持和 master 一致的时间轮消费进度。 主要代码大致如下:
 

private void handleSlaveSynchronize(final BrokerRole role) {
        if (role == BrokerRole.SLAVE) {
            this.brokerController.getSlaveSynchronize().setMasterAddr(this.masterAddress);
            slaveSyncFuture = this.brokerController.getScheduledExecutorService().scheduleAtFixedRate(() -> {
                try {
                    //timer checkpoint, latency-sensitive, so sync it more frequently
                    brokerController.getSlaveSynchronize().syncTimerCheckPoint();
                } catch (final Throwable e) {
                    LOGGER.error("ScheduledTask SlaveSynchronize syncAll error.", e);
                }
            }, 1000 * 3, 1000 * 3, TimeUnit.MILLISECONDS);
.....

当 某个 slave 转变为 master 时,就会从这个消费进度的基础上继续往前推进。 在主干流程的第二步中,如果发现自己的角色从 slave 转变成了 master, 就会根据 TimerCheckPoint 做好准备工作,然后才开始后续的流程

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值