RocketMQ消息消费中长轮训的应用

这里是weihubeats ,觉得文章不错可以关注公众号小奏技术,文章首发。拒绝营销号,拒绝标题党

RocketMQ 版本

  • 5.1.0

长轮训

一般常用的客户端和服务的交互方式有如下两种方式

  • 短轮训

  • 长轮训

  • 短轮训: client不断向服务端发送请求,服务端不管有没有数据,都返回给client

    • 优点: 实现简单
    • 缺点: client会频繁请求server,影响服务端性能
  • 长轮训: client也是不断向服务器发送请求,不同的是如果服务端没有数据,则会主动将请求hold住,不返回给client,这样client就不会因为得到响应数据马上又向server发送请求

    • 优点:不对频繁对server轮训,减少server压力
    • 缺点:实现相对短轮训更复杂

RocketMQ中消费者向broker请求消息的方式就使用的长轮训。

我们来结合源码具体分析分析

源码分析

client 消息拉取

由于我们的重点是分析长轮训,所以消息拉取我们不会从最初的入口开始分析。

我们直接看看消息拉取的请求发起代码

这里的消息拉取主要有两个请求码

  • LITE_PULL_MESSAGE: 提供了SubscribeAssign的使用方式,使用起来更方便。可以理解为push方式,本质还是pull
  • PULL_MESSAGE : 原始的消息拉取方式,需要自己手动更新消费位点

虽然这里请求码是两个,实际broker的处理器都是同一个

broker

这里我们可以看到LITE_PULL_MESSAGEPULL_MESSAGE请求对应的处理器类都是PullMessageProcessor

所以我们来看看PullMessageProcessor

java 复制代码
org.apache.rocketmq.broker.processor.PullMessageProcessor#processRequest(io.netty.channel.Channel, org.apache.rocketmq.remoting.protocol.RemotingCommand, boolean)

方法

可以看到这个类的逻辑非常多。但是大部分逻辑都和我们今天要分析的主线无关。我们都会跳过,只挑重点分析。

其实前面的代码可以到都是一些topicbroker的权限校验

消息拉取的核心方法在这里

这里消息拉取的处理逻辑是通过CompletableFuture来组装完成的

首先是调用getMessageAsync 获取一个CompletableFuture<GetMessageResult> 随后将GetMessageResult传入pullMessageResultHandler.handle方法

所以长轮训的核心方法还是在pullMessageResultHandler.handle

我们进去看看

可以看到这里的方法会对上面消息拉取的结果进行处理,拉取的结果包括四个状态码

  • ResponseCode.SUCCESS
  • ResponseCode.PULL_NOT_FOUND
  • ResponseCode.PULL_RETRY_IMMEDIATELY
  • ResponseCode.PULL_OFFSET_MOVED

这里我们重点关注ResponseCode.PULL_NOT_FOUND消息没拉取到这个状态码的处理逻辑

这里可以看到两行关键代码

  1. 返回值为null
  2. 调用了PullRequestHoldServicesuspendPullRequest方法

我们来重点分析一下

返回值为null意味着什么?

意味着请求被挂起。

我们可以看看clientserver通信的那块代码

可以看到如果返回值responsenull。则什么也不做,意味着请求被挂起,客户端不会收到任何响应

既然请求被挂了,那么就要接着分析了。请求是如何恢复的

这里我们回到org.apache.rocketmq.broker.longpolling.PullRequestHoldService#suspendPullRequest方法看看

这里可以看到将PullRequest这个请求放入到一个Map中,即

java 复制代码
    protected ConcurrentMap<String/* topic@queueId */, ManyPullRequest> pullRequestTable =
        new ConcurrentHashMap<>(1024);

也就是这个请求被暂存了。那什么时候PullRequest回被拿出来重新处理呢?

我们通过查看pullRequestTable的时候发现有多处

首先还是PullRequestHoldService类,我们看看PullRequestHoldService的继承关系

可以看到PullRequestHoldService继承了ServiceThread抽象类,ServiceThread简单理解就是一个线程,然后你继承他实现你自己的任务。就会开一个单独的线程来跑你这个任务

这里就有一个checkHoldRequest方法

我们看看PullRequestHoldService是何时启动这个线程的

可以看到是BrokerController在执行startBasicService()这个方法会启动PullRequestHoldService

接下来我们看看PullRequestHoldService具体干了什么

java 复制代码
 public void run() {
        log.info("{} service started", this.getServiceName());
        while (!this.isStopped()) {
            try {
                if (this.brokerController.getBrokerConfig().isLongPollingEnable()) {
                    this.waitForRunning(5 * 1000);
                } else {
                    this.waitForRunning(this.brokerController.getBrokerConfig().getShortPollingTimeMills());
                }

                long beginLockTimestamp = this.systemClock.now();
                this.checkHoldRequest();
                long costTime = this.systemClock.now() - beginLockTimestamp;
                if (costTime > 5 * 1000) {
                    log.warn("PullRequestHoldService: check hold pull request cost {}ms", costTime);
                }
            } catch (Throwable e) {
                log.warn(this.getServiceName() + " service has exception. ", e);
            }
        }

        log.info("{} service end", this.getServiceName());
    }
  1. 设置了一个开关,然后就是无线死循环轮训
  2. 查看brokerConfig长轮训开关longPollingEnable是否开启,默认开启
  3. 长轮训则休眠等待5秒,未开启长轮训则使用broker配置shortPollingTimeMills段轮训,默认休眠1s
  4. 休眠完成后调用checkHoldRequest方法检查被hold的请求

所以我们接下来该分析checkHoldRequest方法了

可以看到这里去遍历我们之前暂存的pullRequestTable,然后调用getMaxOffsetInQueue去获取queue中的最大消息偏移量offset

然后调用

java 复制代码
org.apache.rocketmq.broker.longpolling.PullRequestHoldService#notifyMessageArriving(java.lang.String, int, long, java.lang.Long, long, byte[], java.util.Map<java.lang.String,java.lang.String>)

重新处理请求

这里的处理流程如下

  1. 如果queue的消息偏移量(maxOffset)大于client拉取的消息偏移量(pullFromThisOffset),则调用executeRequestWhenWakeup重新执行消息拉取请求,然后返回给client
  2. 如果当前时间>= 请求hold时间+请求超时时间,也会重新处理请求

3. 如果两者都不满足,则将请求放回ManyPullRequest

java 复制代码
                if (!replayList.isEmpty()) {
                    mpr.addPullRequest(replayList);
                }

可以看到这里是5s轮训一次去处理被hold住的消息,那么显然5s的时间间隔是不够的,这样很容易造成消息消费延时

那么该如何处理呢?

我们再看看NotifyMessageArrivingListenerarriving方法在哪里被调用

有三个地方调用。我们看看ReputMessageService

这里也是启动一个线程去轮训,不同的是休眠时间只有1毫秒

看看doReput做了啥

可以看到主要是去拉取消息,通过比较reputFromOffsetcommitlogMaxOffset对比看是否有新消息。

如果有则执行消息分发方法。即

java 复制代码
DefaultMessageStore.this.messageArrivingListener.arriving()

这里就调用了我们之前长轮训处理消息的方法NotifyMessageArrivingListener.arriving

总结

总的来说消息拉取这一块的源码还是非常复杂的,我们只能走马观花查看整体流程。

没办法一一俱到分析所有的细节,不然我们会迷失在复杂的源码海洋中。我们先理清整理逻辑即可。

这里我们再总结下我们分析到的流程

  1. clientbroker发送消息拉取请求

  2. broker如果没有消息则会将请求挂起

  3. 挂起的请求有几个处理机会

    a. 默认会有PullRequestHoldService定时5s去轮训处理这些暂存的请求

    b. ReputMessageService会每隔1毫秒去检查是否有消息到来,如果有则分发新的消息到来,其中就包括处理hold住的请求

相关推荐
艾伦~耶格尔1 小时前
Spring Boot 三层架构开发模式入门
java·spring boot·后端·架构·三层架构
man20172 小时前
基于spring boot的篮球论坛系统
java·spring boot·后端
攸攸太上2 小时前
Spring Gateway学习
java·后端·学习·spring·微服务·gateway
罗曼蒂克在消亡3 小时前
graphql--快速了解graphql特点
后端·graphql
潘多编程3 小时前
Spring Boot与GraphQL:现代化API设计
spring boot·后端·graphql
大神薯条老师3 小时前
Python从入门到高手4.3节-掌握跳转控制语句
后端·爬虫·python·深度学习·机器学习·数据分析
2401_857622664 小时前
Spring Boot新闻推荐系统:性能优化策略
java·spring boot·后端
知否技术4 小时前
为什么nodejs成为后端开发者的新宠?
前端·后端·node.js
AskHarries5 小时前
如何优雅的处理NPE问题?
java·spring boot·后端
计算机学姐5 小时前
基于SpringBoot+Vue的高校运动会管理系统
java·vue.js·spring boot·后端·mysql·intellij-idea·mybatis