rocketmq(二)

1. 高級功能

1.1 消息存儲

分佈式隊列因爲有高可靠性的要求,所以數據要進行持久化存儲。
在這裏插入圖片描述

  1. 消息生成者發送消息
  2. MQ收到消息,將消息進行持久化,在存儲中新增一條記錄
  3. 返回ACK給生產者
  4. MQ push 消息給對應的消費者,然後等待消費者返回ACK
  5. 如果消息消費者在指定時間內成功返回ack,那麼MQ認爲消息消費成功,在存儲中刪除消息,即執行第6步;如果MQ在指定時間內沒有收到ACK,則認爲消息消費失敗,會嘗試重新push消息,重複執行4、5、6步驟
  6. MQ刪除消息

1.1.1 存儲介質

  • 關係型數據庫DB
    Apache下開源的另外一款MQ—ActiveMQ(默認採用的KahaDB做消息存儲)可選用JDBC的方式來做消息持久化,通過簡單的xml配置信息即可實現JDBC消息存儲。由於,普通關係型數據庫(如Mysql)在單表數據量達到千萬級別的情況下,其IO讀寫性能往往會出現瓶頸。在可靠性方面,該種方案非常依賴DB,如果一旦DB出現故障,則MQ的消息就無法落盤存儲會導致線上故障

  • 文件系統
    目前業界較爲常用的幾款產品(RocketMQ/Kafka/RabbitMQ)均採用的是消息刷盤至所部署虛擬機/物理機的文件系統來做持久化(刷盤一般可以分爲異步刷盤和同步刷盤兩種模式)。消息刷盤爲消息存儲提供了一種高效率、高可靠性和高性能的數據持久化方式。除非部署MQ機器本身或是本地磁盤掛了,否則一般是不會出現無法持久化的故障問題。

1.1.2 性能對比

文件系統>關係型數據庫DB

1.1.3 消息的存儲和發送

1)消息存儲

磁盤如果使用得當,磁盤的速度完全可以匹配上網絡 的數據傳輸速度。目前的高性能磁盤,順序寫速度可以達到600MB/s, 超過了一般網卡的傳輸速度。但是磁盤隨機寫的速度只有大概100KB/s,和順序寫的性能相差6000倍!因爲有如此巨大的速度差別,好的消息隊列系統會比普通的消息隊列系統速度快多個數量級。RocketMQ的消息用順序寫,保證了消息存儲的速度。
####2)消息發送

Linux操作系統分爲【用戶態】和【內核態】,文件操作、網絡操作需要涉及這兩種形態的切換,免不了進行數據複製。

一臺服務器 把本機磁盤文件的內容發送到客戶端,一般分爲兩個步驟:

1)read;讀取本地文件內容;

2)write;將讀取的內容通過網絡發送出去。

這兩個看似簡單的操作,實際進行了4 次數據複製,分別是:

  1. 從磁盤複製數據到內核態內存;
  2. 從內核態內存復 制到用戶態內存;
  3. 然後從用戶態 內存複製到網絡驅動的內核態內存;
  4. 最後是從網絡驅動的內核態內存復 制到網卡中進行傳輸。
    在這裏插入圖片描述
    通過使用mmap的方式,可以省去向用戶態的內存複製,提高速度。這種機制在Java中是通過MappedByteBuffer實現的

RocketMQ充分利用了上述特性,也就是所謂的“零拷貝”技術,提高消息存盤和網絡發送的速度。

這裏需要注意的是,採用MappedByteBuffer這種內存映射的方式有幾個限制,其中之一是一次只能映射1.5~2G 的文件至用戶態的虛擬內存,這也是爲何RocketMQ默認設置單個CommitLog日誌數據文件爲1G的原因了

1.1.4 消息存儲結構

RocketMQ消息的存儲是由ConsumeQueue和CommitLog配合完成 的,消息真正的物理存儲文件是CommitLog,ConsumeQueue是消息的邏輯隊列,類似數據庫的索引文件,存儲的是指向物理存儲的地址。每 個Topic下的每個Message Queue都有一個對應的ConsumeQueue文件。
在這裏插入圖片描述

1.1.5 刷盤機制

RocketMQ的消息是存儲到磁盤上的,這樣既能保證斷電後恢復, 又可以讓存儲的消息量超出內存的限制。RocketMQ爲了提高性能,會盡可能地保證磁盤的順序寫。消息在通過Producer寫入RocketMQ的時 候,有兩種寫磁盤方式,分佈式同步刷盤和異步刷盤。
在這裏插入圖片描述

1)同步刷盤

在返回寫成功狀態時,消息已經被寫入磁盤。具體流程是,消息寫入內存的PAGECACHE後,立刻通知刷盤線程刷盤, 然後等待刷盤完成,刷盤線程執行完成後喚醒等待的線程,返回消息寫 成功的狀態。

2)異步刷盤

在返回寫成功狀態時,消息可能只是被寫入了內存的PAGECACHE,寫操作的返回快,吞吐量大;當內存裏的消息量積累到一定程度時,統一觸發寫磁盤動作,快速寫入。

3)配置

同步刷盤還是異步刷盤,都是通過Broker配置文件裏的flushDiskType 參數設置的,這個參數被配置成SYNC_FLUSH、ASYNC_FLUSH中的 一個。

1.2 高可用性機制

在這裏插入圖片描述
RocketMQ分佈式集羣是通過Master和Slave的配合達到高可用性的。

Master和Slave的區別:在Broker的配置文件中,參數 brokerId的值爲0表明這個Broker是Master,大於0表明這個Broker是 Slave,同時brokerRole參數也會說明這個Broker是Master還是Slave。

Master角色的Broker支持讀和寫,Slave角色的Broker僅支持讀,也就是 Producer只能和Master角色的Broker連接寫入消息;Consumer可以連接 Master角色的Broker,也可以連接Slave角色的Broker來讀取消息。

1.2.1 消息消費高可用

在Consumer的配置文件中,並不需要設置是從Master讀還是從Slave 讀,當Master不可用或者繁忙的時候,Consumer會被自動切換到從Slave 讀。有了自動切換Consumer這種機制,當一個Master角色的機器出現故障後,Consumer仍然可以從Slave讀取消息,不影響Consumer程序。這就達到了消費端的高可用性。

1.2.2 消息發送高可用

在創建Topic的時候,把Topic的多個Message Queue創建在多個Broker組上(相同Broker名稱,不同 brokerId的機器組成一個Broker組),這樣當一個Broker組的Master不可 用後,其他組的Master仍然可用,Producer仍然可以發送消息。 RocketMQ目前還不支持把Slave自動轉成Master,如果機器資源不足, 需要把Slave轉成Master,則要手動停止Slave角色的Broker,更改配置文 件,用新的配置文件啓動Broker。
在這裏插入圖片描述

1.2.3 消息主從複製

如果一個Broker組有Master和Slave,消息需要從Master複製到Slave 上,有同步和異步兩種複製方式。

1)同步複製

同步複製方式是等Master和Slave均寫 成功後才反饋給客戶端寫成功狀態;

在同步複製方式下,如果Master出故障, Slave上有全部的備份數據,容易恢復,但是同步複製會增大數據寫入 延遲,降低系統吞吐量。

2)異步複製

異步複製方式是隻要Master寫成功 即可反饋給客戶端寫成功狀態。

在異步複製方式下,系統擁有較低的延遲和較高的吞吐量,但是如果Master出了故障,有些數據因爲沒有被寫 入Slave,有可能會丟失;

3)配置

同步複製和異步複製是通過Broker配置文件裏的brokerRole參數進行設置的,這個參數可以被設置成ASYNC_MASTER、 SYNC_MASTER、SLAVE三個值中的一個。

4)總結

實際應用中要結合業務場景,合理設置刷盤方式和主從複製方式, 尤其是SYNC_FLUSH方式,由於頻繁地觸發磁盤寫動作,會明顯降低 性能。通常情況下,應該把Master和Save配置成ASYNC_FLUSH的刷盤 方式,主從之間配置成SYNC_MASTER的複製方式,這樣即使有一臺 機器出故障,仍然能保證數據不丟,是個不錯的選擇。

1.3 負載均衡

1.3.1 Producer負載均衡

Producer端,每個實例在發消息的時候,默認會輪詢所有的message queue發送,以達到讓消息平均落在不同的queue上。而由於queue可以散落在不同的broker,所以消息就發送到不同的broker下,如下圖:
在這裏插入圖片描述
圖中箭頭線條上的標號代表順序,發佈方會把第一條消息發送至 Queue 0,然後第二條消息發送至 Queue 1,以此類推。

1.3.2 Consumer負載均衡

1)集羣模式

在集羣消費模式下,每條消息只需要投遞到訂閱這個topic的Consumer Group下的一個實例即可。RocketMQ採用主動拉取的方式拉取並消費消息,在拉取的時候需要明確指定拉取哪一條message queue。

而每當實例的數量有變更,都會觸發一次所有實例的負載均衡,這時候會按照queue的數量和實例的數量平均分配queue給每個實例。

默認的分配算法是AllocateMessageQueueAveragely,如下圖:
在這裏插入圖片描述.還有另外一種平均的算法是AllocateMessageQueueAveragelyByCircle,也是平均分攤每一條queue,只是以環狀輪流分queue的形式,如下圖:
在這裏插入圖片描述
需要注意的是,集羣模式下,queue都是隻允許分配只一個實例,這是由於如果多個實例同時消費一個queue的消息,由於拉取哪些消息是consumer主動控制的,那樣會導致同一個消息在不同的實例下被消費多次,所以算法上都是一個queue只分給一個consumer實例,一個consumer實例可以允許同時分到不同的queue。

通過增加consumer實例去分攤queue的消費,可以起到水平擴展的消費能力的作用。而有實例下線的時候,會重新觸發負載均衡,這時候原來分配到的queue將分配到其他實例上繼續消費。

但是如果consumer實例的數量比message queue的總數量還多的話,多出來的consumer實例將無法分到queue,也就無法消費到消息,也就無法起到分攤負載的作用了。所以需要控制讓queue的總數量大於等於consumer的數量。

####2)廣播模式

由於廣播模式下要求一條消息需要投遞到一個消費組下面所有的消費者實例,所以也就沒有消息被分攤消費的說法。

在實現上,其中一個不同就是在consumer分配queue的時候,所有consumer都分到所有的queue。
在這裏插入圖片描述
對於無序消息(普通、定時、延時、事務消息),當消費者消費消息失敗時,您可以通過設置返回狀態達到消息重試的結果。

無序消息的重試只針對集羣消費方式生效;廣播方式不提供失敗重試特性,即消費失敗後,失敗消息不再重試,繼續消費新的消息。

1)重試次數

消息隊列 RocketMQ 默認允許每條消息最多重試 16 次,每次重試的間隔時間如下:

第幾次重試 與上次重試的間隔時間 第幾次重試 與上次重試的間隔時間
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 次後仍然失敗,消息將不再投遞。如果嚴格按照上述重試時間間隔計算,某條消息在一直消費失敗的前提下,將會在接下來的 4 小時 46 分鐘之內進行 16 次重試,超過這個時間範圍消息將不再重試投遞。

注意: 一條消息無論重試多少次,這些重試消息的 Message ID 不會改變。

2)配置方式

消費失敗後,重試配置方式

集羣消費方式下,消息消費失敗後期望消息重試,需要在消息監聽器接口的實現中明確進行配置(三種方式任選一種):

  • 返回 Action.ReconsumeLater (推薦)
  • 返回 Null
  • 拋出異常
public class MessageListenerImpl implements MessageListener {
    @Override
    public Action consume(Message message, ConsumeContext context) {
        //處理消息
        doConsumeMessage(message);
        //方式1:返回 Action.ReconsumeLater,消息將重試
        return Action.ReconsumeLater;
        //方式2:返回 null,消息將重試
        return null;
        //方式3:直接拋出異常, 消息將重試
        throw new RuntimeException("Consumer Message exceotion");
    }
}

消費失敗後,不重試配置方式

集羣消費方式下,消息失敗後期望消息不重試,需要捕獲消費邏輯中可能拋出的異常,最終返回 Action.CommitMessage,此後這條消息將不會再重試。

public class MessageListenerImpl implements MessageListener {
    @Override
    public Action consume(Message message, ConsumeContext context) {
        try {
            doConsumeMessage(message);
        } catch (Throwable e) {
            //捕獲消費邏輯中的所有異常,並返回 Action.CommitMessage;
            return Action.CommitMessage;
        }
        //消息處理正常,直接返回 Action.CommitMessage;
        return Action.CommitMessage;
    }
}

自定義消息最大重試次數

消息隊列 RocketMQ 允許 Consumer 啓動的時候設置最大重試次數,重試時間間隔將按照如下策略:

  • 最大重試次數小於等於 16 次,則重試時間間隔同上表描述。
  • 最大重試次數大於 16 次,超過 16 次的重試時間間隔均爲每次 2 小時。
Properties properties = new Properties();
//配置對應 Group ID 的最大消息重試次數爲 20 次
properties.put(PropertyKeyConst.MaxReconsumeTimes,"20");
Consumer consumer =ONSFactory.createConsumer(properties);

注意:

  • 消息最大重試次數的設置對相同 Group ID 下的所有 Consumer 實例有效。
  • 如果只對相同 Group ID 下兩個 Consumer 實例中的其中一個設置了 MaxReconsumeTimes,那麼該配置對兩個 Consumer 實例均生效。
  • 配置採用覆蓋的方式生效,即最後啓動的 Consumer 實例會覆蓋之前的啓動實例的配置

獲取消息重試次數

消費者收到消息後,可按照如下方式獲取消息的重試次數:

public class MessageListenerImpl implements MessageListener {
    @Override
    public Action consume(Message message, ConsumeContext context) {
        //獲取消息的重試次數
        System.out.println(message.getReconsumeTimes());
        return Action.CommitMessage;
    }
}

1.5 死信隊列

當一條消息初次消費失敗,消息隊列 RocketMQ 會自動進行消息重試;達到最大重試次數後,若消費依然失敗,則表明消費者在正常情況下無法正確地消費該消息,此時,消息隊列 RocketMQ 不會立刻將消息丟棄,而是將其發送到該消費者對應的特殊隊列中。

在消息隊列 RocketMQ 中,這種正常情況下無法被消費的消息稱爲死信消息(Dead-Letter Message),存儲死信消息的特殊隊列稱爲死信隊列(Dead-Letter Queue)。

1.5.1 死信特性

死信消息具有以下特性

  • 不會再被消費者正常消費。
  • 有效期與正常消息相同,均爲 3 天,3 天后會被自動刪除。因此,請在死信消息產生後的 3 天內及時處理。

死信隊列具有以下特性:

  • 一個死信隊列對應一個 Group ID, 而不是對應單個消費者實例。
  • 如果一個 Group ID 未產生死信消息,消息隊列 RocketMQ 不會爲其創建相應的死信隊列。
  • 一個死信隊列包含了對應 Group ID 產生的所有死信消息,不論該消息屬於哪個 Topic。

1.5.2 查看死信信息

  1. 在控制檯查詢出現死信隊列的主題信息
    2.
  2. 在消息界面根據主題查詢死信消息

在這裏插入圖片描述
3. 選擇重新發送消息

一條消息進入死信隊列,意味着某些因素導致消費者無法正常消費該消息,因此,通常需要您對其進行特殊處理。排查可疑因素並解決問題後,可以在消息隊列 RocketMQ 控制檯重新發送該消息,讓消費者重新消費一次。

1.6 消費冪等

消息隊列 RocketMQ 消費者在接收到消息以後,有必要根據業務上的唯一 Key 對消息做冪等處理的必要性。

1.6.1 消費冪等的必要性

在互聯網應用中,尤其在網絡不穩定的情況下,消息隊列 RocketMQ 的消息有可能會出現重複,這個重複簡單可以概括爲以下情況:

  • 發送時消息重複

    當一條消息已被成功發送到服務端並完成持久化,此時出現了網絡閃斷或者客戶端宕機,導致服務端對客戶端應答失敗。 如果此時生產者意識到消息發送失敗並嘗試再次發送消息,消費者後續會收到兩條內容相同並且 Message ID 也相同的消息。

  • 投遞時消息重複

    消息消費的場景下,消息已投遞到消費者並完成業務處理,當客戶端給服務端反饋應答的時候網絡閃斷。 爲了保證消息至少被消費一次,消息隊列 RocketMQ 的服務端將在網絡恢復後再次嘗試投遞之前已被處理過的消息,消費者後續會收到兩條內容相同並且 Message ID 也相同的消息。

  • 負載均衡時消息重複(包括但不限於網絡抖動、Broker 重啓以及訂閱方應用重啓)

    當消息隊列 RocketMQ 的 Broker 或客戶端重啓、擴容或縮容時,會觸發 Rebalance,此時消費者可能會收到重複消息。

1.6.2 處理方式

因爲 Message ID 有可能出現衝突(重複)的情況,所以真正安全的冪等處理,不建議以 Message ID 作爲處理依據。 最好的方式是以業務唯一標識作爲冪等處理的關鍵依據,而業務的唯一標識可以通過消息 Key 進行設置:

Message message = new Message();
message.setKey("ORDERID_100");
SendResult sendResult = producer.send(message);

訂閱方收到消息時可以根據消息的 Key 進行冪等處理:

consumer.subscribe("ons_test", "*", new MessageListener() {
    public Action consume(Message message, ConsumeContext context) {
        String key = message.getKey()
        // 根據業務唯一標識的 key 做冪等處理
    }
});

2. 源碼分析

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