metaq最佳實踐

_吹雪_發表於2018-10-09

1. 前言

本文件旨在描述RocketMQ使用過程中的一些最佳實踐,建議使用者這樣做,但是非必須。

2. Producer最佳實踐

2.1 傳送訊息注意事項

  1. 一個應用盡可能用一個Topic,訊息子型別用tags來標識,tags可以由應用自由設定。只有傳送訊息設定了tags,消費方在訂閱訊息時,才可以利用tags在broker做訊息過濾。message.setTags(“TagA”);

  2. 每個訊息在業務層面的唯一標識碼,要設定到keys欄位,方便將來定位訊息丟失問題。伺服器會為每個訊息建立索引(雜湊索引),應用可以通過topic,key來查詢這條訊息內容,以及訊息被誰消費。由於是雜湊索引,請務必保證key儘可能唯一,這樣可以避免潛在的雜湊衝突。
    // 訂單Id
    String orderId = “20034568923546”;
    message.setKeys(orderId);

  3. 訊息傳送成功或者失敗,要列印訊息日誌,務必要列印sendresult和key欄位。

  4. send訊息方法,只要不拋異常,就代表傳送成功。但是傳送成功會有多個狀態,在sendResult裡定義。

  • SEND_OK
    訊息傳送成功
  • FLUSH_DISK_TIMEOUT
    訊息傳送成功,但是伺服器刷盤超時, 訊息已經進入伺服器佇列,只有此時伺服器當機,訊息才會丟失
  • FLUSH_SLAVE_TIMEOUT
    訊息傳送成功,但是伺服器同步到Slave時超時, 訊息已經進入伺服器佇列,只有此時伺服器當機,訊息才會丟失
  • SLAVE_NOT_AVAILABLE
    訊息傳送成功,但是此時slave不可用, 訊息已經進入伺服器佇列,只有此時伺服器當機,訊息才會丟失

對於精衛傳送順序訊息的應用,由於順序訊息的侷限性,可能會涉及到主備自動切換問題,所以如果sendresult中的status欄位不等於SEND_OK,就應該嘗試重試。對於其他應用,則沒有必要這樣。

  1. 對於訊息不可丟失應用,務必要有訊息重發機制
    例如如果訊息傳送失敗,儲存到資料庫,能有定時程式嘗試重發,或者人工觸發重發。

2.2訊息傳送失敗如何處理

Producer的send方法本身支援內部重試,重試邏輯如下:

  1. 至多重試3次。
  2. 如果傳送失敗,則輪轉到下一個Broker。
  3. 這個方法的總耗時時間不超過sendMsgTimeout設定的值,預設10s。所以,如果本身向broker傳送訊息產生超時異常,就不會再做重試。

以上策略仍然不能保證訊息一定傳送成功,為保證訊息一定成功,建議應用這樣做如果呼叫send同步方法傳送失敗, 則嘗試將訊息儲存到db,由後臺執行緒定時重試,保證訊息一定到達Broker。

上述db重試方式為什麼沒有整合到MQ客戶端內部做,而是要求應用自己去完成,我們基於以下幾點考慮

  1. MQ的客戶端設計為無狀態模式,方便任意的水平擴充套件,且對機器資源的消耗僅僅是cpu、記憶體、網路。
  2. 如果MQ客戶端內部整合一個KV儲存模組,那麼資料只有同步落盤才能較可靠,而同步落盤本身效能開銷較大,所以通常會採用非同步落盤,又由於應用關閉過程不受MQ運維人員控制,可能經常會發生kill -9這樣暴力方式關閉,造成資料沒有及時落盤而丟失。
  3. Producer所在機器的可靠性較低,一般為虛擬機器,不適合儲存重要資料。

綜上,建議重試過程交由應用來控制。

2.3選擇oneway形式傳送

一個RPC呼叫,通常是這樣一個過程

  1. 客戶端傳送請求到伺服器
  2. 伺服器處理該請求
  3. 伺服器向客戶端返回應答所以一個RPC的耗時時間是上述三個步驟的總和,而某些場景要求耗時非常短,但是對可靠性要求並不高,例如日誌收集類應用,此類應用可以採用oneway形式呼叫,oneway形式只傳送請求不等待應答,而傳送請求在客戶端實現層面僅僅是一個os系統呼叫的開銷,即將資料寫入客戶端的socket緩衝區,此過程耗時通常在微秒級。

3 Consumer最佳實踐

3.1消費過程要做到冪等(即消費端去重)

如《RocketMQ 原理簡介》中所述,RocketMQ無法避免訊息重複,所以如果業務對消費重複非常敏感,務必要在業務層面去重,有以下幾種去重方式

  1. 將訊息的唯一鍵,可以是msgId,也可以是訊息內容中的唯一標識欄位,例如訂單Id等,消費之前判斷是否在Db或Tair(全域性KV儲存)中存在,如果不存在則插入,並消費,否則跳過。(實際過程要考慮原子性問題,判斷是否存在可以嘗試插入,如果報主鍵衝突,則插入失敗,直接跳過)
    msgId一定是全域性唯一識別符號,但是可能會存在同樣的訊息有兩個不同msgId的情況(有多種原因),這種情況可能會使業務上重複消費,建議最好使用訊息內容中的唯一標識欄位去重。
  2. 使用業務層面的狀態機去重

3.3消費速度慢處理方式

3.3.1提高消費並行度

絕大部分訊息消費行為屬於IO密集型,即可能是運算元據庫,或者呼叫RPC,這類消費行為的消費速度在於後端資料庫或者外系統的吞吐量,通過增加消費並行度,可以提高總的消費吞吐量,但是並行度增加到一定程度,反而會下降,如圖所示,呈現拋物線形式。所以應用必須要設定合理的並行度。CPU密集型應用除外。

3.3.2批量方式消費

某些業務流程如果支援批量方式消費,則可以很大程度上提高消費吞吐量,例如訂單扣款類應用,一次處理一個訂單耗時1秒鐘,一次處理10個訂單可能也只耗時2秒鐘,這樣即可大幅度提高消費的吞吐量,通過設定consumer的consumeMessageBatchMaxSize這個引數,預設是1,即一次只消費一條訊息,例如設定為N,那麼每次消費的訊息數小於等於N。

3.3.3跳過非重要訊息

發生訊息堆積時,如果消費速度一直追不上傳送速度,可以選擇丟棄不重要的訊息
如何判斷消費發生了堆積?

public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
    longoffset = msgs.get(0).getQueueOffset();
    String maxOffset = msgs.get(0).getProperty(Message.PROPERTY_MAX_OFFSET);
    longdiff = Long.parseLong(maxOffset) - offset;
    if (diff > 100000) {
        // TODO訊息堆積情況的特殊處理
        returnConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    }
    // TODO正常消費過程
    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}

如以上程式碼所示,當某個佇列的訊息數堆積到100000條以上,則嘗試丟棄部分或全部訊息,這樣就可以快速追上傳送訊息的速度。

3.3.4優化每條訊息消費過程

舉例如下,某條訊息的消費過程如下
1.根據訊息從DB查詢資料1
2.根據訊息從DB查詢資料2
3.複雜的業務計算
4.向DB插入資料3
5.向DB插入資料4
這條訊息的消費過程與DB互動了4次,如果按照每次5ms計算,那麼總共耗時20ms,假設業務計算耗時5ms,那麼總過耗時25ms,如果能把4次DB互動優化為2次,那麼總耗時就可以優化到15ms,也就是說總體效能提高了40%。

對於Mysql等DB,如果部署在磁碟,那麼與DB進行互動,如果資料沒有命中cache,每次互動的RT會直線上升,如果採用SSD,則RT上升趨勢要明顯好於磁碟。個別應用可能會遇到這種情況:線上下壓測消費過程中,db表現非常好,每次RT都很短,但是上線執行一段時間,RT就會變長,消費吞吐量直線下降。

主要原因是線下壓測時間過短,線上執行一段時間後,cache命中率下降,那麼RT就會增加。建議線上下壓測時,要測試足夠長時間,儘可能模擬線上環境,壓測過程中,資料的分佈也很重要,資料不同,可能cache的命中率也會完全不同。3.4消費列印日誌如果訊息量較少,建議在消費入口方法列印訊息,方便後續排查問題。

3.4 消費列印日誌

如果訊息量較少,建議在消費入口方法列印訊息,方便後續排查問題。

public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
    log.info("RECEIVE_MSG_BEGIN: " + msgs.toString());
    // TODO正常消費過程
    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;}
}

如果能列印每條訊息消費耗時,那麼在排查消費慢等線上問題時,會更方便。