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住的请求

相关推荐
齐 飞18 分钟前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb
LunarCod34 分钟前
WorkFlow源码剖析——Communicator之TCPServer(中)
后端·workflow·c/c++·网络框架·源码剖析·高性能高并发
码农派大星。1 小时前
Spring Boot 配置文件
java·spring boot·后端
杜杜的man2 小时前
【go从零单排】go中的结构体struct和method
开发语言·后端·golang
幼儿园老大*2 小时前
走进 Go 语言基础语法
开发语言·后端·学习·golang·go
llllinuuu2 小时前
Go语言结构体、方法与接口
开发语言·后端·golang
cookies_s_s2 小时前
Golang--协程和管道
开发语言·后端·golang
为什么这亚子2 小时前
九、Go语言快速入门之map
运维·开发语言·后端·算法·云原生·golang·云计算
想进大厂的小王2 小时前
项目架构介绍以及Spring cloud、redis、mq 等组件的基本认识
redis·分布式·后端·spring cloud·微服务·架构
customer083 小时前
【开源免费】基于SpringBoot+Vue.JS医院管理系统(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·开源·intellij-idea