RocketMQ消息丟失如何排查?
消息丟失如何排查?當我們在使用mq的時候,經常會遇到消息消費異常的問題,原因有很多種,比如:
- producer發(fā)送失敗
- consumer消費異常
- consumer根本就沒收到消息
「那么我們該如何排查了?」
其實借助RocketMQ-Dashboard就能高效的排查,里面有很多你想象不到的功能。
首先我們先查找期望消費的消息,查找的方式有很多種,根據消息id,時間等。
「消息沒找到?」
說明proder發(fā)送異常,也有可能是消息過期了,因為rocketmq的消息默認保存72h,此時到producer端的日志進一步確認即可。
「消息找到了!」
接著看消息的消費狀態(tài),如下圖消息的消費狀態(tài)為NOT_ONLINE。
「NOT_ONLINE代表什么含義呢?」
別著急,我們一步步來分析,先看看TrackType到底有多少種狀態(tài)。
public enum TrackType {
CONSUMED,
CONSUMED_BUT_FILTERED,
PULL,
NOT_CONSUME_YET,
NOT_ONLINE,
UNKNOWN
}
每種類型的解釋如下:
類型 | 解釋 |
CONSUMED | 消息已經被消費 |
CONSUMED_BUT_FILTERED | 消息已經投遞但被過濾 |
PULL | 消息消費的方式是拉模式 |
NOT_CONSUME_YET | 目前沒有被消費 |
NOT_ONLINE | CONSUMER不在線 |
UNKNOWN | 未知錯誤 |
「怎么判定消息已經被消費?」
上一節(jié)我們講到,broker會用一個map來保存每個queue的消費進度,「如果queue的offset大于被查詢消息的offset則消息被消費,否則沒有被消費」(NOT_CONSUME_YET)。
我們在RocketMQ-Dashboard上其實就能看到每個隊列broker端的offset(代理者位點)以及消息消費的offset(消費者位點),差值就是沒有被消費的消息。
當消息都被消費時,差值為0,如下圖所示:
「CONSUMED_BUT_FILTERED表示消息已經投遞,但是已經被過濾掉了」。例如producer發(fā)的是topicA,tagA,但是consumer訂閱的卻是topicA,tagB。
「CONSUMED_BUT_FILTERED(消息已經被投遞但被過濾)是怎么發(fā)生的呢?」
這個就不得不提到RocketMQ中的一個概念,「消息消費要滿足訂閱關系一致性,即一個consumerGroup中的所有消費者訂閱的topic和tag必須保持一致,不然就會造成消息丟失」。
如下圖場景,發(fā)送了4條消息,consumer1訂閱了topica-taga,而consumer2訂閱了topica-tab。consumer1消費q0中的數據,consumer2消費q1中的數據。
投遞到q0的msg-1和msg-3只有msg-1能被正常消費,而msg-3則是CONSUMED_BUT_FILTERED。因為msg-3被投遞到q0,但是consumer1不消費tagb的消息導致消息被過濾,造成消息丟失。
同理msg-2這條消息也會丟失。
「注意,還有一個非常重要的點」!
雖然消息消費失敗了,但是消息的offset還會正常提交,即 「消息消費失敗了,但是狀態(tài)也會是CONSUMED」。
「RocketMQ認為消息消費失敗需要重試的場景有哪些?」
- 返回ConsumeConcurrentlyStatus.RECONSUME_LATER
- 返回null
- 主動或被動拋出異常
「那么消費失敗的消息去哪了呢?」
當消息消費失敗,會被放到重試隊列中,Topic名字為%RETRY% + consumerGroup。
「Consumer沒訂閱這個topic啊,怎么才能消費到重試消息?」
其實在Consumer啟動的時候,框架內部幫你訂閱了這個topic,所以重試消息能被消費到。
「另外消息不是一直重試,而是每隔1段時間進行重試」
第幾次重試 | 與上次重試的間隔時間 | 第幾次重試 | 與上次重試的間隔時間 |
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 小時 |
8 | 6 分鐘 | 16 | 2 小時 |
當消息超過最大消費次數16次,會將消息投遞到死信隊列中,死信隊列的topic名為%DLQ% + consumerGroup。
「因此當你發(fā)現消息狀態(tài)為CONSUMED,但是消費失敗時,去重試隊列和死信隊列中找就行了」。
消息消費異常排查實戰(zhàn)
這個問題發(fā)生的背景是這樣的,就是我們有2個系統(tǒng),中間通過mq來保證數據的一致性,結果有一天數據不一致了,那肯定是consumer消費消息有問題,或者producer發(fā)送消息有問題。
先根據時間段找到了消息,確保了發(fā)送沒有問題,接著看消息的狀態(tài)為NOT_CONSUME_YET,說明consumer在線但是沒有消息。
「NOT_CONSUME_YET表明消息沒有被消費」,但是消息發(fā)送都過了好長時間了,consumer不應該沒消費啊,查日志consumer確實沒有消費。
用RocketMQ-Dashboard查看一下代理者位點和消費者位點,0隊列正常消費,其他隊列沒有被消費。
「感覺這個負載均衡策略有點問題啊,怎么0隊列這么多消息,別的隊列都怎么沒消息,問一波中間件的同學,是不是又改負載均衡策略了?」
確實改了!測試環(huán)境下,采用隊列緯度區(qū)分多環(huán)境,0是基準環(huán)境,我們團隊目前還沒有用多環(huán)境,所以收發(fā)消息都會在隊列0上,其他隊列不會用到(「你可以簡單認為測試環(huán)境發(fā)送和消費消息只會用到0隊列」)。
「那么問題來了!」
首先消息的狀態(tài)是NOT_CONSUME_YET,說明消息肯定被投遞到0隊列之外了,但是中間件的小伙伴卻說消息不會被投遞到0隊列。
要想驗證我的想法首先需要證明沒有被消費的消息確實被投遞到0隊列之外的隊列了。
中間走的彎路就不說了,直到我看了看RocketMQ-Dashboard的源碼,「發(fā)現Dashboard其實返回了消息的很多信息,但是并沒有在頁面展示出來,直接看接口返回」。
乖乖,發(fā)現了新世界,消息的所有屬性都在這了,看到queuId為14,果然驗證了我的想法。
再看bornHost居然是我們辦公室的網段。
「難道本地啟動的負載均衡策略和測試環(huán)境的負載均衡策略不一樣?」
本地debug一波代碼,果然是本地的producer會往所有的隊列發(fā)送消息,并且consumer也會消費所有隊列的消息。
「至此找出問題了!」
producer在本地啟了一個服務,注冊到測試環(huán)境的zk,測試環(huán)境的部分請求打到本地,往0隊列之外的隊列發(fā)了消息,但是測試環(huán)境的consumer只會消費0隊列中的消息,導致消息遲遲沒有被消費。