1 消息存儲
分布式隊列因為有高可靠性的要求,所以數據要進行持久化存儲。
- 生產者發送消息到MQ。
- MQ接收到消息,進行數據持久化,在存儲系統中新增一條記錄。
- 返回ACK確認給生產者。
- 消費者上線后,MQ將消息push給對應的消費者。
- 消費者在指定時間內消費完消息后,成功則返回ACK,MQ接收到ACK后在存儲系統中刪除消息,即第6步;
若MQ在指定時間內沒有收到ACK,則認為消息失敗,會嘗試重新push消息,即重復執行4,5,6步驟。 - MQ在存儲系統中刪除確認后的消息。
1.1 存儲介質
- 關系型數據庫
mysql等,但是過于依賴db,數據量大的情況下可能出現性能瓶頸 - 文件系統
目前比較主流,通過消息刷盤的方式進行數據持久化,性能好于db
1.2 消息的存儲和發送
1.2.1 消息存儲
目前的高性能慈庵,順序 寫速度可以達到600MB/s,但是 隨機 寫的速度只有大概100KB/s,速度差別很大。因此RocketMQ的消息采用 順序 寫,保證了消息存儲的速度。
1.2.2 消息發送
Linux操作系統分為【用戶態】和【內核態】,文件操作、網絡操作需要涉及這兩種形態的切換,免不了進行數據復制。
一臺服務器把本機磁盤文件的內容發送到客戶端,一般分為兩個步驟:
1)read:讀取本地文件內容;
2)write:將讀取的內容通過網絡發送出去。
這兩步實際進行了 4 次數據復制,分別是:
- 從磁盤復制數據到內核態內存;
- 從內核態內存復制到用戶態內存;
- 然后從用戶態內存復制到網絡驅動的內核態內存;
- 最后是從網絡驅動的內核態內存復 制到網卡中進行傳輸。
采用mmap(將一個文件或者其它對象映射進內存)方式,可以省去向用戶態內存的復制,提高速度,這種機制在Java中是通過MappedByteBuffer實現的。RocketMQ充分利用了上述特性,也就是所謂的 “零拷貝” 技術,提高消息存盤和網絡發送的速度。
ps:采用MappedByteBuffer這種內存映射的方式有幾個限制,其中之一是一次只能映射 1.5~2G 的文件至用戶態的虛擬內存,這也是為何RocketMQ默認設置單個CommitLog日志數據文件為1G的原因。
1.3 消息存儲結構
RocketMQ消息的存儲是由ConsumeQueue和CommitLog配合完成的。
CommitLog存儲了消息的信息,ConsumeQueue是消息的邏輯隊列,類似數據庫的索引文件,存儲的是指向物理存儲的地址。
每 個Topic下的每個Message Queue都有一個對應的ConsumeQueue文件。
- CommitLog:存儲消息的元數據,1G大小,滿了會自動創建新的文件,也是1G。
- ConsumerQueue:存儲消息在CommitLog的索引,文件很小,可以加載到內存,提升效率
- IndexFile:為了消息查詢提供了一種通過key或時間區間來查詢消息的方法,這種通過IndexFile來查找消息的方法不影響發送與消費消息的主流程
1.4 刷盤機制
RocketMQ的消息是存儲到磁盤上的,這樣既能保證斷電后恢復, 又可以讓存儲的消息量超出內存的限制。
RocketMQ為了提高性能,會盡可能地保證磁盤的 順序寫 。消息在通過Producer寫入RocketMQ的時候,有兩種寫磁盤方式,分為同步刷盤和異步刷盤。
1) 同步刷盤
在返回寫成功狀態時,消息已經被寫入磁盤。具體流程是,消息寫入內存的PAGECACHE后,立刻通知刷盤線程刷盤, 然后等待刷盤完成,刷盤線程執行完成后喚醒等待的線程,返回消息寫 成功的狀態。
2)異步刷盤
在返回寫成功狀態時,消息可能只是被寫入了內存的PAGECACHE,寫操作的返回快,吞吐量大;當內存里的消息量積累到一定程度時,統一觸發寫磁盤動作,快速寫入。
3)配置
同步刷盤還是異步刷盤,都是通過Broker配置文件里的flushDiskType 參數設置的,這個參數被配置成SYNC_FLUSH、ASYNC_FLUSH中的一個。
2 高可用機制
通過建立RocketMQ分布式集群達到高可用。
Broker中Master和Slave的區別:
在Broker的配置文件中,參數 brokerId的值為 0 表明這個Broker是 Master ,大于0表明這個Broker是 Slave,同時brokerRole參數也會說明這個Broker是Master還是Slave。
master支持讀和寫,slave僅支持讀。
2.1 消息消費高可用
在Consumer的配置文件中,并不需要設置是從Master讀還是從Slave 讀,當Master不可用或者繁忙的時候,Consumer會被自動切換到從Slave 讀。有了自動切換Consumer這種機制,當一個Master角色的機器出現故障后,Consumer仍然可以從Slave讀取消息,不影響Consumer程序,從而達到了消費端的高可用性。
2.2 消息發送高可用
在創建Topic的時候,把Topic的多個Message Queue創建在多個Broker組上(相同Broker名稱,不同 brokerId的機器組成一個Broker組),這樣當一個Broker組的Master不可用后,其他組的Master仍然可用,Producer仍然可以發送消息。 RocketMQ目前還不支持把Slave自動轉成Master,如果機器資源不足, 需要把Slave轉成Master,則要手動停止Slave角色的Broker,更改配置文件,用新的配置文件啟動Broker。
2.3 消息主從復制
如果一個Broker組有Master和Slave,消息需要從Master復制到Slave 上,有同步和異步兩種復制方式。
1)同步復制
同步復制方式是等Master和Slave均寫成功后才反饋給客戶端寫成功狀態;
在同步復制方式下,如果Master出故障, Slave上有全部的備份數據,容易恢復,但是同步復制會增大數據寫入 延遲,降低系統吞吐量。
2)異步復制
異步復制方式是只要Master寫成功即可反饋給客戶端寫成功狀態。
在異步復制方式下,系統擁有較低的延遲和較高的吞吐量,但是如果Master出了故障,有些數據因為沒有被寫 入Slave,有可能會丟失;
3)配置
同步復制和異步復制是通過Broker配置文件里的brokerRole參數進行設置的,這個參數可以被設置成ASYNC_MASTER、 SYNC_MASTER、SLAVE(slave機器設置成這個參數)三個值中的一個。
4)總結
實際應用中要結合業務場景,合理設置刷盤方式和主從復制方式, 尤其是SYNC_FLUSH方式,由于頻繁地觸發磁盤寫動作,會明顯降低性能。通常情況下,應該把Master和Save配置成 ASYNC_FLUSH 的刷盤方式,主從之間配置成 SYNC_MASTER 的復制方式,這樣即使有一臺機器出故障,仍然能保證數據不丟。
3 負載均衡
3.1 Producer負載均衡
Producer端,每個實例在發消息的時候,默認會 輪詢 所有的message queue發送,以達到讓消息平均落在不同的queue上。而由于queue可以散落在不同的broker,所以消息就發送到不同的broker下,如下圖:
圖中箭頭線條上的標號代表順序,發布方會把第一條消息發送至 Queue 0,然后第二條消息發送至 Queue 1,以此類推。
3.2 Consumer負載均衡
1) 集群模式
在集群消費模式下,每條消息只需要投遞到訂閱這個topic的Consumer Group下的一個實例即可。RocketMQ采用 主動拉取 的方式拉取并消費消息,在拉取的時候需要明確指定拉取哪一條message queue。
每當實例的數量有變更,都會觸發一次所有實例的負載均衡,這時候會按照queue的數量和實例的數量平均分配queue給每個實例。
默認的分配算法是AllocateMessageQueueAveragely,如下圖:
還有另外一種平均的算法是AllocateMessageQueueAveragelyByCircle,也是平均分攤每一條queue,只是以環狀輪流分queue的形式,如下圖:
ps: 集群模式下,queue都是只允許分配只 一個實例 ,這是由于如果多個實例同時消費一個queue的消息,由于拉取哪些消息是consumer主動控制的,那樣會導致同一個消息在不同的實例下被消費多次,所以算法上都是一個queue只分給一個consumer實例,一個consumer實例可以允許同時分到不同的queue。
通過增加consumer實例去分攤queue的消費,可以起到水平擴展的消費能力的作用。而有實例下線的時候,會重新觸發負載均衡,這時候原來分配到的queue將分配到其他實例上繼續消費。
2)廣播模式
由于廣播模式下要求一條消息需要投遞到一個消費組下面所有的消費者實例,所以也就沒有消息被分攤消費的說法。
在實現上,其中一個不同就是在consumer分配queue的時候,所有consumer都分到所有的queue。
4 消息重試
4.1 順序消息重試
對于順序消息,當消費者消費消息失敗后,消息隊列 RocketMQ 會自動不斷進行消息重試(每次間隔時間為 1 秒),這時,應用會出現消息消費被阻塞的情況。因此,在使用順序消息時,務必保證應用能夠及時監控并處理消費失敗的情況,避免阻塞現象的發生。
4.2 無序消息的重試
對于無序消息(普通、定時、延時、事務消息),當消費者消費消息失敗時,您可以通過設置返回狀態達到消息重試的結果。
無序消息的重試只針對 集群消費 方式生效;廣播方式 不提供 失敗重試特性,即消費失敗后,失敗消息不再重試,繼續消費新的消息。
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);
ps:
- 消息最大重試次數的設置對相同 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;
}
}
5 死信隊列
當一條消息初次消費失敗,消息隊列 RocketMQ 會自動進行消息重試;達到最大重試次數后,若消費依然失敗,則表明消費者在正常情況下無法正確地消費該消息,此時,消息隊列 RocketMQ 不會立刻將消息丟棄,而是將其發送到該消費者對應的特殊隊列中。
在消息隊列 RocketMQ 中,這種正常情況下無法被消費的消息稱為死信消息(Dead-Letter Message),存儲死信消息的特殊隊列稱為死信隊列(Dead-Letter Queue)。
5.1 死信特性
死信消息具有以下特性
- 不會再被消費者正常消費。
- 有效期與正常消息相同,均為 3 天,3 天后會被自動刪除。因此,請在死信消息產生后的 3 天內及時處理。
死信隊列具有以下特性:
- 一個死信隊列對應一個 Group ID, 而不是對應單個消費者實例。
- 如果一個 Group ID 未產生死信消息,消息隊列 RocketMQ 不會為其創建相應的死信隊列。
- 一個死信隊列包含了對應 Group ID 產生的所有死信消息,不論該消息屬于哪個 Topic。
5.2 查看死信信息
- 在控制臺查詢出現死信隊列的主題信息
- 在消息界面根據主題查詢死信消息
- 選擇重新發送消息
一條消息進入死信隊列,意味著某些因素導致消費者無法正常消費該消息,因此,通常需要您對其進行特殊處理。排查可疑因素并解決問題后,可以在消息隊列 RocketMQ 控制臺重新發送該消息,讓消費者重新消費一次。
6 消費冪等
消息隊列 RocketMQ 消費者在接收到消息以后,有必要根據業務上的唯一 Key 對消息做冪等處理的必要性。(防止重復消費)
6.1 消費冪等的必要性
在互聯網應用中,尤其在網絡不穩定的情況下,消息隊列 RocketMQ 的消息有可能會出現重復,這個重復簡單可以概括為以下情況:
-
發送時消息重復
當一條消息已被成功發送到服務端并完成持久化,此時出現了網絡閃斷或者客戶端宕機,導致服務端對客戶端應答失敗。 如果此時生產者意識到消息發送失敗并嘗試再次發送消息,消費者后續會收到兩條內容相同并且 Message ID 也相同的消息。
-
投遞時消息重復
消息消費的場景下,消息已投遞到消費者并完成業務處理,當客戶端給服務端反饋應答的時候網絡閃斷。 為了保證消息至少被消費一次,消息隊列 RocketMQ 的服務端將在網絡恢復后再次嘗試投遞之前已被處理過的消息,消費者后續會收到兩條內容相同并且 Message ID 也相同的消息。
-
負載均衡時消息重復(包括但不限于網絡抖動、Broker 重啟以及訂閱方應用重啟)
當消息隊列 RocketMQ 的 Broker 或客戶端重啟、擴容或縮容時,會觸發 Rebalance,此時消費者可能會收到重復消息。
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 做冪等處理
}
});