RocketMQ的tag還有這個“坑”!

>作者簡介:大家好,我是《RocketMQ 技術內幕》一書作者、RocketMQ 開源社區首席佈道師,公衆號「中間件興趣圈」維護者,主打成體系剖析 Java 主流中間件,已發佈 Kafka、RocketMQ、Dubbo、Sentinel、Canal、ElasticJob 等中間件 15 個專欄。

RocketMQ提供了基於Tag的消息過濾機制,但在使用過程中有很多朋友或多或少會有一些疑問,我不經意在RocketMQ官方釘釘羣,我記得有好多朋友都有問到如下問題:

今天我就與RocketMQ Tag幾個值得關注的問題,和大家來做一個分享,看過後的朋友,如果覺得有幫助,期待你的點贊支持

  • 消費組訂閱關係不一致爲什麼會到來消息丟失?
  • 如果一個tag的消息數量很少,是否會顯示很高的延遲?

1、消費組訂閱關係不一致導致消息丟失

從消息消費的視角來看消費組是一個基本的物理隔離單位,每一個消費組擁有自己的消費位點、消費線程池等。

RocketMQ的初學者容易犯這樣一個錯誤:消費組中的不同消費者,訂閱同一個topic的不同的tag,這樣會導致消息丟失(部分消息沒有消費),在思考這個問題時,我們不妨先來看一張圖:

簡單闡述一下其核心關鍵點:

  1. 例如一個Topic共有4個隊列。

  2. 消息發送者連續發送4條tagA的消息後,再連續發送4條tagb的消息,消息發送者端默認採取輪循的負載均衡機制,這樣topic的每一個隊列中都存在tagA、tabB兩個tag的消息。

  3. 消費組dw_tag_test的IP爲192.168.3.10的消費者訂閱tagA,另外一個IP爲192.168.3.11的消費者訂閱tagB。

  4. 消費組內的消費者在進行消息消費之前,首先會進行隊列負載,默認爲平均分配,分配結果:

    • 192.168.3.10 分配到q0、q1。

    • 192.168.3.11 分配到q2、q3。

      • 消費者然後向Broker發起消息拉取請求,192.168.3.10消費者會由於只訂閱了tagA,這樣存在q0、q1中的tagB的消息會被過濾,但被過濾的tagB並不會投遞到另外一個訂閱了tagB的消費者,造成這部分消息沒有被投遞,從而導致消息丟失。
      • 同樣192.168.3.11消費者會由於只訂閱了tagB,這樣存在q2、q3中的tagA的消息會被過濾,但被過濾的tagA並不會投遞到另外一個訂閱了tagA的消費者,造成這部分消息沒有被投遞,從而導致消息丟失。

2、如果一個tag的消息數量很少,是否會顯示很高的延遲?

開篇有羣友會存在這樣一個擔憂,其場景大概如下圖所示:

消費者在消費offset=100的這條tag1消息後,後面連續出現1000W條非tag1的消息,這個消費組的積壓會持續增加,直接到1000W嗎?

要想明白這個問題,我們至少應該要重點去查看如下幾個功能的源碼:

  • 消息拉取流程
  • 位點提交機制

> 本文不準備全流程去分析這塊的源碼,如果大家對這塊代碼有興趣,可以查閱筆者出版的《RocketMQ技術內幕》書籍

本文將從以問題爲導向,經過自己的思考,並找到關鍵源碼加以求證,最後進行簡單的示例代碼進行驗證。

遇到問題之前,我們可以先嚐試思考一下,如果這個功能要我們實現,我們大概會怎麼去思考?

要判斷消費組在消費爲offset=100的消息後,在接下來1000W條消息都會被過濾的情況下,如果我們希望位點能夠提交,我們應該怎麼設計?我覺得應該至少有如下幾個關鍵點:

  • 消息消息拉取時連續1000W條消息找不到合適的消息,服務端會如何處理
  • 客戶端拉取到消息與未拉取到消息兩種情況如何提交位點

2.1 消息拉取流程中的關鍵設計

客戶端向服務端拉取消息,連續1000W條消息都不符合條件,一次過濾查找這麼多消息,肯定非常耗時,客戶端也不能等待這麼久,那服務端必須採取措施,必須觸發一個停止查找的條件並向客戶端返回NO_MESSAGE,客戶端在消息查找時會等待多久呢?

核心關鍵點一:客戶端在向服務端發起消息拉取請求時會設置超時時間,代碼如下所示:

其中與超時時間相關的兩個變量,其含義分別:

  • long brokerSuspendMaxTimeMillis 在當前沒有符合的消息時在Broker端允許掛起的時間,默認爲15s,暫時不支持自定義。
  • long timeoutMillis 消息拉取的超時時間,默認爲30s,暫時不支持自定義。

即一次消息拉取最大的超時時間爲30s。

核心關鍵點二:Broker端在處理消息拉取時設置了完備的退出條件,具體由DefaultMessageStore的getMessage方法事項,具體代碼如下所述:

核心要點

  • 首先客戶端在發起時會傳入一個本次期望拉取的消息數量,對應上述代碼中的maxMsgNums,如果拉取到指定條數到消息(讀者朋友們如體代碼讀者可以查閱isTheBatchFull方法),則正常退出。
  • 另外一個非常關鍵的過濾條件,即一次消息拉取過程中,服務端最大掃描的索引字節數,即一次拉取掃描ConsumeQueue的字節數量,取16000與期望拉取條數乘以20,因爲一個consumequeue條目佔20個字節。
  • 服務端還蘊含了一個長輪循機制,即如果掃描了指定的字節數,但一條消息都沒查詢到,會在broker端掛起一段時間,如果有新消息到來並符合過濾條件,則會喚醒,向客戶端返回消息。

回到這個問題,如果服務端連續1000W條非tag1的消息,拉取請求不會一次性篩選,而是會返回,不至於讓客戶端超時

從這裏可以打消第一個顧慮:服務端在沒有找到消息時不會傻傻等待不返回,接下來看是否會有積壓的關鍵是看如何提交位點。

2.2 位點提交機制

2.2.1 客戶端拉取到合適的消息位點提交機制

Pull線程從服務端拉取到結構後會將消息提交到消費組線程池,主要定義在DefaultMQPushConsumerImpl的PullTask類中,具體代碼如下所示:

衆所周知,RocketMQ是在消費成功後進行位點提交,代碼在ConsumeMessageConcurrentlyService中,如下所示:

這裏的核心要點:

  • 消費端成功消息完消費後,會採用最小位點提交機制,確保消費不丟失。

  • 最小位點提交機制,其實就是將拉取到的消息放入一個TreeMap中,然後消費線程成功消費一條消息後,將該消息從TreeMap中移除,再計算位點:

    • 如果當前TreeMap中還有消息在處理,則返回TreeMap中的第一條消息(最小位點)
    • 如果當前TreeMap中已沒有消息處理,返回的位點爲this.queueOffsetMax,queueOffsetMax的表示的是當前消費隊列中拉取到的最大消費位點,因爲此時拉取到的消息全部消費了。
  • 最後調用updateoffset方法,更新本地的位點緩存(有定時持久機制)

2.2.2 客戶端沒有拉取到合適的消息位點提交機制

客戶端如果沒有拉取到合適的消息,例如全部被tag過濾了,在DefaultMqPushConsumerImpl的PullTask中定義了處理方式,具體如下所示:

其關鍵代碼在correctTasOffset中,具體代碼請看:

核心要點:如果此時處理隊列中的消息爲0時,則會將下一次拉取偏移量當成位點,而這個值在服務端進行消息查找時會向前驅動,代碼在DefaultMessageStore的getMessage中:

故從這裏可以看到,就算消息全部過濾掉了,位點還是會向前驅動的,不會造成大量積壓。

2.2.3 消息拉取時會附帶一次位點提交

其實RocketMQ的位點提交,客戶端提交位點時會先存儲在本地緩存中,然後定時將位點信息一次性提交到Broker端,其實還存在另外一種較爲隱式位點提交機制:

即在消息拉取時,如果本地緩存中存在位點信息,會設置一個系統標記:FLAG_COMMIT_OFFSET,該標記在服務端會觸發一次位點提交,具體代碼如下:

2.2.4 總結與驗證

綜上述所述,使用TAG並不會因爲對應tag數量比較少,從而造成大量積壓的情況。

爲了驗證這個觀點,我也做了一個簡單的驗證,具體方法是啓動一個消息發送者,向指定topic發送tag B的消息,而消費者只訂閱tag A,但消費者並不會出現消費積壓,測試代碼如下圖所示:

查看消費組積壓情況如下圖所示:

文章首發:https://www.codingw.net/Article?id=759

最後說一句(求關注,別白嫖我)

如果這篇文章對您有所幫助,或者有所啓發的話,幫忙點個贊。

掌握一到兩門java主流中間件,是敲開BAT等大廠必備的技能,送給大家一個Java中間件學習路線,助力大家實現職場的蛻變。

Java進階之梯,成長路線與學習資料,助力突破中間件領域

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章