RocketMQ消息丟失如何排查?
消息丟失如何排查?當(dāng)我們?cè)谑褂胢q的時(shí)候,經(jīng)常會(huì)遇到消息消費(fèi)異常的問(wèn)題,原因有很多種,比如:
- producer發(fā)送失敗
- consumer消費(fèi)異常
- consumer根本就沒(méi)收到消息
「那么我們?cè)撊绾闻挪榱?」
其實(shí)借助RocketMQ-Dashboard就能高效的排查,里面有很多你想象不到的功能。

首先我們先查找期望消費(fèi)的消息,查找的方式有很多種,根據(jù)消息id,時(shí)間等。
「消息沒(méi)找到?」
說(shuō)明proder發(fā)送異常,也有可能是消息過(guò)期了,因?yàn)閞ocketmq的消息默認(rèn)保存72h,此時(shí)到producer端的日志進(jìn)一步確認(rèn)即可。
「消息找到了!」
接著看消息的消費(fèi)狀態(tài),如下圖消息的消費(fèi)狀態(tài)為NOT_ONLINE。

「NOT_ONLINE代表什么含義呢?」
別著急,我們一步步來(lái)分析,先看看TrackType到底有多少種狀態(tài)。
public enum TrackType {
CONSUMED,
CONSUMED_BUT_FILTERED,
PULL,
NOT_CONSUME_YET,
NOT_ONLINE,
UNKNOWN
}
每種類(lèi)型的解釋如下:
類(lèi)型 | 解釋 |
CONSUMED | 消息已經(jīng)被消費(fèi) |
CONSUMED_BUT_FILTERED | 消息已經(jīng)投遞但被過(guò)濾 |
PULL | 消息消費(fèi)的方式是拉模式 |
NOT_CONSUME_YET | 目前沒(méi)有被消費(fèi) |
NOT_ONLINE | CONSUMER不在線 |
UNKNOWN | 未知錯(cuò)誤 |
「怎么判定消息已經(jīng)被消費(fèi)?」
上一節(jié)我們講到,broker會(huì)用一個(gè)map來(lái)保存每個(gè)queue的消費(fèi)進(jìn)度,「如果queue的offset大于被查詢消息的offset則消息被消費(fèi),否則沒(méi)有被消費(fèi)」(NOT_CONSUME_YET)。

我們?cè)赗ocketMQ-Dashboard上其實(shí)就能看到每個(gè)隊(duì)列broker端的offset(代理者位點(diǎn))以及消息消費(fèi)的offset(消費(fèi)者位點(diǎn)),差值就是沒(méi)有被消費(fèi)的消息。

當(dāng)消息都被消費(fèi)時(shí),差值為0,如下圖所示:

「CONSUMED_BUT_FILTERED表示消息已經(jīng)投遞,但是已經(jīng)被過(guò)濾掉了」。例如producer發(fā)的是topicA,tagA,但是consumer訂閱的卻是topicA,tagB。
「CONSUMED_BUT_FILTERED(消息已經(jīng)被投遞但被過(guò)濾)是怎么發(fā)生的呢?」
這個(gè)就不得不提到RocketMQ中的一個(gè)概念,「消息消費(fèi)要滿足訂閱關(guān)系一致性,即一個(gè)consumerGroup中的所有消費(fèi)者訂閱的topic和tag必須保持一致,不然就會(huì)造成消息丟失」。
如下圖場(chǎng)景,發(fā)送了4條消息,consumer1訂閱了topica-taga,而consumer2訂閱了topica-tab。consumer1消費(fèi)q0中的數(shù)據(jù),consumer2消費(fèi)q1中的數(shù)據(jù)。
投遞到q0的msg-1和msg-3只有msg-1能被正常消費(fèi),而msg-3則是CONSUMED_BUT_FILTERED。因?yàn)閙sg-3被投遞到q0,但是consumer1不消費(fèi)tagb的消息導(dǎo)致消息被過(guò)濾,造成消息丟失。
同理msg-2這條消息也會(huì)丟失。

「注意,還有一個(gè)非常重要的點(diǎn)」!
雖然消息消費(fèi)失敗了,但是消息的offset還會(huì)正常提交,即 「消息消費(fèi)失敗了,但是狀態(tài)也會(huì)是CONSUMED」。
「RocketMQ認(rèn)為消息消費(fèi)失敗需要重試的場(chǎng)景有哪些?」
- 返回ConsumeConcurrentlyStatus.RECONSUME_LATER
- 返回null
- 主動(dòng)或被動(dòng)拋出異常
「那么消費(fèi)失敗的消息去哪了呢?」
當(dāng)消息消費(fèi)失敗,會(huì)被放到重試隊(duì)列中,Topic名字為%RETRY% + consumerGroup。
「Consumer沒(méi)訂閱這個(gè)topic啊,怎么才能消費(fèi)到重試消息?」

其實(shí)在Consumer啟動(dòng)的時(shí)候,框架內(nèi)部幫你訂閱了這個(gè)topic,所以重試消息能被消費(fèi)到。
「另外消息不是一直重試,而是每隔1段時(shí)間進(jìn)行重試」
第幾次重試 | 與上次重試的間隔時(shí)間 | 第幾次重試 | 與上次重試的間隔時(shí)間 |
1 | 10 秒 | 9 | 7 分鐘 |
2 | 30 秒 | 10 | 8 分鐘 |
3 | 1 分鐘 | 11 | 9 分鐘 |
4 | 2 分鐘 | 12 | 10 分鐘 |
5 | 3 分鐘 | 13 | 20 分鐘 |
6 | 4 分鐘 | 14 | 30 分鐘 |
7 | 5 分鐘 | 15 | 1 小時(shí) |
8 | 6 分鐘 | 16 | 2 小時(shí) |
當(dāng)消息超過(guò)最大消費(fèi)次數(shù)16次,會(huì)將消息投遞到死信隊(duì)列中,死信隊(duì)列的topic名為%DLQ% + consumerGroup。
「因此當(dāng)你發(fā)現(xiàn)消息狀態(tài)為CONSUMED,但是消費(fèi)失敗時(shí),去重試隊(duì)列和死信隊(duì)列中找就行了」。
消息消費(fèi)異常排查實(shí)戰(zhàn)
這個(gè)問(wèn)題發(fā)生的背景是這樣的,就是我們有2個(gè)系統(tǒng),中間通過(guò)mq來(lái)保證數(shù)據(jù)的一致性,結(jié)果有一天數(shù)據(jù)不一致了,那肯定是consumer消費(fèi)消息有問(wèn)題,或者producer發(fā)送消息有問(wèn)題。
先根據(jù)時(shí)間段找到了消息,確保了發(fā)送沒(méi)有問(wèn)題,接著看消息的狀態(tài)為NOT_CONSUME_YET,說(shuō)明consumer在線但是沒(méi)有消息。

「NOT_CONSUME_YET表明消息沒(méi)有被消費(fèi)」,但是消息發(fā)送都過(guò)了好長(zhǎng)時(shí)間了,consumer不應(yīng)該沒(méi)消費(fèi)啊,查日志consumer確實(shí)沒(méi)有消費(fèi)。
用RocketMQ-Dashboard查看一下代理者位點(diǎn)和消費(fèi)者位點(diǎn),0隊(duì)列正常消費(fèi),其他隊(duì)列沒(méi)有被消費(fèi)。

「感覺(jué)這個(gè)負(fù)載均衡策略有點(diǎn)問(wèn)題啊,怎么0隊(duì)列這么多消息,別的隊(duì)列都怎么沒(méi)消息,問(wèn)一波中間件的同學(xué),是不是又改負(fù)載均衡策略了?」
確實(shí)改了!測(cè)試環(huán)境下,采用隊(duì)列緯度區(qū)分多環(huán)境,0是基準(zhǔn)環(huán)境,我們團(tuán)隊(duì)目前還沒(méi)有用多環(huán)境,所以收發(fā)消息都會(huì)在隊(duì)列0上,其他隊(duì)列不會(huì)用到(「你可以簡(jiǎn)單認(rèn)為測(cè)試環(huán)境發(fā)送和消費(fèi)消息只會(huì)用到0隊(duì)列」)。
「那么問(wèn)題來(lái)了!」
首先消息的狀態(tài)是NOT_CONSUME_YET,說(shuō)明消息肯定被投遞到0隊(duì)列之外了,但是中間件的小伙伴卻說(shuō)消息不會(huì)被投遞到0隊(duì)列。
要想驗(yàn)證我的想法首先需要證明沒(méi)有被消費(fèi)的消息確實(shí)被投遞到0隊(duì)列之外的隊(duì)列了。
中間走的彎路就不說(shuō)了,直到我看了看RocketMQ-Dashboard的源碼,「發(fā)現(xiàn)Dashboard其實(shí)返回了消息的很多信息,但是并沒(méi)有在頁(yè)面展示出來(lái),直接看接口返回」。

乖乖,發(fā)現(xiàn)了新世界,消息的所有屬性都在這了,看到queuId為14,果然驗(yàn)證了我的想法。
再看bornHost居然是我們辦公室的網(wǎng)段。
「難道本地啟動(dòng)的負(fù)載均衡策略和測(cè)試環(huán)境的負(fù)載均衡策略不一樣?」
本地debug一波代碼,果然是本地的producer會(huì)往所有的隊(duì)列發(fā)送消息,并且consumer也會(huì)消費(fèi)所有隊(duì)列的消息。
「至此找出問(wèn)題了!」
producer在本地啟了一個(gè)服務(wù),注冊(cè)到測(cè)試環(huán)境的zk,測(cè)試環(huán)境的部分請(qǐng)求打到本地,往0隊(duì)列之外的隊(duì)列發(fā)了消息,但是測(cè)試環(huán)境的consumer只會(huì)消費(fèi)0隊(duì)列中的消息,導(dǎo)致消息遲遲沒(méi)有被消費(fèi)。
































