这里是weihubeats ,觉得文章不错可以关注公众号小奏技术,文章首发。拒绝营销号,拒绝标题党
RocketMQ 版本
- 5.1.0
长轮训
一般常用的客户端和服务的交互方式有如下两种方式
-
短轮训
-
长轮训
-
短轮训:
client
不断向服务端发送请求,服务端不管有没有数据,都返回给client
。- 优点: 实现简单
- 缺点:
client
会频繁请求server
,影响服务端性能
-
长轮训:
client
也是不断向服务器发送请求,不同的是如果服务端没有数据,则会主动将请求hold
住,不返回给client
,这样client
就不会因为得到响应数据马上又向server
发送请求- 优点:不对频繁对
server
轮训,减少server
压力 - 缺点:实现相对短轮训更复杂
- 优点:不对频繁对
RocketMQ中消费者向broker
请求消息的方式就使用的长轮训。
我们来结合源码具体分析分析
源码分析
client 消息拉取
由于我们的重点是分析长轮训,所以消息拉取我们不会从最初的入口开始分析。
我们直接看看消息拉取的请求发起代码
这里的消息拉取主要有两个请求码
- LITE_PULL_MESSAGE: 提供了
Subscribe
和Assign
的使用方式,使用起来更方便。可以理解为push方式,本质还是pull - PULL_MESSAGE : 原始的消息拉取方式,需要自己手动更新消费位点
虽然这里请求码是两个,实际broker
的处理器都是同一个
broker
这里我们可以看到LITE_PULL_MESSAGE
和PULL_MESSAGE
请求对应的处理器类都是PullMessageProcessor
所以我们来看看PullMessageProcessor
的
java
org.apache.rocketmq.broker.processor.PullMessageProcessor#processRequest(io.netty.channel.Channel, org.apache.rocketmq.remoting.protocol.RemotingCommand, boolean)
方法
可以看到这个类的逻辑非常多。但是大部分逻辑都和我们今天要分析的主线无关。我们都会跳过,只挑重点分析。
其实前面的代码可以到都是一些
topic
、broker
的权限校验
消息拉取的核心方法在这里
这里消息拉取的处理逻辑是通过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
消息没拉取到这个状态码的处理逻辑
这里可以看到两行关键代码
- 返回值为null
- 调用了
PullRequestHoldService
的suspendPullRequest
方法
我们来重点分析一下
返回值为null意味着什么?
意味着请求被挂起。
我们可以看看client
和server
通信的那块代码
可以看到如果返回值response
为null
。则什么也不做,意味着请求被挂起,客户端不会收到任何响应
既然请求被挂了,那么就要接着分析了。请求是如何恢复的
这里我们回到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());
}
- 设置了一个开关,然后就是无线死循环轮训
- 查看
brokerConfig
长轮训开关longPollingEnable
是否开启,默认开启 - 长轮训则休眠等待5秒,未开启长轮训则使用
broker
配置shortPollingTimeMills
段轮训,默认休眠1s - 休眠完成后调用
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>)
重新处理请求
这里的处理流程如下
- 如果queue的消息偏移量(maxOffset)大于
client
拉取的消息偏移量(pullFromThisOffset),则调用executeRequestWhenWakeup
重新执行消息拉取请求,然后返回给client
- 如果当前时间>= 请求hold时间+请求超时时间,也会重新处理请求
3. 如果两者都不满足,则将请求放回ManyPullRequest
中
java
if (!replayList.isEmpty()) {
mpr.addPullRequest(replayList);
}
可以看到这里是5s轮训一次去处理被hold
住的消息,那么显然5s的时间间隔是不够的,这样很容易造成消息消费延时
那么该如何处理呢?
我们再看看NotifyMessageArrivingListener
的arriving
方法在哪里被调用
有三个地方调用。我们看看ReputMessageService
类
这里也是启动一个线程去轮训,不同的是休眠时间只有1毫秒
看看doReput
做了啥
可以看到主要是去拉取消息,通过比较reputFromOffset
和commitlog
的MaxOffset
对比看是否有新消息。
如果有则执行消息分发方法。即
java
DefaultMessageStore.this.messageArrivingListener.arriving()
这里就调用了我们之前长轮训处理消息的方法NotifyMessageArrivingListener.arriving
总结
总的来说消息拉取这一块的源码还是非常复杂的,我们只能走马观花查看整体流程。
没办法一一俱到分析所有的细节,不然我们会迷失在复杂的源码海洋中。我们先理清整理逻辑即可。
这里我们再总结下我们分析到的流程
-
client
向broker
发送消息拉取请求 -
broker
如果没有消息则会将请求挂起 -
挂起的请求有几个处理机会
a. 默认会有
PullRequestHoldService
定时5s去轮训处理这些暂存的请求b.
ReputMessageService
会每隔1毫秒去检查是否有消息到来,如果有则分发新的消息到来,其中就包括处理hold住的请求