本文章基于开源rocketmq4.9.3版本进行分析,如无特殊说明,文中以集群、顺序消费为示例备注源码;如果错误请留言斧正~
rocketmq开源注释源码地址:点击跳转
1 前言
在过去一年的生产运维中,由于团队负责的业务具有高复杂度和大数据量特性,经过多次技术论证与架构设计,我们在系统响应时效与数据一致性之间采取了折中方案——基于MySQL Binlog转换实现最终一致性。这一方案衍生出大量的消息队列生产消费场景,并催生了一个专职消费MQ消息的服务。在服务运行过程中,我们发现了若干典型问题,现通过重新研读RocketMQ源码探究其根源。以下是核心问题梳理:
-
问题一:某个
MessageQueue
一直不消费,但其他MessageQueue
却能正常消费。这是为什么呢? -
问题二:线程数过多是否会导致线程饿死现象?
-
问题三:每次启动消息拉取线程是按照
MessageQueue
还是按照ConsumerGroup
进行的? -
问题四:单个
ConsumerGroup
消费时,需要启动多少个线程?-
定时拉取 NameServer 线程、定时更新 Topic 线程、定时持久化位点线程、定时发送心跳(清理下线 Broker)、定时 Rebalance 线程、Netty 线程(复杂发送和响应处理)、定时重新负载、消费者线程池(最多启动 20 个线程)等,至少启动 7 个线程 + 当前客户端分配的
MessageQueue
数量。是否有遗漏启动的线程呢?
-
-
问题五:
MessageQueue
与 Topic 信息分别存储在哪里?是在 Broker 端吗? -
问题六:Consumer 心跳同步首次发送是在什么时机?多久同步一次?
-
消费者上线时,默认触发一次 Rebalance,如果当前客户端分配的
MessageQueue
发生了变更,会默认触发一次心跳同步。
-
-
问题七:Rebalance 的执行过程是怎样的?
-
问题八:消息拉取过程,第一次消息拉取是在什么时机触发的?消费者上线时是否会触发消息拉取动作?
-
问题九:以顺序消费的重试机制为例,顺序消费的消息挂起实现过程是怎样的?在重试过程中,如何保证
MessageQueue
中的消息顺序消费? -
问题十:顺序消费时最大重试次数是多少?
-
顺序消费默认无限重试,可以在启动消费者时指定最大重试次数:
DefaultMQPushConsumer#maxReconsumeTimes
。
-
-
问题十一:顺序消费写入死信队列的逻辑是怎样的?
-
问题十二:Broker 端根据消费重新消费次数超过最大重试次数后,替换 Topic 信息为死信队列。这个过程是如何实现的?
2 消费者注册与启动
2.1 启动示例
// 创建消费者
DefaultMQPushConsumer pushConsumer = new DefaultMQPushConsumer("CID_IM_USER_MESSAGE");
pushConsumer.setNamesrvAddr("127.0.0.1:9876");
// 设置订阅topic
pushConsumer.subscribe("IM_USER_MESSAGE", "*");
// 设置最大消费线程数(默认20)
pushConsumer.setConsumeThreadMax(10);
// 设置最大重试次数(顺序消费默认Integer.MAX_VALUE,并发消费默认16)
pushConsumer.setMaxReconsumeTimes(2);
// 每次拉取消息的数量,默认32,最大1024
pushConsumer.setPullBatchSize(1000);
// 每次批量消费消息的数量,默认1,最大1024
pushConsumer.setConsumeMessageBatchMaxSize(1024);
// 连续两次消息拉取间隔,默认不间隔(消费完立刻拉取下一批消息)
pushConsumer.setPullInterval(60*3*1000);
// 消费点位保存的间隔,默认5秒
pushConsumer.setPersistConsumerOffsetInterval(1000*60);
// 消息消费的实际处理逻辑,由业务方自行实现,有序消费和并发消费两种选择
pushConsumer.registerMessageListener(new MessageListenerOrderly() {
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
return ConsumeOrderlyStatus.SUCCESS;
}
});
pushConsumer.start();
2.1 创建消费者实例
DefaultMQPushConsumer pushConsumer = new DefaultMQPushConsumer("CID_IM_USER_MESSAGE");
- 创建消息消费的门面类:new DefaultMQPushConsumer()
- 设置默认负载分配策略,默认平均分配new AllocateMessageQueueAveragely()
- 创建用于实际执行消费拉取的类:new DefaultMQPushConsumerImpl(this, rpcHook)
DefaultMQPushConsumerImpl包含主要的属性如下:
- 创建时初始化的属性:
- DefaultMQPushConsumer defaultMQPushConsumer;临时暂存各种配置信息
- RebalanceImpl rebalanceImpl = new RebalancePushImpl(this);处理重新负载的类
- long consumerStartTimestamp = System.currentTimeMillis();消费起始点位
- 实际启动时初始化的属性:
- MQClientInstance mQClientFactory;
-
PullAPIWrapper pullAPIWrapper;启动netty线程,用于client与broker直接的数据交换
-
MessageListener messageListenerInner;业务方实现的消费者处理handler
-
OffsetStore offsetStore;点位保存实现类
-
ConsumeMessageService consumeMessageService;消息拉取成功后实际执行消息消费处理的类
2.3 设置消费基本参数信息
2.3.1 设置最大消费线程数
pushConsumer.setConsumeThreadMax(10);
消息按topic.messageQueue从broker拉取到后,启动线程池消费消息,每个messageQueue对应一个线程池处理消息,每个consumerGroup默认最多启动20线程;
2.3.2 设置消息消费的最大重试次数
pushConsumer.setMaxReconsumeTimes(2);
顺序消费默认Integer.MAX_VALUE,即无限重试;并发消费默认16;
2.3.3 每次从broker拉取消息的数量
pushConsumer.setPullBatchSize(1000);
消费者每次从broker拉取消息的数量,默认32,最大1024
2.3.4 每次消费消息的数量
pushConsumer.setConsumeMessageBatchMaxSize(1024);
从broker批量拉取到消息后,异步批量消费消息的数量,默认1,最大1024;
2.3.5 消息消费节流间隔时长
pushConsumer.setPullInterval(60*3*1000);
从broker连续拉取消息的间隔时长,默认0,即消费完当前拉取批次消息后立即拉取下一批消息;
2.3.7 消费点位保存频率
pushConsumer.setPersistConsumerOffsetInterval(1000*60);
消费点位保存频率,默认5秒保存一次;
2.3.8 注册消费业务处理Listener
pushConsumer.registerMessageListener(new MessageListenerOrderly() {
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
return ConsumeOrderlyStatus.SUCCESS;
}
});
消息消费的实际处理逻辑,由业务方自行实现,有序消费和并发消费两种选择
其他参数不在列举;
2.4 启动消费
2.4.1 消费线程模型图
以下消费模型按顺序消费逻辑梳理,其中有3个broker,每个broker有4个MessageQueue,4个消费者实例,使用默认的负载策略(平均分配),则最终messageQueue消费分配结果为:3 3 3 3,即每个消费者消费3个messageQueue;
启动消费者示例 DefaultMQPushConsumerImpl#start,主要以创建基础实例为主,其中包含配置参数检查、创建nettyClient实例、创建消费拉取实例、创建重新负载实例、注册消息过滤钩子、创建消费点位保存实例等;
2.4.2 检查配置参数
检查常见参数是否超过指定阈值、消费者参数是否完整等
this.checkConfig();
2.4.3 创建MQClientInstance
this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQPushConsumer, this.rpcHook);
2.4.3.1 创建nettyClient
创建MQClientAPIImpl,内部初始了nettyclient,用于与broker、nameServer进行消息同步,包含:获取brokerNameAddr、获取/更新topic信息、消息拉取、心跳同步等关于数据交换的全部逻辑;
// KR1:创建MQClientAPIImpl,内部初始了nettyclient,用于与broker进行消息同步,负责与broker进行消息交互,包含:获取brokerNameAddr、获取/更新topic信息
this.mQClientAPIImpl = new MQClientAPIImpl(this.nettyClientConfig, this.clientRemotingProcessor, rpcHook, clientConfig);
2.4.3.2 创建消息拉取实例
创建从broker端拉取消息的线程实例,使用生产消费者模式实现消息拉取,主要包含:
-
消息拉取信号队列:LinkedBlockingQueue<PullRequest> pullRequestQueue
-
消费实例持有对象:MQClientInstance mQClientFactory
-
异步定时线程池(单线程):ScheduledExecutorService scheduledExecutorService,线程名称:PullMessageServiceScheduledThread,主要负责如下任务
-
延迟异步发送拉取消息信号
-
执行延迟异步任务
-
// KR1:创建消息拉取线程,用于消息拉取
this.pullMessageService = new PullMessageService(this);
2.4.3.3 创建重新负载实例
创建重新负载线程实例,根据定时调度,按consumerGroup执行重新负载逻辑,主要包含:
-
消费实例持有对象:MQClientInstance mQClientFactory,主要调用void doRebalance(),具体重新负载的实现逻辑参考下面章节;
// KR1:创建重新负载线程,用于重新负载
this.rebalanceService = new RebalanceService(this);
2.4.4 创建并注册消息过滤hook
this.pullAPIWrapper = new PullAPIWrapper(mQClientFactory,this.defaultMQPushConsumer.getConsumerGroup(), isUnitMode());
// KR2:注册消息过滤hook
this.pullAPIWrapper.registerFilterMessageHook(filterMessageHookList);
2.4.5 创建消费点位保存实例
switch (this.defaultMQPushConsumer.getMessageModel()) {
case BROADCASTING:
this.offsetStore = new LocalFileOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
break;
// KR1:集群消费的点位保存类
case CLUSTERING:
this.offsetStore = new RemoteBrokerOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
break;
default:
break;
}
this.defaultMQPushConsumer.setOffsetStore(this.offsetStore)
this.offsetStore.load();
2.4.6 创建并启动消费实例
if (this.getMessageListenerInner() instanceof MessageListenerOrderly) {
this.consumeOrderly = true;
this.consumeMessageService =
new ConsumeMessageOrderlyService(this, (MessageListenerOrderly) this.getMessageListenerInner());
} else if (this.getMessageListenerInner() instanceof MessageListenerConcurrently) {
this.consumeOrderly = false;
this.consumeMessageService =
new ConsumeMessageConcurrentlyService(this, (MessageListenerConcurrently) this.getMessageListenerInner());
}
// KR3:不同的消费类型,启动周期性任务
this.consumeMessageService.start();
-
顺序消费ConsumeMessageOrderlyService
-
ConsumeMessageOrderlyService.this.lockMQPeriodically();
-
RebalanceImpl#lockAll
-
查询本地保存的【消息队列】与【队列处理process】关系缓存【ConcurrentMap<MessageQueue, ProcessQueue> processQueueTable】,构建brokerName与当前消费的messageQueues关系
-
按broker逐个上报【当前client+当前consuemrGroup】锁定【当前messageQueues】
-
锁定成功后,更新本地缓存的队列处理process的锁定状态与锁定时间戳
-
远程锁定结果更新成功后,本地重新更新锁定结果,保证本地缓存与broker端锁定结果一致性;
-
-
-
并发消费ConsumeMessageOrderlyService
-
定时周期性在本地锁定当前client需要消费的messageQueue;
-
2.4.7 缓存消费分组与消费实例
boolean registerOK = mQClientFactory.registerConsumer(this.defaultMQPushConsumer.getConsumerGroup(), this);
ConcurrentMap<String/* group */, MQConsumerInner> consumerTable
2.4.8 启动一组线程
mQClientFactory.start();
启动一组线程,用于定时执行消费拉取、点位保存、重新负载、心跳同步等逻辑;
2.4.8.1 启动数据交换层线程
this.mQClientAPIImpl.start();
NettyRemotingClient#start
-
启动netty作为数据交换层:与broker端进行数据交互的操作,全部通过当前NettyRemotingClient异步执行;
-
启动周期性线程:
-
延迟3秒,每隔1秒,异步从response中获取channel响应,并分发给对应的线程处理;
-
如果响应时间超时,则放弃当前异步请求响应channel;
-
this.timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
try {
NettyRemotingClient.this.scanResponseTable();
} catch (Throwable e) {
log.error("scanResponseTable exception", e);
}
}
}, 1000 * 3, 1000);
2.4.8.2 启动一组线程执行周期性任务
this.startScheduledTask();
-
延迟10秒,间隔2秒更新本地缓存的nameServer列表
MQClientInstance.this.mQClientAPIImpl.fetchNameServerAddr();
-
延迟10秒,间隔30秒,定时从NameServer拉取Topic信息
MQClientInstance.this.updateTopicRouteInfoFromNameServer();
-
延迟1秒,间隔30秒,定时清理下线的broker并发送心跳信息
// KR3:删除离线的broker信息
MQClientInstance.this.cleanOfflineBroker();
-
发送心跳信息
// KR3:向远程broker发送心跳
MQClientInstance.this.sendHeartbeatToAllBrokerWithLock();
消费者心跳信息包含:当前ClientId、ConsumerGroup、ConsumerType、MessageModel、ConsumerFromWhere、subscriptions
this.sendHeartbeatToAllBroker();
心跳信息==>
HeartbeatData heartbeatData = new HeartbeatData();
// clientID
heartbeatData.setClientID(this.clientId);
// Consumer
for (Map.Entry<String, MQConsumerInner> entry : this.consumerTable.entrySet()) {
MQConsumerInner impl = entry.getValue();
if (impl != null) {
ConsumerData consumerData = new ConsumerData();
consumerData.setGroupName(impl.groupName());
consumerData.setConsumeType(impl.consumeType());
consumerData.setMessageModel(impl.messageModel());
consumerData.setConsumeFromWhere(impl.consumeFromWhere());
consumerData.getSubscriptionDataSet().addAll(impl.subscriptions());
consumerData.setUnitMode(impl.isUnitMode());
heartbeatData.getConsumerDataSet().add(consumerData);
}
}
2.4.9 启动消息拉取实例线程
2.4.9.1 启动消息拉取实例线程
-
启动消息拉取线程 this.pullMessageService.start();
-
线程内逻辑:同步阻塞的从本地缓存队列(PullMessageService#pullRequestQueue)中获取请求拉取信号,执行消息拉取与消费动作;
// KR2:生产者消费者模型,如果有生产者产出消息,则执行消息拉取消费,否则阻塞等待【触发rebalance时发起第一次消息拉取】
PullRequest pullRequest = this.pullRequestQueue.take();
this.pullMessage(pullRequest);
顺序消费:本地缓存的client与messageQueue关系中,当前client锁定了当前messageQueue的processQueue才能执行消费,即if (processQueue.isLocked())
消息拉取与消费过程大致如下:
-
查询本地缓存的消费点位
this.rebalanceImpl.computePullFromWhereWithException(pullRequest.getMessageQueue())
-
定义消息拉取成功后pullCallback
-
注册pullCallback,发起消息拉取动作
-
执行pullCallback
DefaultMQPushConsumerImpl#pullMessage
=>
// KR2:消息拉取成功后,处理的回调逻辑
PullCallback pullCallback = new PullCallback() {}
// KR1:触发(异步)消息拉取动作
this.pullAPIWrapper.pullKernelImpl();
2.4.9.2 执行pullCallback
- 消息填充至processQueue
- 顺序消费启动消费线程池,提交异步消费任务
- ConsumeMessageOrderlyService#consumeExecutor线程池名称前缀:ConsumeMessageThread_
- 默认情况下,当前线程池默认最多创建20个核心线程(仅在提交或创建消费任务时,根据是否存在空闲线程决定是否创建新线程
-
DefaultMQPushConsumer#consumeThreadMin默认20
-
DefaultMQPushConsumer#consumeThreadMax默认20
-
- 是否设置了消息拉取间隔参数
DefaultMQPushConsumerImpl.this.executePullRequestLater(pullRequest, DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval())
- 发送消息拉取信号PullMessageService#pullRequestQueue
2.4.9.4 执行消息消费
ConsumeMessageOrderlyService.ConsumeRequest#run
-
messageQueue加锁
-
从processQueue中获取一批(DefaultMQPushConsumer#consumeMessageBatchMaxSize)消息,
List<MessageExt> msgs = this.processQueue.takeMessages(consumeBatchSize);
-
执行消费前hook方法ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.executeHookBefore(consumeMessageContext);
-
执行业务方实现的consumeMessageListener
status = messageListener.consumeMessage(Collections.unmodifiableList(msgs), context); -
执行消费后的hook方法,无论本次本次消费是否成功,默认都会执行一次hookafter方法(需要业务方自行判断消费结果,决定是否可以执行hook方法)ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.executeHookAfter(consumeMessageContext);
-
本批次消息消费成功后,更新本地缓存的消费点位兵器返回是否继续拉取下一批消息标识continueConsume = ConsumeMessageOrderlyService.this.processConsumeResult(msgs, status, context, this);
消息消费流程图如下:
消费点位更新流程图如下:
2.4.10 启动rebalance实例线程
2.4.10.1 启动重新负载实例线程
this.rebalanceService.start();
log.info(this.getServiceName() + " service started");
while (!this.isStopped()) {
// 间隔20秒执行rebalance
this.waitForRunning(waitInterval);
this.mqClientFactory.doRebalance();
}
log.info(this.getServiceName() + " service end");
2.4.10.2 执行重新负载逻辑
执行重新负载方法:MQClientInstance#doRebalance
-
从本地缓存的consuemrGroup与MQConsumerInner关系(MQClientInstance#consumerTable)遍历consumerGroup
-
遍历当前consumerGroup下的全部topic(RebalanceImpl#subscriptionInner)执行重新负载 impl.doRebalance();=》RebalanceImpl#doRebalance
-
按topic执行重新负载RebalanceImpl#rebalanceByTopic
-
在本地缓(RebalanceImpl#topicSubscribeInfoTable)中查询当前topic下全部的messageQueue
-
从远程broker查询当前consumerGroup最新的clientId
List<String> cidAll = this.mQClientFactory.findConsumerIdList(topic, consumerGroup);
-
按重新负载(默认平均分配)策略计算得出分配结果allocateResult = strategy.allocate(this.consumerGroup, this.mQClientFactory.getClientId(), mqAll, cidAll);
-
根据本次计算得出的负载结果,判断当前消费者消费的messageQueue是否发生了变更
boolean changed = this.updateProcessQueueTableInRebalance(topic, allocateResultSet, isOrder);
-
更新远端当前消费者client在当前topic下最新的负载情况this.messageQueueChanged(topic, mqSet, allocateResultSet);触发心跳同步this.getmQClientFactory().sendHeartbeatToAllBrokerWithLock();
整体流程图如下:
2.4.11 更新本地缓存topic与messageQueue关系
this.updateTopicSubscribeInfoWhenSubscriptionChanged();
2.4.12 校验client在broker上状态是否可用
this.mQClientFactory.checkClientInBroker();
2.4.13 发送心跳
this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();
2.4.14 立即重新负载
this.mQClientFactory.rebalanceImmediately();
3 消费者流程大图
高清图片:https://www.yuque.com/chiguo/sg50s7/sbagvgkb0qu024ic?#《rocketmq消费端逻辑大图》