RocketMQ消息發送常見錯誤與解決方案

點擊上方“中間件興趣圈”選擇“設爲星標”

做積極的人,越努力越幸運!

文將結合自己使用RocketMQ的經驗,對消息發送常見的問題進行分享,基本會遵循出現,分析問題、解決問題。

1、No route info of this topic


無法找到路由信息,其完整的錯誤堆棧信息如下:

且很多讀者朋友會說Broker端開啓了自動創建主題也會出現上述問題。

RocketMQ的路由尋找流程如下圖所示:

上面的核心關鍵點如下:
  • 如果Broker開啓了自動創建Topic,在啓動的時候會默認創建主題:TBW102,並會隨着Broker發送到Nameserver的心跳包彙報給Nameserver,繼而從Nameserver查詢路由信息時能返回路由信息。

  • 消息發送者在消息發送時首先會查本地緩存,如果本地緩存中存在,直接返回路由信息。

  • 如果緩存不存在,則向Nameserver查詢路由信息,如果Nameserver存在該路由信息,就直接返回。

  • 如果Nameserver不存在該topic的路由信息,如果沒有開啓自動創建主題,則拋出 No route info of this topic。

  • 如果開啓了自動創建主題,則使用默認主題向Nameserver查詢路由信息,並使用默認Topic的路由信息爲自己的路由信息,將不會拋出 No route info of this topic。

通常情況下 No route info of this topic 這個錯誤一般是在剛搭建RocketMQ,剛入門 RocketMQ遇到的比較多,通常的排查思路如下:

  • 可以通過rocketmq-console查詢路由信息是否存在,或使用如下命令查詢路由信息:

    cd ${ROCKETMQ_HOME}/bin
    sh ./mqadmin topicRoute -n 127.0.0.1:9876 -t dw_test_0003

    其輸出結果如下所示:

  • 如果通過命令無法查詢到路由信息,則查看Broker是否開啓了自動創建topic,參數爲:autoCreateTopicEnable,該參數默認爲true。但在生產環境不建議開啓。

  • 如果開啓了自動創建路由信息,但還是拋出這個錯誤,這個時候請檢查客戶端(Producer)連接的Nameserver地址是否與Broker中配置的nameserver地址是否一致。

經過上面的步驟,基本就能解決該錯誤。

2、消息發送超時


消息發送超時,通常客戶端的日誌如下:

客戶端報消息發送超時,通常第一懷疑的對象是RocketMQ服務器,是不是Broker性能出現了抖動,無法抗住當前的量。

那我們如何來排查RocketMQ當前是否有性能瓶頸呢?

首先我們執行如下命令查看RocketMQ 消息寫入的耗時分佈情況:

cd /${USER.HOME}/logs/rocketmqlogs/
grep -n 'PAGECACHERT' store.log | more

輸出結果如下所示:

RocketMQ會每一分鐘打印前一分鐘內消息發送的耗時情況分佈,我們從這裏就能窺探RocketMQ消息寫入是否存在明細的性能瓶頸,其區間如下:
  • [<=0ms] 小於0ms,即微妙級別的。

  • [0~10ms] 小於10ms的個數。

  • [10~50ms] 大於10ms小

  • 於50ms的個數

其他區間顯示,絕大多數會落在微妙級別完成,按照筆者的經驗如果100-200ms及以上的區間超過20個後,說明Broker確實存在一定的瓶頸,如果只是少數幾個,說明這個是內存或pagecache的抖動,問題不大。

通常情況下超時通常與Broker端的處理能力關係不大,還有另外一個佐證,在RocketMQ broker中還存在快速失敗機制,即當Broker收到客戶端的請求後會將消息先放入隊列,然後順序執行,如果一條消息隊列中等待超過200ms就會啓動快速失敗,向客戶端返回[TIMEOUT_CLEAN_QUEUE]broker busy,這個在本文的第3部分會詳細介紹。

在RocketMQ客戶端遇到網絡超時,通常可以考慮一些應用本身的垃圾回收,是否由於GC的停頓時間導致的消息發送超時,這個我在測試環境進行壓力測試時遇到過,但生產環境暫時沒有遇到過,大家稍微留意一下。

在RocketMQ中通常遇到網絡超時,通常與網絡的抖動有關係,但由於我對網絡不是特別擅長,故暫時無法找到直接證據,但能找到一些間接證據,例如在一個應用中同時連接了kafka、RocketMQ集羣,發現在出現超時的同一時間發現連接到RocketMQ集羣內所有Broker,連接到kafka集羣都出現了超時。

但出現網絡超時,我們總得解決,那有什麼解決方案嗎?

我們對消息中間件的最低期望就是高併發低延遲,從上面的消息發送耗時分佈情況也可以看出RocketMQ確實符合我們的期望,絕大部分請求都是在微妙級別內,故我給出的方案時,減少消息發送的超時時間,增加重試次數,並增加快速失敗的最大等待時長。具體措施如下:

  • 增加Broker端快速失敗的時長,建議爲1000,在broker的配置文件中增加如下配置:

    maxWaitTimeMillsInQueue=1000

    主要原因是在當前的RocketMQ版本中,快速失敗導致的錯誤爲SYSTEM_BUSY,並不會觸發重試,適當增大該值,儘可能避免觸發該機制,詳情可以參考本文第3部分內容,會重點介紹system_busy、broker_busy。

  • 如果RocketMQ的客戶端版本爲4.3.0以下版本(不含4.3.0)
    將超時時間設置消息發送的超時時間爲500ms,並將重試次數設置爲6次(這個可以適當進行調整,儘量大於3),其背後的哲學是儘快超時,並進行重試,因爲發現局域網內的網絡抖動是瞬時的,下次重試的是就能恢復,並且RocketMQ有故障規避機制,重試的時候會盡量選擇不同的Broker,相關的代碼如下:

    DefaultMQProducer producer = new DefaultMQProducer("dw_test_producer_group");
    producer.setNamesrvAddr("127.0.0.1:9876");
    producer.setRetryTimesWhenSendFailed(5);// 同步發送模式:重試次數
    producer.setRetryTimesWhenSendAsyncFailed(5);// 異步發送模式:重試次數
    producer.start();
    producer.send(msg,500);//消息發送超時時間
  • 如果RocketMQ的客戶端版本爲4.3.0及以上版本

    如果客戶端版本爲4.3.0及其以上版本,由於其設置的消息發送超時時間爲所有重試的總的超時時間,故不能直接通過設置RocketMQ的發送API的超時時間,而是需要對其API進行包裝,重試需要在外層收到進行,例如示例代碼如下:

    public static SendResult send(DefaultMQProducer producer, Message msg, int 
                                retryCount)
     
    {
      Throwable e = null;
      for(int i =0; i < retryCount; i ++ ) {
          try {
              return producer.send(msg,500); //設置超時時間,爲500ms,內部有重試機制
          } catch (Throwable e2) {
              e = e2;
          }
      }
      throw new RuntimeException("消息發送異常",e);
    }

3、System busy、Broker busy


在使用RocketMQ中,如果RocketMQ集羣達到1W/tps的壓力負載水平,System busy、Broker busy就會是大家經常會遇到的問題。例如如下圖所示的異常棧。

縱觀RocketMQ與system busy、broker busy相關的錯誤關鍵字,總共包含如下5個:
  • [REJECTREQUEST]system busy

  • too many requests and system thread pool busy

  • [PC_SYNCHRONIZED]broker busy

  • [PCBUSY_CLEAN_QUEUE]broker busy

  • [TIMEOUT_CLEAN_QUEUE]broker busy

3.1 原理分析

我們先用一張圖來闡述一下在消息發送的全生命週期中分別在什麼時候會拋出上述錯誤。

根據上述5類錯誤日誌,其觸發的原有可以歸納爲如下3種。
  • pagecache壓力較大

    其中如下三類錯誤屬於此種情況

  • [REJECTREQUEST]system busy

  • [PC_SYNCHRONIZED]broker busy

  • [PCBUSY_CLEAN_QUEUE]broker busy

    判斷pagecache是否忙的依據就是在寫入消息時,在向內存追加消息時加鎖的時間,默認的判斷標準是加鎖時間超過1s,就認爲是pagecache壓力大,向客戶端拋出相關的錯誤日誌。

  • 發送線程池擠壓的拒絕策略
    在RocketMQ中處理消息發送的是一個只有一個線程的線程池,內部會維護一個有界隊列,默認長度爲1W,如果當前隊列中擠壓的數量超過1w,執行線程池的拒絕策略,從而拋出[too many requests and system thread pool busy]錯誤。

  • Broker端快速失敗

    默認情況下Broker端開啓了快速失敗機制,就是在Broker端還未發生pagecache繁忙(加鎖超過1s)的情況,但存在一些請求在消息發送隊列中等待200ms的情況,RocketMQ會不再繼續排隊,直接向客戶端返回system busy,但由於rocketmq客戶端目前對該錯誤沒有進行重試處理,所以在解決這類問題的時候需要額外處理。

3.2 PageCache繁忙解決方案

一旦消息服務器出現大量pagecache繁忙(在向內存追加數據加鎖超過1s)的情況,這個是比較嚴重的問題,需要人爲進行干預解決,解決的問題思路如下:

  • transientStorePoolEnable

    開啓transientStorePoolEnable機制,即在broker中配置文件中增加如下配置:

    transientStorePoolEnable=true

    transientStorePoolEnable的原理如下圖所示:

 引入transientStorePoolEnable能緩解pagecache的壓力背後關鍵如下:
  • 消息先寫入到堆外內存中,該內存由於啓用了內存鎖定機制,故消息的寫入是接近直接操作內存,性能能得到保證。

  • 消息進入到堆外內存後,後臺會啓動一個線程,一批一批將消息提交到pagecache,即寫消息時對pagecache的寫操作由單條寫入變成了批量寫入,降低了對pagecache的壓力。

    引入transientStorePoolEnable會增加數據丟失的可能性,如果Broker JVM進程異常退出,提交到PageCache中的消息是不會丟失的,但存在堆外內存(DirectByteBuffer)中但還未提交到PageCache中的這部分消息,將會丟失。但通常情況下,RocketMQ進程退出的可能性不大,通常情況下,如果啓用了transientStorePoolEnable,消息發送端需要有重新推送機制(補償思想)。

  • 擴容

    如果在開啓了transientStorePoolEnable後,還會出現pagecache級別的繁忙,那需要集羣進行擴容,或者對集羣中的topic進行拆分,即將一部分topic遷移到其他集羣中,降低集羣的負載。


溫馨提示:在RocketMQ出現pagecache繁忙造成的broker busy,RocketMQ Client會有重試機制。

3.3 TIMEOUT_CLEAN_QUEUE 解決方案

由於如果出現TIMEOUT_CLEAN_QUEUE的錯誤,客戶端暫時不會對其進行重試,故現階段的建議是適當增加快速失敗的判斷標準,即在broker的配置文件中增加如下配置:

#該值默認爲200,表示200ms
waitTimeMillsInSendQueue=1000

本文來自筆者的另一力作《RocketMQ實戰與進階》,專欄從使用場景入手介紹如何使用 RocketMQ,使用過程中遇到什麼問題,如何解決這些問題,以及爲什麼可以這樣解決,即原理講解(圖)穿插在實戰中。專欄的設計思路重在強調實戰二字,旨在讓一位 RocketMQ 初學者通過對本專欄的學習,快速“打怪升級”,理論與實戰結合,成爲該領域的佼佼者。

本文分享自微信公衆號 - 中間件興趣圈(dingwpmz_zjj)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

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