关于 RocketMQ ClientID 相同引发的消息堆积的问题
首先,造成这个问题的 BUG RocketMQ 官方已经在 3月16号 的这个提交中修复了,这里只是探讨一下在修复之前造成问题的具体细节,更多的上下文可以参考我之前写的 《RocketMQ Consumer 启动时都干了些啥?》 ,这篇文章讲解了 RocketMQ 的 Consumer 启动之后都做了哪些操作,对理解本次要讲解的 BUG 有一定的帮助。
其中讲到了:

重复消费自不必说,你 ClientID 都相同了。本篇着重聊聊为什么会消息堆积。
文章中讲到,初始化 Consumer 时,会初始化 Rebalance 的策略。你可以大致将 Rebalance 策略理解为如何将一个 Topic 下的 m 个 MessageQueue 分配给一个 ConsumerGroup 下的 n 个 Consumer 实例的策略,看着有些绕,其实就长这样:

而从 Consumer 初始化的源码中可以看出,默认情况下 Consumer 采取的 Rebalance 策略是 AllocateMessageQueueAverage()。

默认的策略很好理解,将 MessageQueue 平均的分配给 Consumer。举个例子,假设有 8 个 MessageQueue,2 个 Consumer,那么每个 Consumer 就会被分配到 4 个 MessageQueue。
那如果分配不均匀怎么办?例如只有 7 个 MessageQueue,但是 Consumer 仍然是 2 个。此时 RocketMQ 会将多出来的部分,对已经排好序的 Consumer 再做平均分配,一个一个分发给 Consumer,直到分发完。例如刚刚说的 7 个 MessageQueue 和 2 个 ConsumerGroup 这种 case,排在第一个的 Consumer 就会被分配到 4 个 MessageQueue,而第二个会被分配到 3 个 MessageQueue。
大家可以先理解一下 AllocateMessageQueueAveragely 的实现,作为默认的 Rebalance 的策略,其实现位于这里:

接下来我们看看,AllocateMessageQueueAveragely 内部具体都做了哪些事情。
其核心其实就是实现的 AllocateMessageQueueStrategy 接口中的 allocate 方法。实际上,RocketMQ 对该接口总共有 5 种实现:
AllocateMachineRoomNearby AllocateMessageQueueAveragely AllocateMessageQueueAveragelyByCircle AllocateMessageQueueByConfig AllocateMessageQueueByMachineRoom AllocateMessageQueueConsistentHash
其默认的 AllocateMessageQueueAveragely 只是其中的一种实现而已,那执行 allocate 它需要什么参数呢?

需要以下四个:
ConsumerGroup 消费者组的名字 currentCID 当前消费者的 clientID mqAll 当前 ConsumerGroup 所消费的 Topic 下的所有的 MessageQueue cidAll 当前 ConsumerGroup 下所有消费者的 ClientID
实际上是将某个 Topic 下的所有 MessageQueue 分配给属于同一个消费者的所有消费者实例,粒度是 By Topic 的。
所以到这里剩下的事情就很简单了,无非就是怎么样把这一堆 MessageQueue 分配给这一堆 Consumer。这个怎么样,就对应了 AllocateMessageQueueStrategy 的不同实现。
接下来我们就来看看 AllocateMessageQueueAveragely 是如何对 MessageQueue 进行分配的,之前讲源码我一般都会一步一步的来,结合源码跟图,但是这个源码太短了,我就直接先给出来吧。
public List<MessageQueue> allocate(String consumerGroup, String currentCID, List<MessageQueue> mqAll, List<String> cidAll) {
if (currentCID == null || currentCID.length() < 1) {
throw new IllegalArgumentException("currentCID is empty");
}
if (mqAll == null || mqAll.isEmpty()) {
throw new IllegalArgumentException("mqAll is null or mqAll empty");
}
if (cidAll == null || cidAll.isEmpty()) {
throw new IllegalArgumentException("cidAll is null or cidAll empty");
}
List<MessageQueue> result = new ArrayList<MessageQueue>();
// 判断一下当前的客户端是否在 cidAll 的集合当中
if (!cidAll.contains(currentCID)) {
log.info("[BUG] ConsumerGroup: {} The consumerId: {} not in cidAll: {}",
consumerGroup,
currentCID,
cidAll);
return result;
}
// 拿到当前消费者在所有的消费者实例数组中的位置
int index = cidAll.indexOf(currentCID);
// 用 messageQueue 的数量 对 消费者实例的数量取余数, 这个实际上就把不够均匀分的 MessageQueue 的数量算出来了
// 举个例子, 12 个 MessageQueue, 有 5 个 Consumer, 12 % 5 = 2
int mod = mqAll.size() % cidAll.size();
int averageSize =
mqAll.size() <= cidAll.size() ? 1 : (mod > 0 && index < mod ? mqAll.size() / cidAll.size() + 1 : mqAll.size() / cidAll.size());
int startIndex = (mod > 0 && index < mod) ? index * averageSize : index * averageSize + mod;
int range = Math.min(averageSize, mqAll.size() - startIndex);
for (int i = 0; i < range; i++) {
result.add(mqAll.get((startIndex + i) % mqAll.size()));
}
return result;
}
其实前半部分都是些常规的 check,可以忽略不看,从这里:
int index = cidAll.indexOf(currentCID);
开始,才是核心逻辑。为了避免逻辑混乱,还是假设有 12 个 MessageQueue,5 个 Consumer,同时假设 index=0 。
那么 mod 的值就为 12 % 5 = 2 了。
而 averageSize 的值,稍微有点绕。如果 MessageQueue 的数量比消费者的数量还少,那么就为 1 ;否则,就走这一堆逻辑(mod > 0 && index < mod ? mqAll.size() / cidAll.size() + 1 : mqAll.size() / cidAll.size())。我们 index 是 0,而 mod 是 2,index < mod 则是成立的,那么最终 averageSize 的值就为 12 / 5 + 1 = 3。
接下来是 startIndex,由于这个三元运算符的条件是成立的,所以其值为 0 * 3 ,就为 0。
看了一大堆逻辑,是不是已经晕了?直接举实例:
12 个 Message Queue
5 个 Consumer 实例
按照上面的分法:
排在第 1 的消费者 分到 3 个
排在第 2 的消费者 分到 3 个
排在第 3 的消费者 分到 2 个
排在第 4 的消费者 分到 2 个
排在第 5 的消费者 分到 2 个

所以,你可以大致认为:
先“均分”,12 / 5 取整为 2。然后“均分”完之后还剩下 2 个,那么就从上往下,挨个再分配,这样第 1、第 2 个消费者就会被多分到 1 个。
所以如果有 13 个 MessageQueue,5 个 Consumer,那么第 1、第 2、第 3 就会被分配 3 个。
但并不准确,因为分配的 MessageQueue 是一次性的,例如那 3 个 MessageQueue 是一次性获取的,不会先给 2 个,再给 1 个。
而我们开篇提到的 Consumer 的 ClientID 相同,会造成什么?
当然是 index 的值相同,进而造成 mod、averageSize、startIndex、range 全部相同。那么最后 result.add(mqAll.get((startIndex + i) % mqAll.size())); 时,本来不同的 Consumer,会取到相同的 MessageQueue(举个例子,Consumer 1 和 Consumer 2 都取到了前 3 个 MessageQueue),从而造成有些 MessageQueue(如果有的话) 没有 Consumer 对其消费,而没有被消费,消息也在不停的投递进来,就会造成消息的大量堆积。
当然,现在的新版本从代码上看已经修复这个问题了,这个只是对之前的版本的原因做一个探索。
本篇文章已放到我的 Github github.com/sh-blog 中,欢迎 Star。微信搜索关注【SH的全栈笔记】,回复【队列】获取MQ学习资料,包含基础概念解析和RocketMQ详细的源码解析,持续更新中。
如果你觉得这篇文章对你有帮助,还麻烦点个赞,关个注,分个享,留个言。

关于 RocketMQ ClientID 相同引发的消息堆积的问题的更多相关文章
- 线上kafka消息堆积,consumer掉线,怎么办?
线上kafka消息堆积,所有consumer全部掉线,到底怎么回事? 最近处理了一次线上故障,具体故障表现就是kafka某个topic消息堆积,这个topic的相关consumer全部掉线. 整体排查 ...
- EQueue - 详细谈一下消息持久化以及消息堆积的设计
前言 之前写了一篇文章,总体介绍了EQueue.在看这篇文章之前如果还没看过那篇文章,可能会看不懂这篇文章.所以建议没看过的朋友务必先看一下那篇文章中所提到的各种概念,这样才能更好的理解本文所说的内容 ...
- 转 Kafka、RabbitMQ、RocketMQ等消息中间件的对比 —— 消息发送性能和优势
Kafka.RabbitMQ.RocketMQ等消息中间件的对比 —— 消息发送性能和优势 引言 分布式系统中,我们广泛运用消息中间件进行系统间的数据交换,便于异步解耦.现在开源的消息中间件有很多,前 ...
- RabbitMQ,RocketMQ,Kafka 几种消息队列的对比
常用的几款消息队列的对比 前言 RabbitMQ 优点 缺点 RocketMQ 优点 缺点 Kafka 优点 缺点 如何选择合适的消息队列 参考 常用的几款消息队列的对比 前言 消息队列的作用: 1. ...
- 一次 kafka 消息堆积问题排查
收到某业务组的小伙伴发来的反馈,具体问题如下: 项目中某 kafka 消息组消费特别慢,有时候在 kafka-manager 控制台看到有些消费者已被踢出消费组. 从服务端日志看到如下信息: 该消费组 ...
- RocketMQ系列(三)消息的生产与消费
前面的章节,我们已经把RocketMQ的环境搭建起来了,是一个两主两从的异步集群.接下来,我们就看看怎么去使用RocketMQ,在使用之前,先要在NameServer中创建Topic,我们知道Rock ...
- 2020-04-28:工作中如何解决MQ消息堆积和消息重复的问题?
福哥答案2020-04-28:此答案来自群员,感谢群员支持. 消息堆积 只能考虑 增多消费者 以及后端其他服务 组件的吞吐能力 别的有办法吗 如果更彻底一点 分撒单个队列里的消息 队列 更分门别类 或 ...
- 如何处理RabbitMQ 消息堆积和消息丢失问题
消息堆积 解决方案: 增加消费者或后台相关组件的吞吐能力 增加消费的多线程处理 根据不同的业务实现不同的丢弃任务,选择不同的策略淘汰任务 默认情况下,RabbitMQ消费者为单线程串行消费,设置并行消 ...
- RabbitMQ,RocketMQ,Kafka 事务性,消息丢失和消息重复发送的处理策略
消息队列常见问题处理 分布式事务 什么是分布式事务 常见的分布式事务解决方案 基于 MQ 实现的分布式事务 本地消息表-最终一致性 MQ事务-最终一致性 RocketMQ中如何处理事务 Kafka中如 ...
随机推荐
- MySQL的详细讲解
目录 Mysql的架构与历史 MySQL的逻辑架构 更新中---- Mysql的架构与历史 MySQL的逻辑架构 第二层的架构是所有的跨引擎的功能实现的地方,例如:存储,触发器,视图等. 第三层半酣了 ...
- 初步认识HCIA,什么是计算机网络,拓扑,网络的发展,交换机,路由器,IP,光纤,带宽,广播,ARP......
HCIA ---- 华为认证初级网络工程师 云技术 --- 云存储 云计算 计算机技术 : --- 抽象语言 -- 电线号的转换 抽象语言 -- 编码 ---- 应用层 编码 --- 二进制 -- ...
- CF992E Nastya and King-Shamans(线段树二分+思维)
这是一道卡常好题 从160s卡到36s qwq 由于题目设计到原数组的单点修改,那么就对应着前缀和数组上的区间加. 很显然能想到用线段树来维护这么个东西. 那么该如果求题目要求的位置呢 我们来看这个题 ...
- AOP的简单介绍
1.AOP简介 AOP面向切面编程,采取横向抽取机制,取代了传统纵向继承体系重复性代码(性能监视.安全检查.缓存) SpringAOP使用纯java实现,不需要专门的编译过程和类加载器,在运行期间以代 ...
- Linux命令查看内存、整体负载、端口查看、进程查看、vim编辑器(3)
一.资源占用命令 1.查看内存(free) free命令默认是以kb为单位显示的. free -m用Mb单位来显示. free -h显示单位 . free -h -s 3 ,每隔三秒刷新一次,如果 ...
- 电脑日常使用bug记录
1.由于电脑太卡了,于是决定关一点服务,一不小心,电脑无线无法使用了.启动无线服务时提示"windows无法启动wlan autoconfig服务错误1068依赖服务" 启动 Ex ...
- 如何知道当前使用的python的安装路径
电脑里多处安装了python,那么如何得知当前使用python的安装路径呢? 方法一 运行python指令: import sys print(sys.executable) 方法二 对于终端和Win ...
- Convolutional Neural Network-week1编程题(TensorFlow实现手势数字识别)
1. TensorFlow model import math import numpy as np import h5py import matplotlib.pyplot as plt impor ...
- 算法:拉丁方阵(Latin Square)
拉丁方阵(英语:Latin square)是一种 n × n 的方阵,在这种 n × n 的方阵里,恰有 n 种不同的元素,每一种不同的元素在同一行或同一列里只出现一次.以下是两个拉丁方阵举例: 拉丁 ...
- JavaScript中的this对象指向理解
在JavaScript中,this不是固定不变的,它的指向取决于上下文环境,一般的,认为this指向使用它时所在的对象.主要有以下几类指向: 在方法中,this 表示该方法所属的对象. 如果单独使用, ...