首先,造成這個問題的 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詳細的原始碼解析,持續更新中。
如果你覺得這篇文章對你有幫助,還麻煩點個贊,關個注,分個享,留個言。