RocketMQ(四):消费前如何拉取消息?(长轮询机制)
上篇文章从Broker接收消息开始,到消息持久化到各种文件结束,分析完消息在Broker持久化的流程与原理
消费者消费消息前需要先从Broker进行获取消息,然后再进行消费
为了流程的完整性,本篇文章就先来分析下消费者是如何获取消息的,文章内容导图如下:
获取消息的方式
消费者并不是每次要消费一条数据就向Broker获取一条数据的,这样RPC的开销太大了,因此先从Broker获取一批数据到内存中,再进行消费
消费端获取消息通常有三种方式:推送消息、拉取消息、长轮询(推拉结合)
推送消息:消息持久化到Broker后,Broker监听到有新消息,主动将消息推送到对应的消费者
Broker主动推送消息具有很好的实时性,但如果消费端没有流控,推送大量消息时会增加消费端压力,导致消息堆积、吞吐量、性能下降
拉取消息:消费端可以根据自身的能力主动向Broker拉取适量的消息,但不好预估拉取消息的频率,拉取太慢会导致实时性差,拉取太快可能导致压力大、消息堆积
长轮询:在拉取消息的基础上进行改进,如果在broker没拉取到消息,则会等待一段时间,直到消息到达或超时再触发拉取消息
长轮询相当于在拉取消息的同时,通过监听消息到达,增加推送的优点,将拉取、推送的优点结合,但长连接会更占资源,大量长连接会导致开销大
RocketMQ中常用的消费者DefaultMQPushConsumer
,虽然从名字看是“推送”的方式,但获取消息用的是长轮询的方式
这种特殊的拉取消息方式能到达实时推送的效果,并在消费者端做好流控(拉取消息达到阈值就延时拉取)以防压力过大
拉取消息原理
DefaultMQPushConsumer
的内部实现DefaultMQPushConsumerImpl
有一个MQ客户端实例MQClientInstance
它内部包含的PullMessageService
组件,就是用于长轮询拉取消息的
PullMessageService会使用DefaultMQPushConsumerImpl与Broker建立长连接并拉取消息,拉取的消息存放在本地内存队列(processQueue)中,方便后续给消费者消费
其中涉及一些组件,先简单介绍,方便后续描述:
-
ProcessQueue:从Broker拉取的消息存放在这个内存队列中
- 底层使用有序的TreeMap来进行存储的,其中Key为偏移量、Value为存储的消息
-
PullRequest:拉取请求,拉取消息(以队列为)基本单位
-
PullMessageService轮询时,每次取出PullRequest再进行后续流程
-
存储消费者组、对应的MessageQueue(broker上的队列)、ProcessQueue(消费者内存队列)、拉取的偏移量等信息
-
PullRequest(拉消息)、MessageQueue、ProcessQueue(存消息)一一对应
-
-
PullRequestQueue:PullRequest的队列
- 由于消费者可能同时消费多条队列,每次拉取的基本单位又是以同个队列进行拉取,因此PullMessageService需要轮询取出PullRequest进行后续拉取流程
- 拉取消息失败或下次拉取消息都会把PullRequest重新投入队列中,由后续PullMessageService轮询取出再进行拉取消息
简化的流程为:
- 从队列取出PullRequest,然后封装请求向Broker异步发送
- 响应后通过回调将查到的消息放入其内存队列中,方便后续消费
- 在此期间最终都会将PullRequest放回队列(失败可能延时放回),便于下次拉取该队列的消息
发送拉取消息请求
PullMessageService启动时也会使用线程进行轮询,会从pullRequestQueue取出PullRequest进行后续的拉取消息
public void run() {
//...
while (!this.isStopped()) {
//取出PullRequest 没有则阻塞
PullRequest pullRequest = this.pullRequestQueue.take();
//拉取消息
this.pullMessage(pullRequest);
}
}
pullMessage 拉取消息前准备参数
pullMessage
最终会调用DefaultMQPushConsumerImpl.pullMessage
,代码虽然很多,但主要流程为校验、获取参数、调用核心方法
- 进行参数、状态、流控的校验,如果失败会调用
executePullRequestLater
后续延时50ms将拉取请求重新放回队列中,也就是后续再进行该队列的消息拉取 - 如果是第一次执行,要获取消费进度的偏移量
computePullFromWhereWithException
,后续使用PullRequest上的nextOffset(集群模式向Broker获取) - 获取消费端相关信息(后续会封装成请求),创建回调,回调在RPC后调用
- 执行拉取消息的核心方法
pullKernelImpl
public void pullMessage(final PullRequest pullRequest) {
//获取内存队列
final ProcessQueue processQueue = pullRequest.getProcessQueue();
//内存队列设置最新的拉取时间
pullRequest.getProcessQueue().setLastPullTimestamp(System.currentTimeMillis());
//参数、状态校验、流控..
//内存队列中的消息数量
long cachedMessageCount = processQueue.getMsgCount().get();
//内存队列中消息大小 MB
long cachedMessageSizeInMiB = processQueue.getMsgSize().get() / (1024 * 1024);
//如果数量太多 默认1000 说明当前已经消息堆积 需要进行流控 后续定时将拉取请求再放入队列中 后续再来拉取消息
if (cachedMessageCount > this.defaultMQPushConsumer.getPullThresholdForQueue()) {
this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL);
return;
}
//消息太大 类似 同理 默认100MB
if (cachedMessageSizeInMiB > this.defaultMQPushConsumer.getPullThresholdSizeForQueue()) {
this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL);
return;
}
//...
//如果是第一次要去获取拉取消息的偏移量
offset = this.rebalanceImpl.computePullFromWhereWithException(pullRequest.getMessageQueue());
//获取当前Topic的订阅数据
final SubscriptionData subscriptionData = this.rebalanceImpl.getSubscriptionInner().get(pullRequest.getMessageQueue().getTopic());
final long beginTimestamp = System.currentTimeMillis();
//创建回调 这里的回调是从broker拉取消息后执行的回调 后面再分析,这里先省略代码
PullCallback pullCallback = new PullCallback();
//...
try {
//拉取消息核心实现
this.pullAPIWrapper.pullKernelImpl(
pullRequest.getMessageQueue(),
subExpression,
subscriptionData.getExpressionType(),
subscriptionData.getSubVersion(),
pullRequest.getNextOffset(),
this.defaultMQPushConsumer.getPullBatchSize(),
sysFlag,
commitOffsetValue,
BROKER_SUSPEND_MAX_TIME_MILLIS,
CONSUMER_TIMEOUT_MILLIS_WHEN_SUSPEND,
CommunicationMode.ASYNC,
pullCallback
);
} catch (Exception e) {
log.error("pullKernelImpl exception", e);
this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
}
}
computePullFromWhereWithException 获取拉取消息的偏移量
computePullFromWhereWithException
方法由再平衡组件RebalancePushImpl
调用
(再平衡是消费者间重新分配队列的机制,增加/减少队列、消费者都会触发再平衡机制,平均分配给消费者队列,PullRequest也是它分配的,细节后文再说)
这里的拉取消息偏移量又可以叫上一次消费的偏移量,因为拉取消息从上次消费的偏移量开始拉取
当消费者首次拉取消息时,需要查询拉取偏移量(即上一次消费的偏移量),广播模式下这个偏移量在消费者端记录,就可以从内存中获取
而集群模式下,偏移量在broker记录,需要从broker获取,最终调用fetchConsumeOffsetFromBroker
获取
fetchConsumeOffsetFromBroker
也是先去获取Broker信息,本地没有就从NameServer获取
然后通过客户端API的queryConsumerOffset
发送获取消费偏移量的请求
pullKernelImpl 拉取消息核心
在拉取消息核心方法中会去获取Broker等信息、然后封装请求,再通过Netty调用
public PullResult pullKernelImpl(
final MessageQueue mq,
final String subExpression,
final String expressionType,
final long subVersion,
final long offset,
final int maxNums,
final int sysFlag,
final long commitOffset,
final long brokerSuspendMaxTimeMillis,
final long timeoutMillis,
final CommunicationMode communicationMode,
final PullCallback pullCallback
) throws MQClie