2022雙十一籌備:一個細小疏忽差點釀成大禍
1、背景
最近一周一來,總是有項目組中反饋遇到了消息重復推送的問題,連續(xù)出現單條消息短時被多個消費者消費的問題:
同時給出了證據,相同的msgId的消息被打印了兩條,并且兩者相隔7s。
講真,由于最近負責的事情比較多,并且RocketMQ又無法保證消息被重復消費,所以一開始并未引起我的充分重視,而是簡單的闡述了RocketMQ的設計原理不保證重復消費,需要消費端實現冪等。
但就是這個疏忽,后面集中爆發(fā),整個集群中消費組消費絕大部分都出現了嚴重的抖動,造成消費組頻繁積壓,開啟了一波緊張的問題排查與線上止血操作,同時并引發(fā)了我尋根究底之旅。
關于為什么會出現整個MQ集群消費組消費抖動,在這里先丟一個懸念,本文想和大家先來聊聊RocketMQ哪些設計會導致消息消費的重復消費。
2、探究RocketMQ重復消費
在RocketMQ的設計理念中,消息發(fā)送、消息消費端的相關機制都會帶來消息重復消費,接下來我們分別探討。
2.1 消息發(fā)送時重試造成消息重復發(fā)送
在RocketMQ中,由于消息發(fā)送者無法實時感知Broker路由變化,為了保證消息發(fā)送的高可用性,RocketMQ在消息發(fā)送時引入了重新機制,消息在客戶端的發(fā)送簡要流程如下圖所示:
也就是如果發(fā)送消息的過程中,如果一臺broker發(fā)送抖動導致消息發(fā)送超時,發(fā)送超時并不代表消息沒有成功寫入到Broker,也有可能是寫入到Broker耗時比較長罷了,但客戶端等不及,判定發(fā)生失敗,則嘗試再找另外一臺Broker發(fā)送一次消息,這樣就會導致發(fā)送了兩條消息,并且這兩條消息的msgId,消息體都一樣,但在消息集群中存在了兩條消息,也就是這兩條消息的msgId相同,但offsetMsgId不同。
在這里我普及一下RocketMQ中兩個消息id的含義:
- msgId 消息id,由發(fā)送者創(chuàng)建,全局唯一
- offsetMsgId 消息偏移,由Broker創(chuàng)建,記載了服務端的地址與消息的物理偏移量。
如果出現消息發(fā)送者發(fā)送了兩條消息,就會出項文章開頭那種情況,日志中會出現兩條相同的日志。
那進一步,那如何判斷出現消息消費重復是否是消息發(fā)送端的問題呢?
其實非常簡單,我們只需要在打印消息體的日志中再增加如下三個屬性就可以輕而易舉的進行判別,這些屬性如下:
- brokerName 消息存儲的broker的名稱
- queueId 消息消費隊列ID
- queueOffset 消息消費隊列偏移量
如果打印出來的消息,這幾個參數都相同,那就可以斷定是消息發(fā)送時重復發(fā)送了消息。
2.2消息消費時重復消費
介紹完消息發(fā)送時可能造成重復消費后,我們再來探究一下消息消費端是否會造成重復消費。
縱觀我對RocketMQ的理解,RocketMQ消息消費時造成消息消費重復主要有如下三個原因:
- 最小位點提交機制
- 消費位點批量提交
- 消費組重平衡
接下來我們簡單說明一下,詳細的解讀建議參考我最近新上市的書籍《RocketMQ實戰(zhàn)》,目前5折優(yōu)惠。
2.2.1最小位點提交機制
RocketMQ消息消費時采取了最小位點提交機制,說明如下圖所示:
例如消息消費線程池中的三個消費線程t1,t2,t3分別在處理消息偏移量1,2,3的消息,由于是并發(fā)消費,如果t3線程將msg3先消費完成,此時向服務端提交位點,是上報msg3的偏移量為最新的消費進度嗎?
顯然是不行的,因為一旦將3設置為最新的位點,但此時偏移量1,2的消息還未成功消費,一旦消費者重啟,那下一次將從偏移量為3的消息開始消費,這樣msg1,msg2這兩條消息將丟失。
所以RocketMQ為了確保消息消費端不發(fā)生消息丟失,采取了最小位點提交機制,也就是雖然線程t3將消息3消費完成,它要做的是在處理隊列中首先將msg3移除,然后選擇處理隊列中最小的偏移量作為位點進行上報,也就是t3線程消費完msg3后,會向服務端提交msg1的偏移量。
這樣也會帶來另外一個問題,消息重復消費,例如t3將msg3消費完成后,提交的位點是msg1的位點,如果消費者重啟,則會繼續(xù)從msg1開始消費,msg3就會再次被消費。
消息重復消費,消息丟失,兩害取其輕,確保消息不丟失是頭等大事,估在設計上就必然會出現消息重復消費。
2.2.2 消費為位點批量提交機制
消息這在消費完消息后,需要向服務端匯報位點,但如果每消費完一條消息,就向服務端發(fā)送位點提交請求,服務端性能肯定抗不住,故RocketMQ為了確保服務端的穩(wěn)定性,采取了批量提交位點的機制,具體如下圖所示:
客戶端會首先在本地緩存位點信息,然后每隔5s向服務端發(fā)起一次位點提交,服務端收到客戶端位點提交機制后,只是會更新內存中的我點數據,然后每隔5s將內存中的數據持久到磁盤中。
由于位點信息同步的延遲,如果客戶端重啟,也會造成一部分位點信息丟失,同樣會出現重復消費。
2.2.3 重平衡導致消費重復消費
RocketMQ消費者在消費之前,首先需要對隊列進行負載均衡,RocketMQ進行隊列負載的一個基本原則:一個隊列在同一個時間只會分給一個消費者,一個消費者可以消費多個隊列,當消費者個數或者隊列個數發(fā)生變化時,就會觸發(fā)重平衡,示意圖如下所示:
首先,q3隊列是由c2負載消費的,但當新增一個消費者時,q3隊列分配給了c3,也就是c2消費者在處理q3一部分消息后,該隊列被丟棄,位點提交失敗,這樣部分消息在c3消費者上又被消費一次,同樣造成重復消費。
3、總結
關于RocketMQ造成消息重復消費的幾種情況進行了一一解讀,那親愛的讀者朋友,你可以思考一下,集群大面積消息重復,消息消費抖動,你覺得會是哪種原因造成的呢?