1. 高級功能
1.1 消息存儲
分佈式隊列因爲有高可靠性的要求,所以數據要進行持久化存儲。
- 消息生成者發送消息
- MQ收到消息,將消息進行持久化,在存儲中新增一條記錄
- 返回ACK給生產者
- MQ push 消息給對應的消費者,然後等待消費者返回ACK
- 如果消息消費者在指定時間內成功返回ack,那麼MQ認爲消息消費成功,在存儲中刪除消息,即執行第6步;如果MQ在指定時間內沒有收到ACK,則認爲消息消費失敗,會嘗試重新push消息,重複執行4、5、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 次數據複製,分別是:
- 從磁盤複製數據到內核態內存;
- 從內核態內存復 制到用戶態內存;
- 然後從用戶態 內存複製到網絡驅動的內核態內存;
- 最後是從網絡驅動的內核態內存復 制到網卡中進行傳輸。
通過使用mmap的方式,可以省去向用戶態的內存複製,提高速度。這種機制在Java中是通過MappedByteBuffer實現的
RocketMQ充分利用了上述特性,也就是所謂的“零拷貝”技術,提高消息存盤和網絡發送的速度。
這裏需要注意的是,採用MappedByteBuffer這種內存映射的方式有幾個限制,其中之一是一次只能映射1.5~2G 的文件至用戶態的虛擬內存,這也是爲何RocketMQ默認設置單個CommitLog日誌數據文件爲1G的原因了
1.1.4 消息存儲結構
RocketMQ消息的存儲是由ConsumeQueue和CommitLog配合完成 的,消息真正的物理存儲文件是CommitLog,ConsumeQueue是消息的邏輯隊列,類似數據庫的索引文件,存儲的是指向物理存儲的地址。每 個Topic下的每個Message Queue都有一個對應的ConsumeQueue文件。
- CommitLog:存儲消息的元數據
- ConsumerQueue:存儲消息在CommitLog的索引
- IndexFile:爲了消息查詢提供了一種通過key或時間區間來查詢消息的方法,這種通過IndexFile來查找消息的方法不影響發送與消費消息的主流程
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.4 消息重試
1.4.1 順序消息的重試
對於順序消息,當消費者消費消息失敗後,消息隊列 RocketMQ 會自動不斷進行消息重試(每次間隔時間爲 1 秒),這時,應用會出現消息消費被阻塞的情況。因此,在使用順序消息時,務必保證應用能夠及時監控並處理消費失敗的情況,避免阻塞現象的發生。
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);
注意:
- 消息最大重試次數的設置對相同 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 查看死信信息
- 在控制檯查詢出現死信隊列的主題信息
- 在消息界面根據主題查詢死信消息
- 選擇重新發送消息
一條消息進入死信隊列,意味着某些因素導致消費者無法正常消費該消息,因此,通常需要您對其進行特殊處理。排查可疑因素並解決問題後,可以在消息隊列 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. 源碼分析
2.1 環境搭建
依賴工具
- JDK :1.8+
- Maven
- IntelliJ IDEA
2.1.1 源碼拉取
從官方倉庫 https://github.com/apache/rocketmq clone
或者download
源碼。
源碼目錄結構:
-
broker: broker 模塊(broke 啓動進程)
-
client :消息客戶端,包含消息生產者、消息消費者相關類
-
common :公共包
-
dev :開發者信息(非源代碼)
-
distribution :部署實例文件夾(非源代碼)
-
example: RocketMQ 例代碼
-
filter :消息過濾相關基礎類
-
filtersrv:消息過濾服務器實現相關類(Filter啓動進程)
-
logappender:日誌實現相關類
-
namesrv:NameServer實現相關類(NameServer啓動進程)
-
openmessageing:消息開放標準
-
remoting:遠程通信模塊,給予Netty
-
srcutil:服務工具類
-
store:消息存儲實現相關類
-
style:checkstyle相關實現
-
test:測試相關類
-
tools:工具類,監控命令相關實現類
###2.1.2 導入IDEA
執行安裝
clean install -Dmaven.test.skip=true
2.1.3 調試
創建conf
配置文件夾,從distribution
拷貝broker.conf
和logback_broker.xml
和logback_namesrv.xml
1)啓動NameServer
- 展開namesrv模塊,右鍵NamesrvStartup.java
- 配置ROCKETMQ_HOME
-
重新啓動
控制檯打印結果
The Name Server boot success. serializeType=JSON
2)啓動Broker
broker.conf
配置文件內容
brokerClusterName = DefaultCluster
brokerName = broker-a
brokerId = 0
# namesrvAddr地址
namesrvAddr=127.0.0.1:9876
deleteWhen = 04
fileReservedTime = 48
brokerRole = ASYNC_MASTER
flushDiskType = ASYNC_FLUSH
autoCreateTopicEnable=true
# 存儲路徑
storePathRootDir=E:\\RocketMQ\\data\\rocketmq\\dataDir
# commitLog路徑
storePathCommitLog=E:\\RocketMQ\\data\\rocketmq\\dataDir\\commitlog
# 消息隊列存儲路徑
storePathConsumeQueue=E:\\RocketMQ\\data\\rocketmq\\dataDir\\consumequeue
# 消息索引存儲路徑
storePathIndex=E:\\RocketMQ\\data\\rocketmq\\dataDir\\index
# checkpoint文件路徑
storeCheckpoint=E:\\RocketMQ\\data\\rocketmq\\dataDir\\checkpoint
# abort文件存儲路徑
abortFile=E:\\RocketMQ\\data\\rocketmq\\dataDir\\abort
- 創建數據文件夾
dataDir
- 啓動
BrokerStartup
,配置broker.conf
和ROCKETMQ_HOME
3)發送消息
- 進入example模塊的
org.apache.rocketmq.example.quickstart
- 指定Namesrv地址
DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
producer.setNamesrvAddr("127.0.0.1:9876");
- 運行
main
方法,發送消息
4)消費消息
- 進入example模塊的
org.apache.rocketmq.example.quickstart
- 指定Namesrv地址
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_4");
consumer.setNamesrvAddr("127.0.0.1:9876");
- 運行
main
方法,消費消息
2.2 NameServer
2.2.1 架構設計
消息中間件的設計思路一般是基於主題訂閱發佈的機制,消息生產者(Producer)發送某一個主題到消息服務器,消息服務器負責將消息持久化存儲,消息消費者(Consumer)訂閱該興趣的主題,消息服務器根據訂閱信息(路由信息)將消息推送到消費者(Push模式)或者消費者主動向消息服務器拉去(Pull模式),從而實現消息生產者與消息消費者解耦。爲了避免消息服務器的單點故障導致的整個系統癱瘓,通常會部署多臺消息服務器共同承擔消息的存儲。那消息生產者如何知道消息要發送到哪臺消息服務器呢?如果某一臺消息服務器宕機了,那麼消息生產者如何在不重啓服務情況下感知呢?
NameServer就是爲了解決以上問題設計的。
Broker消息服務器在啓動的時向所有NameServer註冊,消息生產者(Producer)在發送消息時之前先從NameServer獲取Broker服務器地址列表,然後根據負載均衡算法從列表中選擇一臺服務器進行發送。NameServer與每臺Broker保持長連接,並間隔30S檢測Broker是否存活,如果檢測到Broker宕機,則從路由註冊表中刪除。但是路由變化不會馬上通知消息生產者。這樣設計的目的是爲了降低NameServer實現的複雜度,在消息發送端提供容錯機制保證消息發送的可用性。
NameServer本身的高可用是通過部署多臺NameServer來實現,但彼此之間不通訊,也就是NameServer服務器之間在某一個時刻的數據並不完全相同,但這對消息發送並不會造成任何影響,這也是NameServer設計的一個亮點,總之,RocketMQ設計追求簡單高效。
2.2.2 啓動流程
啓動類:org.apache.rocketmq.namesrv.NamesrvStartup
步驟一
解析配置文件,填充NameServerConfig、NettyServerConfig屬性值,並創建NamesrvController
代碼:NamesrvController#createNamesrvController
//創建NamesrvConfig
final NamesrvConfig namesrvConfig = new NamesrvConfig();
//創建NettyServerConfig
final NettyServerConfig nettyServerConfig = new NettyServerConfig();
//設置啓動端口號
nettyServerConfig.setListenPort(9876);
//解析啓動-c參數
if (commandLine.hasOption('c')) {
String file = commandLine.getOptionValue('c');
if (file != null) {
InputStream in = new BufferedInputStream(new FileInputStream(file));
properties = new Properties();
properties.load(in);
MixAll.properties2Object(properties, namesrvConfig);
MixAll.properties2Object(properties, nettyServerConfig);
namesrvConfig.setConfigStorePath(file);
System.out.printf("load config properties file OK, %s%n", file);
in.close();
}
}
//解析啓動-p參數
if (commandLine.hasOption('p')) {
InternalLogger console = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_CONSOLE_NAME);
MixAll.printObjectProperties(console, namesrvConfig);
MixAll.printObjectProperties(console, nettyServerConfig);
System.exit(0);
}
//將啓動參數填充到namesrvConfig,nettyServerConfig
MixAll.properties2Object(ServerUtil.commandLine2Properties(commandLine), namesrvConfig);
//創建NameServerController
final NamesrvController controller = new NamesrvController(namesrvConfig, nettyServerConfig);
NamesrvConfig屬性
private String rocketmqHome = System.getProperty(MixAll.ROCKETMQ_HOME_PROPERTY, System.getenv(MixAll.ROCKETMQ_HOME_ENV));
private String kvConfigPath = System.getProperty("user.home") + File.separator + "namesrv" + File.separator + "kvConfig.json";
private String configStorePath = System.getProperty("user.home") + File.separator + "namesrv" + File.separator + "namesrv.properties";
private String productEnvName = "center";
private boolean clusterTest = false;
private boolean orderMessageEnable = false;
**rocketmqHome:**rocketmq主目錄
**kvConfig:**NameServer存儲KV配置屬性的持久化路徑
**configStorePath:**nameServer默認配置文件路徑
**orderMessageEnable:**是否支持順序消息
NettyServerConfig屬性
private int listenPort = 8888;
private int serverWorkerThreads = 8;
private int serverCallbackExecutorThreads = 0;
private int serverSelectorThreads = 3;
private int serverOnewaySemaphoreValue = 256;
private int serverAsyncSemaphoreValue = 64;
private int serverChannelMaxIdleTimeSeconds = 120;
private int serverSocketSndBufSize = NettySystemConfig.socketSndbufSize;
private int serverSocketRcvBufSize = NettySystemConfig.socketRcvbufSize;
private boolean serverPooledByteBufAllocatorEnable = true;
private boolean useEpollNativeSelector = false;
**listenPort:**NameServer監聽端口,該值默認會被初始化爲9876
**serverWorkerThreads:**Netty業務線程池線程個數
**serverCallbackExecutorThreads:**Netty public任務線程池線程個數,Netty網絡設計,根據業務類型會創建不同的線程池,比如處理消息發送、消息消費、心跳檢測等。如果該業務類型未註冊線程池,則由public線程池執行。
**serverSelectorThreads:**IO線程池個數,主要是NameServer、Broker端解析請求、返回相應的線程個數,這類線程主要是處理網路請求的,解析請求包,然後轉發到各個業務線程池完成具體的操作,然後將結果返回給調用方;
**serverOnewaySemaphoreValue:**send oneway消息請求併發讀(Broker端參數);
**serverAsyncSemaphoreValue:**異步消息發送最大併發度;
**serverChannelMaxIdleTimeSeconds :**網絡連接最大的空閒時間,默認120s。
**serverSocketSndBufSize:**網絡socket發送緩衝區大小。
serverSocketRcvBufSize: 網絡接收端緩存區大小。
**serverPooledByteBufAllocatorEnable:**ByteBuffer是否開啓緩存;
**useEpollNativeSelector:**是否啓用Epoll IO模型。
步驟二
根據啓動屬性創建NamesrvController實例,並初始化該實例。NameServerController實例爲NameServer核心控制器
代碼:NamesrvController#initialize
public boolean initialize() {
//加載KV配置
this.kvConfigManager.load();
//創建NettyServer網絡處理對象
this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.brokerHousekeepingService);
//開啓定時任務:每隔10s掃描一次Broker,移除不活躍的Broker
this.remotingExecutor =
Executors.newFixedThreadPool(nettyServerConfig.getServerWorkerThreads(), new ThreadFactoryImpl("RemotingExecutorThread_"));
this.registerProcessor();
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
NamesrvController.this.routeInfoManager.scanNotActiveBroker();
}
}, 5, 10, TimeUnit.SECONDS);
//開啓定時任務:每隔10min打印一次KV配置
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
NamesrvController.this.kvConfigManager.printAllPeriodically();
}
}, 1, 10, TimeUnit.MINUTES);
return true;
}
步驟三
在JVM進程關閉之前,先將線程池關閉,及時釋放資源
代碼:NamesrvStartup#start
//註冊JVM鉤子函數代碼
Runtime.getRuntime().addShutdownHook(new ShutdownHookThread(log, new Callable<Void>() {
@Override
public Void call() throws Exception {
//釋放資源
controller.shutdown();
return null;
}
}));
2.2.3 路由管理
NameServer的主要作用是爲消息的生產者和消息消費者提供關於主題Topic的路由信息,那麼NameServer需要存儲路由的基礎信息,還要管理Broker節點,包括路由註冊、路由刪除等。
2.2.3.1 路由元信息
代碼:RouteInfoManager
private final HashMap<String/* topic */, List<QueueData>> topicQueueTable;
private final HashMap<String/* brokerName */, BrokerData> brokerAddrTable;
private final HashMap<String/* clusterName */, Set<String/* brokerName */>> clusterAddrTable;
private final HashMap<String/* brokerAddr */, BrokerLiveInfo> brokerLiveTable;
private final HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable;
**topicQueueTable:**Topic消息隊列路由信息,消息發送時根據路由表進行負載均衡
**brokerAddrTable:**Broker基礎信息,包括brokerName、所屬集羣名稱、主備Broker地址
**clusterAddrTable:**Broker集羣信息,存儲集羣中所有Broker名稱
**brokerLiveTable:**Broker狀態信息,NameServer每次收到心跳包是會替換該信息
**filterServerTable:**Broker上的FilterServer列表,用於類模式消息過濾。
RocketMQ基於定於發佈機制,一個Topic擁有多個消息隊列,一個Broker爲每一個主題創建4個讀隊列和4個寫隊列。多個Broker組成一個集羣,集羣由相同的多臺Broker組成Master-Slave架構,brokerId爲0代表Master,大於0爲Slave。BrokerLiveInfo中的lastUpdateTimestamp存儲上次收到Broker心跳包的時間。
2.2.3.2 路由註冊
1)發送心跳包
RocketMQ路由註冊是通過Broker與NameServer的心跳功能實現的。Broker啓動時向集羣中所有的NameServer發送心跳信息,每隔30s向集羣中所有NameServer發送心跳包,NameServer收到心跳包時會更新brokerLiveTable緩存中BrokerLiveInfo的lastUpdataTimeStamp信息,然後NameServer每隔10s掃描brokerLiveTable,如果連續120S沒有收到心跳包,NameServer將移除Broker的路由信息同時關閉Socket連接。
代碼:BrokerController#start
//註冊Broker信息
this.registerBrokerAll(true, false, true);
//每隔30s上報Broker信息到NameServer
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
BrokerController.this.registerBrokerAll(true, false, brokerConfig.isForceRegister());
} catch (Throwable e) {
log.error("registerBrokerAll Exception", e);
}
}
}, 1000 * 10, Math.max(10000, Math.min(brokerConfig.getRegisterNameServerPeriod(), 60000)),
TimeUnit.MILLISECONDS);
代碼:BrokerOuterAPI#registerBrokerAll
//獲得nameServer地址信息
List<String> nameServerAddressList = this.remotingClient.getNameServerAddressList();
//遍歷所有nameserver列表
if (nameServerAddressList != null && nameServerAddressList.size() > 0) {
//封裝請求頭
final RegisterBrokerRequestHeader requestHeader = new RegisterBrokerRequestHeader();
requestHeader.setBrokerAddr(brokerAddr);
requestHeader.setBrokerId(brokerId);
requestHeader.setBrokerName(brokerName);
requestHeader.setClusterName(clusterName);
requestHeader.setHaServerAddr(haServerAddr);
requestHeader.setCompressed(compressed);
//封裝請求體
RegisterBrokerBody requestBody = new RegisterBrokerBody();
requestBody.setTopicConfigSerializeWrapper(topicConfigWrapper);
requestBody.setFilterServerList(filterServerList);
final byte[] body = requestBody.encode(compressed);
final int bodyCrc32 = UtilAll.crc32(body);
requestHeader.setBodyCrc32(bodyCrc32);
final CountDownLatch countDownLatch = new CountDownLatch(nameServerAddressList.size());
for (final String namesrvAddr : nameServerAddressList) {
brokerOuterExecutor.execute(new Runnable() {
@Override
public void run() {
try {
//分別向NameServer註冊
RegisterBrokerResult result = registerBroker(namesrvAddr,oneway, timeoutMills,requestHeader,body);
if (result != null) {
registerBrokerResultList.add(result);
}
log.info("register broker[{}]to name server {} OK", brokerId, namesrvAddr);
} catch (Exception e) {
log.warn("registerBroker Exception, {}", namesrvAddr, e);
} finally {
countDownLatch.countDown();
}
}
});
}
try {
countDownLatch.await(timeoutMills, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
}
}
代碼:BrokerOutAPI#registerBroker
if (oneway) {
try {
this.remotingClient.invokeOneway(namesrvAddr, request, timeoutMills);
} catch (RemotingTooMuchRequestException e) {
// Ignore
}
return null;
}
RemotingCommand response = this.remotingClient.invokeSync(namesrvAddr, request, timeoutMills);
2)處理心跳包
org.apache.rocketmq.namesrv.processor.DefaultRequestProcessor
網路處理類解析請求類型,如果請求類型是爲***REGISTER_BROKER***,則將請求轉發到RouteInfoManager#regiesterBroker
代碼:DefaultRequestProcessor#processRequest
//判斷是註冊Broker信息
case RequestCode.REGISTER_BROKER:
Version brokerVersion = MQVersion.value2Version(request.getVersion());
if (brokerVersion.ordinal() >= MQVersion.Version.V3_0_11.ordinal()) {
return this.registerBrokerWithFilterServer(ctx, request);
} else {
//註冊Broker信息
return this.registerBroker(ctx, request);
}
代碼:DefaultRequestProcessor#registerBroker
RegisterBrokerResult result = this.namesrvController.getRouteInfoManager().registerBroker(
requestHeader.getClusterName(),
requestHeader.getBrokerAddr(),
requestHeader.getBrokerName(),
requestHeader.getBrokerId(),
requestHeader.getHaServerAddr(),
topicConfigWrapper,
null,
ctx.channel()
);
代碼:RouteInfoManager#registerBroker
維護路由信息
//加鎖
this.lock.writeLock().lockInterruptibly();
//維護clusterAddrTable
Set<String> brokerNames = this.clusterAddrTable.get(clusterName);
if (null == brokerNames) {
brokerNames = new HashSet<String>();
this.clusterAddrTable.put(clusterName, brokerNames);
}
brokerNames.add(brokerName);
//維護brokerAddrTable
BrokerData brokerData = this.brokerAddrTable.get(brokerName);
//第一次註冊,則創建brokerData
if (null == brokerData) {
registerFirst = true;
brokerData = new BrokerData(clusterName, brokerName, new HashMap<Long, String>());
this.brokerAddrTable.put(brokerName, brokerData);
}
//非第一次註冊,更新Broker
Map<Long, String> brokerAddrsMap = brokerData.getBrokerAddrs();
Iterator<Entry<Long, String>> it = brokerAddrsMap.entrySet().iterator();
while (it.hasNext()) {
Entry<Long, String> item = it.next();
if (null != brokerAddr && brokerAddr.equals(item.getValue()) && brokerId != item.getKey()) {
it.remove();
}
}
String oldAddr = brokerData.getBrokerAddrs().put(brokerId, brokerAddr);
registerFirst = registerFirst || (null == oldAddr);
//維護topicQueueTable
if (null != topicConfigWrapper && MixAll.MASTER_ID == brokerId) {
if (this.isBrokerTopicConfigChanged(brokerAddr, topicConfigWrapper.getDataVersion()) ||
registerFirst) {
ConcurrentMap<String, TopicConfig> tcTable = topicConfigWrapper.getTopicConfigTable();
if (tcTable != null) {
for (Map.Entry<String, TopicConfig> entry : tcTable.entrySet()) {
this.createAndUpdateQueueData(brokerName, entry.getValue());
}
}
}
}
代碼:RouteInfoManager#createAndUpdateQueueData
private void createAndUpdateQueueData(final String brokerName, final TopicConfig topicConfig) {
//創建QueueData
QueueData queueData = new QueueData();
queueData.setBrokerName(brokerName);
queueData.setWriteQueueNums(topicConfig.getWriteQueueNums());
queueData.setReadQueueNums(topicConfig.getReadQueueNums());
queueData.setPerm(topicConfig.getPerm());
queueData.setTopicSynFlag(topicConfig.getTopicSysFlag());
//獲得topicQueueTable中隊列集合
List<QueueData> queueDataList = this.topicQueueTable.get(topicConfig.getTopicName());
//topicQueueTable爲空,則直接添加queueData到隊列集合
if (null == queueDataList) {
queueDataList = new LinkedList<QueueData>();
queueDataList.add(queueData);
this.topicQueueTable.put(topicConfig.getTopicName(), queueDataList);
log.info("new topic registered, {} {}", topicConfig.getTopicName(), queueData);
} else {
//判斷是否是新的隊列
boolean addNewOne = true;
Iterator<QueueData> it = queueDataList.iterator();
while (it.hasNext()) {
QueueData qd = it.next();
//如果brokerName相同,代表不是新的隊列
if (qd.getBrokerName().equals(brokerName)) {
if (qd.equals(queueData)) {
addNewOne = false;
} else {
log.info("topic changed, {} OLD: {} NEW: {}", topicConfig.getTopicName(), qd,
queueData);
it.remove();
}
}
}
//如果是新的隊列,則添加隊列到queueDataList
if (addNewOne) {
queueDataList.add(queueData);
}
}
}
//維護brokerLiveTable
BrokerLiveInfo prevBrokerLiveInfo = this.brokerLiveTable.put(brokerAddr,new BrokerLiveInfo(
System.currentTimeMillis(),
topicConfigWrapper.getDataVersion(),
channel,
haServerAddr));
//維護filterServerList
if (filterServerList != null) {
if (filterServerList.isEmpty()) {
this.filterServerTable.remove(brokerAddr);
} else {
this.filterServerTable.put(brokerAddr, filterServerList);
}
}
if (MixAll.MASTER_ID != brokerId) {
String masterAddr = brokerData.getBrokerAddrs().get(MixAll.MASTER_ID);
if (masterAddr != null) {
BrokerLiveInfo brokerLiveInfo = this.brokerLiveTable.get(masterAddr);
if (brokerLiveInfo != null) {
result.setHaServerAddr(brokerLiveInfo.getHaServerAddr());
result.setMasterAddr(masterAddr);
}
}
}
2.2.3.3 路由刪除
Broker
每隔30s向NameServer
發送一個心跳包,心跳包包含BrokerId
,Broker
地址,Broker
名稱,Broker
所屬集羣名稱、Broker
關聯的FilterServer
列表。但是如果Broker
宕機,NameServer
無法收到心跳包,此時NameServer
如何來剔除這些失效的Broker
呢?NameServer
會每隔10s掃描brokerLiveTable
狀態表,如果BrokerLive
的lastUpdateTimestamp的時間戳距當前時間超過120s,則認爲Broker
失效,移除該Broker
,關閉與Broker
連接,同時更新topicQueueTable
、brokerAddrTable
、brokerLiveTable
、filterServerTable
。
RocketMQ有兩個觸發點來刪除路由信息:
- NameServer定期掃描brokerLiveTable檢測上次心跳包與當前系統的時間差,如果時間超過120s,則需要移除broker。
- Broker在正常關閉的情況下,會執行unregisterBroker指令
這兩種方式路由刪除的方法都是一樣的,就是從相關路由表中刪除與該broker相關的信息。
代碼:NamesrvController#initialize
//每隔10s掃描一次爲活躍Broker
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
NamesrvController.this.routeInfoManager.scanNotActiveBroker();
}
}, 5, 10, TimeUnit.SECONDS);
代碼:RouteInfoManager#scanNotActiveBroker
public void scanNotActiveBroker() {
//獲得brokerLiveTable
Iterator<Entry<String, BrokerLiveInfo>> it = this.brokerLiveTable.entrySet().iterator();
//遍歷brokerLiveTable
while (it.hasNext()) {
Entry<String, BrokerLiveInfo> next = it.next();
long last = next.getValue().getLastUpdateTimestamp();
//如果收到心跳包的時間距當時時間是否超過120s
if ((last + BROKER_CHANNEL_EXPIRED_TIME) < System.currentTimeMillis()) {
//關閉連接
RemotingUtil.closeChannel(next.getValue().getChannel());
//移除broker
it.remove();
//維護路由表
this.onChannelDestroy(next.getKey(), next.getValue().getChannel());
}
}
}
代碼:RouteInfoManager#onChannelDestroy
//申請寫鎖,根據brokerAddress從brokerLiveTable和filterServerTable移除
this.lock.writeLock().lockInterruptibly();
this.brokerLiveTable.remove(brokerAddrFound);
this.filterServerTable.remove(brokerAddrFound);
//維護brokerAddrTable
String brokerNameFound = null;
boolean removeBrokerName = false;
Iterator<Entry<String, BrokerData>> itBrokerAddrTable =this.brokerAddrTable.entrySet().iterator();
//遍歷brokerAddrTable
while (itBrokerAddrTable.hasNext() && (null == brokerNameFound)) {
BrokerData brokerData = itBrokerAddrTable.next().getValue();
//遍歷broker地址
Iterator<Entry<Long, String>> it = brokerData.getBrokerAddrs().entrySet().iterator();
while (it.hasNext()) {
Entry<Long, String> entry = it.next();
Long brokerId = entry.getKey();
String brokerAddr = entry.getValue();
//根據broker地址移除brokerAddr
if (brokerAddr.equals(brokerAddrFound)) {
brokerNameFound = brokerData.getBrokerName();
it.remove();
log.info("remove brokerAddr[{}, {}] from brokerAddrTable, because channel destroyed",
brokerId, brokerAddr);
break;
}
}
//如果當前主題只包含待移除的broker,則移除該topic
if (brokerData.getBrokerAddrs().isEmpty()) {
removeBrokerName = true;
itBrokerAddrTable.remove();
log.info("remove brokerName[{}] from brokerAddrTable, because channel destroyed",
brokerData.getBrokerName());
}
}
//維護clusterAddrTable
if (brokerNameFound != null && removeBrokerName) {
Iterator<Entry<String, Set<String>>> it = this.clusterAddrTable.entrySet().iterator();
//遍歷clusterAddrTable
while (it.hasNext()) {
Entry<String, Set<String>> entry = it.next();
//獲得集羣名稱
String clusterName = entry.getKey();
//獲得集羣中brokerName集合
Set<String> brokerNames = entry.getValue();
//從brokerNames中移除brokerNameFound
boolean removed = brokerNames.remove(brokerNameFound);
if (removed) {
log.info("remove brokerName[{}], clusterName[{}] from clusterAddrTable, because channel destroyed",
brokerNameFound, clusterName);
if (brokerNames.isEmpty()) {
log.info("remove the clusterName[{}] from clusterAddrTable, because channel destroyed and no broker in this cluster",
clusterName);
//如果集羣中不包含任何broker,則移除該集羣
it.remove();
}
break;
}
}
}
//維護topicQueueTable隊列
if (removeBrokerName) {
//遍歷topicQueueTable
Iterator<Entry<String, List<QueueData>>> itTopicQueueTable =
this.topicQueueTable.entrySet().iterator();
while (itTopicQueueTable.hasNext()) {
Entry<String, List<QueueData>> entry = itTopicQueueTable.next();
//主題名稱
String topic = entry.getKey();
//隊列集合
List<QueueData> queueDataList = entry.getValue();
//遍歷該主題隊列
Iterator<QueueData> itQueueData = queueDataList.iterator();
while (itQueueData.hasNext()) {
//從隊列中移除爲活躍broker信息
QueueData queueData = itQueueData.next();
if (queueData.getBrokerName().equals(brokerNameFound)) {
itQueueData.remove();
log.info("remove topic[{} {}], from topicQueueTable, because channel destroyed",
topic, queueData);
}
}
//如果該topic的隊列爲空,則移除該topic
if (queueDataList.isEmpty()) {
itTopicQueueTable.remove();
log.info("remove topic[{}] all queue, from topicQueueTable, because channel destroyed",
topic);
}
}
}
//釋放寫鎖
finally {
this.lock.writeLock().unlock();
}
2.2.3.4 路由發現
RocketMQ路由發現是非實時的,當Topic路由出現變化後,NameServer不會主動推送給客戶端,而是由客戶端定時拉取主題最新的路由。
代碼:DefaultRequestProcessor#getRouteInfoByTopic
public RemotingCommand getRouteInfoByTopic(ChannelHandlerContext ctx,
RemotingCommand request) throws RemotingCommandException {
final RemotingCommand response = RemotingCommand.createResponseCommand(null);
final GetRouteInfoRequestHeader requestHeader =
(GetRouteInfoRequestHeader) request.decodeCommandCustomHeader(GetRouteInfoRequestHeader.class);
//調用RouteInfoManager的方法,從路由表topicQueueTable、brokerAddrTable、filterServerTable中分別填充TopicRouteData的List<QueueData>、List<BrokerData>、filterServer
TopicRouteData topicRouteData = this.namesrvController.getRouteInfoManager().pickupTopicRouteData(requestHeader.getTopic());
//如果找到主題對應你的路由信息並且該主題爲順序消息,則從NameServer KVConfig中獲取關於順序消息相關的配置填充路由信息
if (topicRouteData != null) {
if (this.namesrvController.getNamesrvConfig().isOrderMessageEnable()) {
String orderTopicConf =
this.namesrvController.getKvConfigManager().getKVConfig(NamesrvUtil.NAMESPACE_ORDER_TOPIC_CONFIG,
requestHeader.getTopic());
topicRouteData.setOrderTopicConf(orderTopicConf);
}
byte[] content = topicRouteData.encode();
response.setBody(content);
response.setCode(ResponseCode.SUCCESS);
response.setRemark(null);
return response;
}
response.setCode(ResponseCode.TOPIC_NOT_EXIST);
response.setRemark("No topic route info in name server for the topic: " + requestHeader.getTopic()
+ FAQUrl.suggestTodo(FAQUrl.APPLY_TOPIC_URL));
return response;
}
2.2.4 小結
2.3 Producer
消息生產者的代碼都在client模塊中,相對於RocketMQ來講,消息生產者就是客戶端,也是消息的提供者。
###2.3.1 方法和屬性
####1)主要方法介紹
-
//創建主題 void createTopic(final String key, final String newTopic, final int queueNum) throws MQClientException;
-
//根據時間戳從隊列中查找消息偏移量 long searchOffset(final MessageQueue mq, final long timestamp)
-
//查找消息隊列中最大的偏移量 long maxOffset(final MessageQueue mq) throws MQClientException;
-
//查找消息隊列中最小的偏移量 long minOffset(final MessageQueue mq)
-
//根據偏移量查找消息 MessageExt viewMessage(final String offsetMsgId) throws RemotingException, MQBrokerException, InterruptedException, MQClientException;
-
//根據條件查找消息 QueryResult queryMessage(final String topic, final String key, final int maxNum, final long begin, final long end) throws MQClientException, InterruptedException;
-
//根據消息ID和主題查找消息 MessageExt viewMessage(String topic,String msgId) throws RemotingException, MQBrokerException, InterruptedException, MQClientException;
-
//啓動 void start() throws MQClientException;
-
//關閉 void shutdown();
-
//查找該主題下所有消息 List<MessageQueue> fetchPublishMessageQueues(final String topic) throws MQClientException;
-
//同步發送消息 SendResult send(final Message msg) throws MQClientException, RemotingException, MQBrokerException, InterruptedException;
-
//同步超時發送消息 SendResult send(final Message msg, final long timeout) throws MQClientException, RemotingException, MQBrokerException, InterruptedException;
-
//異步發送消息 void send(final Message msg, final SendCallback sendCallback) throws MQClientException, RemotingException, InterruptedException;
-
//異步超時發送消息 void send(final Message msg, final SendCallback sendCallback, final long timeout) throws MQClientException, RemotingException, InterruptedException;
-
//發送單向消息 void sendOneway(final Message msg) throws MQClientException, RemotingException, InterruptedException;
-
//選擇指定隊列同步發送消息 SendResult send(final Message msg, final MessageQueue mq) throws MQClientException, RemotingException, MQBrokerException, InterruptedException;
-
//選擇指定隊列異步發送消息 void send(final Message msg, final MessageQueue mq, final SendCallback sendCallback) throws MQClientException, RemotingException, InterruptedException;
-
//選擇指定隊列單項發送消息 void sendOneway(final Message msg, final MessageQueue mq) throws MQClientException, RemotingException, InterruptedException;
-
//批量發送消息 SendResult send(final Collection<Message> msgs) throws MQClientException, RemotingException, MQBrokerException,InterruptedException;
2)屬性介紹
producerGroup:生產者所屬組
createTopicKey:默認Topic
defaultTopicQueueNums:默認主題在每一個Broker隊列數量
sendMsgTimeout:發送消息默認超時時間,默認3s
compressMsgBodyOverHowmuch:消息體超過該值則啓用壓縮,默認4k
retryTimesWhenSendFailed:同步方式發送消息重試次數,默認爲2,總共執行3次
retryTimesWhenSendAsyncFailed:異步方法發送消息重試次數,默認爲2
retryAnotherBrokerWhenNotStoreOK:消息重試時選擇另外一個Broker時,是否不等待存儲結果就返回,默認爲false
maxMessageSize:允許發送的最大消息長度,默認爲4M
2.3.2 啓動流程
代碼:DefaultMQProducerImpl#start
//檢查生產者組是否滿足要求
this.checkConfig();
//更改當前instanceName爲進程ID
if (!this.defaultMQProducer.getProducerGroup().equals(MixAll.CLIENT_INNER_PRODUCER_GROUP)) {
this.defaultMQProducer.changeInstanceNameToPID();
}
//獲得MQ客戶端實例
this.mQClientFactory = MQClientManager.getInstance().getAndCreateMQClientInstance(this.defaultMQProducer, rpcHook);
整個JVM中只存在一個MQClientManager實例,維護一個MQClientInstance緩存表
ConcurrentMap<String/* clientId */, MQClientInstance> factoryTable = new ConcurrentHashMap<String,MQClientInstance>();
同一個clientId只會創建一個MQClientInstance。
MQClientInstance封裝了RocketMQ網絡處理API,是消息生產者和消息消費者與NameServer、Broker打交道的網絡通道
代碼:MQClientManager#getAndCreateMQClientInstance
public MQClientInstance getAndCreateMQClientInstance(final ClientConfig clientConfig,
RPCHook rpcHook) {
//構建客戶端ID
String clientId = clientConfig.buildMQClientId();
//根據客戶端ID或者客戶端實例
MQClientInstance instance = this.factoryTable.get(clientId);
//實例如果爲空就創建新的實例,並添加到實例表中
if (null == instance) {
instance =
new MQClientInstance(clientConfig.cloneClientConfig(),
this.factoryIndexGenerator.getAndIncrement(), clientId, rpcHook);
MQClientInstance prev = this.factoryTable.putIfAbsent(clientId, instance);
if (prev != null) {
instance = prev;
log.warn("Returned Previous MQClientInstance for clientId:[{}]", clientId);
} else {
log.info("Created new MQClientInstance for clientId:[{}]", clientId);
}
}
return instance;
}
代碼:DefaultMQProducerImpl#start
//註冊當前生產者到到MQClientInstance管理中,方便後續調用網路請求
boolean registerOK = mQClientFactory.registerProducer(this.defaultMQProducer.getProducerGroup(), this);
if (!registerOK) {
this.serviceState = ServiceState.CREATE_JUST;
throw new MQClientException("The producer group[" + this.defaultMQProducer.getProducerGroup()
+ "] has been created before, specify another name please." + FAQUrl.suggestTodo(FAQUrl.GROUP_NAME_DUPLICATE_URL),
null);
}
//啓動生產者
if (startFactory) {
mQClientFactory.start();
}
2.3.3 消息發送
代碼:DefaultMQProducerImpl#send(Message msg)
//發送消息
public SendResult send(Message msg) {
return send(msg, this.defaultMQProducer.getSendMsgTimeout());
}
代碼:DefaultMQProducerImpl#send(Message msg,long timeout)
//發送消息,默認超時時間爲3s
public SendResult send(Message msg,long timeout){
return this.sendDefaultImpl(msg, CommunicationMode.SYNC, null, timeout);
}
代碼:DefaultMQProducerImpl#sendDefaultImpl
//校驗消息
Validators.checkMessage(msg, this.defaultMQProducer);
1)驗證消息
代碼:Validators#checkMessage
public static void checkMessage(Message msg, DefaultMQProducer defaultMQProducer)
throws MQClientException {
//判斷是否爲空
if (null == msg) {
throw new MQClientException(ResponseCode.MESSAGE_ILLEGAL, "the message is null");
}
// 校驗主題
Validators.checkTopic(msg.getTopic());
// 校驗消息體
if (null == msg.getBody()) {
throw new MQClientException(ResponseCode.MESSAGE_ILLEGAL, "the message body is null");
}
if (0 == msg.getBody().length) {
throw new MQClientException(ResponseCode.MESSAGE_ILLEGAL, "the message body length is zero");
}
if (msg.getBody().length > defaultMQProducer.getMaxMessageSize()) {
throw new MQClientException(ResponseCode.MESSAGE_ILLEGAL,
"the message body size over max value, MAX: " + defaultMQProducer.getMaxMessageSize());
}
}
####2)查找路由
代碼:DefaultMQProducerImpl#tryToFindTopicPublishInfo
private TopicPublishInfo tryToFindTopicPublishInfo(final String topic) {
//從緩存中獲得主題的路由信息
TopicPublishInfo topicPublishInfo = this.topicPublishInfoTable.get(topic);
//路由信息爲空,則從NameServer獲取路由
if (null == topicPublishInfo || !topicPublishInfo.ok()) {
this.topicPublishInfoTable.putIfAbsent(topic, new TopicPublishInfo());
this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic);
topicPublishInfo = this.topicPublishInfoTable.get(topic);
}
if (topicPublishInfo.isHaveTopicRouterInfo() || topicPublishInfo.ok()) {
return topicPublishInfo;
} else {
//如果未找到當前主題的路由信息,則用默認主題繼續查找
this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic, true, this.defaultMQProducer);
topicPublishInfo = this.topicPublishInfoTable.get(topic);
return topicPublishInfo;
}
}
代碼:TopicPublishInfo
public class TopicPublishInfo {
private boolean orderTopic = false; //是否是順序消息
private boolean haveTopicRouterInfo = false;
private List<MessageQueue> messageQueueList = new ArrayList<MessageQueue>(); //該主題消息隊列
private volatile ThreadLocalIndex sendWhichQueue = new ThreadLocalIndex();//每選擇一次消息隊列,該值+1
private TopicRouteData topicRouteData;//關聯Topic路由元信息
}
代碼:MQClientInstance#updateTopicRouteInfoFromNameServer
TopicRouteData topicRouteData;
//使用默認主題從NameServer獲取路由信息
if (isDefault && defaultMQProducer != null) {
topicRouteData = this.mQClientAPIImpl.getDefaultTopicRouteInfoFromNameServer(defaultMQProducer.getCreateTopicKey(),
1000 * 3);
if (topicRouteData != null) {
for (QueueData data : topicRouteData.getQueueDatas()) {
int queueNums = Math.min(defaultMQProducer.getDefaultTopicQueueNums(), data.getReadQueueNums());
data.setReadQueueNums(queueNums);
data.setWriteQueueNums(queueNums);
}
}
} else {
//使用指定主題從NameServer獲取路由信息
topicRouteData = this.mQClientAPIImpl.getTopicRouteInfoFromNameServer(topic, 1000 * 3);
}
代碼:MQClientInstance#updateTopicRouteInfoFromNameServer
//判斷路由是否需要更改
TopicRouteData old = this.topicRouteTable.get(topic);
boolean changed = topicRouteDataIsChange(old, topicRouteData);
if (!changed) {
changed = this.isNeedUpdateTopicRouteInfo(topic);
} else {
log.info("the topic[{}] route info changed, old[{}] ,new[{}]", topic, old, topicRouteData);
}
代碼:MQClientInstance#updateTopicRouteInfoFromNameServer
if (changed) {
//將topicRouteData轉換爲發佈隊列
TopicPublishInfo publishInfo = topicRouteData2TopicPublishInfo(topic, topicRouteData);
publishInfo.setHaveTopicRouterInfo(true);
//遍歷生產
Iterator<Entry<String, MQProducerInner>> it = this.producerTable.entrySet().iterator();
while (it.hasNext()) {
Entry<String, MQProducerInner> entry = it.next();
MQProducerInner impl = entry.getValue();
if (impl != null) {
//生產者不爲空時,更新publishInfo信息
impl.updateTopicPublishInfo(topic, publishInfo);
}
}
}
代碼:MQClientInstance#topicRouteData2TopicPublishInfo
public static TopicPublishInfo topicRouteData2TopicPublishInfo(final String topic, final TopicRouteData route) {
//創建TopicPublishInfo對象
TopicPublishInfo info = new TopicPublishInfo();
//關聯topicRoute
info.setTopicRouteData(route);
//順序消息,更新TopicPublishInfo
if (route.getOrderTopicConf() != null && route.getOrderTopicConf().length() > 0) {
String[] brokers = route.getOrderTopicConf().split(";");
for (String broker : brokers) {
String[] item = broker.split(":");
int nums = Integer.parseInt(item[1]);
for (int i = 0; i < nums; i++) {
MessageQueue mq = new MessageQueue(topic, item[0], i);
info.getMessageQueueList().add(mq);
}
}
info.setOrderTopic(true);
} else {
//非順序消息更新TopicPublishInfo
List<QueueData> qds = route.getQueueDatas();
Collections.sort(qds);
//遍歷topic隊列信息
for (QueueData qd : qds) {
//是否是寫隊列
if (PermName.isWriteable(qd.getPerm())) {
BrokerData brokerData = null;
//遍歷寫隊列Broker
for (BrokerData bd : route.getBrokerDatas()) {
//根據名稱獲得讀隊列對應的Broker
if (bd.getBrokerName().equals(qd.getBrokerName())) {
brokerData = bd;
break;
}
}
if (null == brokerData) {
continue;
}
if (!brokerData.getBrokerAddrs().containsKey(MixAll.MASTER_ID)) {
continue;
}
//封裝TopicPublishInfo寫隊列
for (int i = 0; i < qd.getWriteQueueNums(); i++) {
MessageQueue mq = new MessageQueue(topic, qd.getBrokerName(), i);
info.getMessageQueueList().add(mq);
}
}
}
info.setOrderTopic(false);
}
//返回TopicPublishInfo對象
return info;
}
3)選擇隊列
- 默認不啓用Broker故障延遲機制
代碼:TopicPublishInfo#selectOneMessageQueue(lastBrokerName)
public MessageQueue selectOneMessageQueue(final String lastBrokerName) {
//第一次選擇隊列
if (lastBrokerName == null) {
return selectOneMessageQueue();
} else {
//sendWhichQueue
int index = this.sendWhichQueue.getAndIncrement();
//遍歷消息隊列集合
for (int i = 0; i < this.messageQueueList.size(); i++) {
//sendWhichQueue自增後取模
int pos = Math.abs(index++) % this.messageQueueList.size();
if (pos < 0)
pos = 0;
//規避上次Broker隊列
MessageQueue mq = this.messageQueueList.get(pos);
if (!mq.getBrokerName().equals(lastBrokerName)) {
return mq;
}
}
//如果以上情況都不滿足,返回sendWhichQueue取模後的隊列
return selectOneMessageQueue();
}
}
代碼:TopicPublishInfo#selectOneMessageQueue()
//第一次選擇隊列
public MessageQueue selectOneMessageQueue() {
//sendWhichQueue自增
int index = this.sendWhichQueue.getAndIncrement();
//對隊列大小取模
int pos = Math.abs(index) % this.messageQueueList.size();
if (pos < 0)
pos = 0;
//返回對應的隊列
return this.messageQueueList.get(pos);
}
- 啓用Broker故障延遲機制
public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName) {
//Broker故障延遲機制
if (this.sendLatencyFaultEnable) {
try {
//對sendWhichQueue自增
int index = tpInfo.getSendWhichQueue().getAndIncrement();
//對消息隊列輪詢獲取一個隊列
for (int i = 0; i < tpInfo.getMessageQueueList().size(); i++) {
int pos = Math.abs(index++) % tpInfo.getMessageQueueList().size();
if (pos < 0)
pos = 0;
MessageQueue mq = tpInfo.getMessageQueueList().get(pos);
//驗證該隊列是否可用
if (latencyFaultTolerance.isAvailable(mq.getBrokerName())) {
//可用
if (null == lastBrokerName || mq.getBrokerName().equals(lastBrokerName))
return mq;
}
}
//從規避的Broker中選擇一個可用的Broker
final String notBestBroker = latencyFaultTolerance.pickOneAtLeast();
//獲得Broker的寫隊列集合
int writeQueueNums = tpInfo.getQueueIdByBroker(notBestBroker);
if (writeQueueNums > 0) {
//獲得一個隊列,指定broker和隊列ID並返回
final MessageQueue mq = tpInfo.selectOneMessageQueue();
if (notBestBroker != null) {
mq.setBrokerName(notBestBroker);
mq.setQueueId(tpInfo.getSendWhichQueue().getAndIncrement() % writeQueueNums);
}
return mq;
} else {
latencyFaultTolerance.remove(notBestBroker);
}
} catch (Exception e) {
log.error("Error occurred when selecting message queue", e);
}
return tpInfo.selectOneMessageQueue();
}
return tpInfo.selectOneMessageQueue(lastBrokerName);
}
- 延遲機制接口規範
public interface LatencyFaultTolerance<T> {
//更新失敗條目
void updateFaultItem(final T name, final long currentLatency, final long notAvailableDuration);
//判斷Broker是否可用
boolean isAvailable(final T name);
//移除Fault條目
void remove(final T name);
//嘗試從規避的Broker中選擇一個可用的Broker
T pickOneAtLeast();
}
- FaultItem:失敗條目
class FaultItem implements Comparable<FaultItem> {
//條目唯一鍵,這裏爲brokerName
private final String name;
//本次消息發送延遲
private volatile long currentLatency;
//故障規避開始時間
private volatile long startTimestamp;
}
- 消息失敗策略
public class MQFaultStrategy {
//根據currentLatency本地消息發送延遲,從latencyMax尾部向前找到第一個比currentLatency小的索引,如果沒有找到,返回0
private long[] latencyMax = {50L, 100L, 550L, 1000L, 2000L, 3000L, 15000L};
//根據這個索引從notAvailableDuration取出對應的時間,在該時長內,Broker設置爲不可用
private long[] notAvailableDuration = {0L, 0L, 30000L, 60000L, 120000L, 180000L, 600000L};
}
原理分析
代碼:DefaultMQProducerImpl#sendDefaultImpl
sendResult = this.sendKernelImpl(msg,
mq,
communicationMode,
sendCallback,
topicPublishInfo,
timeout - costTime);
endTimestamp = System.currentTimeMillis();
this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false);
如果上述發送過程出現異常,則調用DefaultMQProducerImpl#updateFaultItem
public void updateFaultItem(final String brokerName, final long currentLatency, boolean isolation) {
//參數一:broker名稱
//參數二:本次消息發送延遲時間
//參數三:是否隔離
this.mqFaultStrategy.updateFaultItem(brokerName, currentLatency, isolation);
}
代碼:MQFaultStrategy#updateFaultItem
public void updateFaultItem(final String brokerName, final long currentLatency, boolean isolation) {
if (this.sendLatencyFaultEnable) {
//計算broker規避的時長
long duration = computeNotAvailableDuration(isolation ? 30000 : currentLatency);
//更新該FaultItem規避時長
this.latencyFaultTolerance.updateFaultItem(brokerName, currentLatency, duration);
}
}
代碼:MQFaultStrategy#computeNotAvailableDuration
private long computeNotAvailableDuration(final long currentLatency) {
//遍歷latencyMax
for (int i = latencyMax.length - 1; i >= 0; i--) {
//找到第一個比currentLatency的latencyMax值
if (currentLatency >= latencyMax[i])
return this.notAvailableDuration[i];
}
//沒有找到則返回0
return 0;
}
代碼:LatencyFaultToleranceImpl#updateFaultItem
public void updateFaultItem(final String name, final long currentLatency, final long notAvailableDuration) {
//獲得原FaultItem
FaultItem old = this.faultItemTable.get(name);
//爲空新建faultItem對象,設置規避時長和開始時間
if (null == old) {
final FaultItem faultItem = new FaultItem(name);
faultItem.setCurrentLatency(currentLatency);
faultItem.setStartTimestamp(System.currentTimeMillis() + notAvailableDuration);
old = this.faultItemTable.putIfAbsent(name, faultItem);
if (old != null) {
old.setCurrentLatency(currentLatency);
old.setStartTimestamp(System.currentTimeMillis() + notAvailableDuration);
}
} else {
//更新規避時長和開始時間
old.setCurrentLatency(currentLatency);
old.setStartTimestamp(System.currentTimeMillis() + notAvailableDuration);
}
}
4)發送消息
消息發送API核心入口***DefaultMQProducerImpl#sendKernelImpl***
private SendResult sendKernelImpl(
final Message msg, //待發送消息
final MessageQueue mq, //消息發送隊列
final CommunicationMode communicationMode, //消息發送內模式
final SendCallback sendCallback, pp //異步消息回調函數
final TopicPublishInfo topicPublishInfo, //主題路由信息
final long timeout //超時時間
)
代碼:DefaultMQProducerImpl#sendKernelImpl
//獲得broker網絡地址信息
String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(mq.getBrokerName());
if (null == brokerAddr) {
//沒有找到從NameServer更新broker網絡地址信息
tryToFindTopicPublishInfo(mq.getTopic());
brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(mq.getBrokerName());
}
//爲消息分類唯一ID
if (!(msg instanceof MessageBatch)) {
MessageClientIDSetter.setUniqID(msg);
}
boolean topicWithNamespace = false;
if (null != this.mQClientFactory.getClientConfig().getNamespace()) {
msg.setInstanceId(this.mQClientFactory.getClientConfig().getNamespace());
topicWithNamespace = true;
}
//消息大小超過4K,啓用消息壓縮
int sysFlag = 0;
boolean msgBodyCompressed = false;
if (this.tryToCompressMessage(msg)) {
sysFlag |= MessageSysFlag.COMPRESSED_FLAG;
msgBodyCompressed = true;
}
//如果是事務消息,設置消息標記MessageSysFlag.TRANSACTION_PREPARED_TYPE
final String tranMsg = msg.getProperty(MessageConst.PROPERTY_TRANSACTION_PREPARED);
if (tranMsg != null && Boolean.parseBoolean(tranMsg)) {
sysFlag |= MessageSysFlag.TRANSACTION_PREPARED_TYPE;
}
//如果註冊了消息發送鉤子函數,在執行消息發送前的增強邏輯
if (this.hasSendMessageHook()) {
context = new SendMessageContext();
context.setProducer(this);
context.setProducerGroup(this.defaultMQProducer.getProducerGroup());
context.setCommunicationMode(communicationMode);
context.setBornHost(this.defaultMQProducer.getClientIP());
context.setBrokerAddr(brokerAddr);
context.setMessage(msg);
context.setMq(mq);
context.setNamespace(this.defaultMQProducer.getNamespace());
String isTrans = msg.getProperty(MessageConst.PROPERTY_TRANSACTION_PREPARED);
if (isTrans != null && isTrans.equals("true")) {
context.setMsgType(MessageType.Trans_Msg_Half);
}
if (msg.getProperty("__STARTDELIVERTIME") != null || msg.getProperty(MessageConst.PROPERTY_DELAY_TIME_LEVEL) != null) {
context.setMsgType(MessageType.Delay_Msg);
}
this.executeSendMessageHookBefore(context);
}
代碼:SendMessageHook
public interface SendMessageHook {
String hookName();
void sendMessageBefore(final SendMessageContext context);
void sendMessageAfter(final SendMessageContext context);
}
代碼:DefaultMQProducerImpl#sendKernelImpl
//構建消息發送請求包
SendMessageRequestHeader requestHeader = new SendMessageRequestHeader();
//生產者組
requestHeader.setProducerGroup(this.defaultMQProducer.getProducerGroup());
//主題
requestHeader.setTopic(msg.getTopic());
//默認創建主題Key
requestHeader.setDefaultTopic(this.defaultMQProducer.getCreateTopicKey());
//該主題在單個Broker默認隊列樹
requestHeader.setDefaultTopicQueueNums(this.defaultMQProducer.getDefaultTopicQueueNums());
//隊列ID
requestHeader.setQueueId(mq.getQueueId());
//消息系統標記
requestHeader.setSysFlag(sysFlag);
//消息發送時間
requestHeader.setBornTimestamp(System.currentTimeMillis());
//消息標記
requestHeader.setFlag(msg.getFlag());
//消息擴展信息
requestHeader.setProperties(MessageDecoder.messageProperties2String(msg.getProperties()));
//消息重試次數
requestHeader.setReconsumeTimes(0);
requestHeader.setUnitMode(this.isUnitMode());
//是否是批量消息等
requestHeader.setBatch(msg instanceof MessageBatch);
if (requestHeader.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
String reconsumeTimes = MessageAccessor.getReconsumeTime(msg);
if (reconsumeTimes != null) {
requestHeader.setReconsumeTimes(Integer.valueOf(reconsumeTimes));
MessageAccessor.clearProperty(msg, MessageConst.PROPERTY_RECONSUME_TIME);
}
String maxReconsumeTimes = MessageAccessor.getMaxReconsumeTimes(msg);
if (maxReconsumeTimes != null) {
requestHeader.setMaxReconsumeTimes(Integer.valueOf(maxReconsumeTimes));
MessageAccessor.clearProperty(msg, MessageConst.PROPERTY_MAX_RECONSUME_TIMES);
}
}
case ASYNC: //異步發送
Message tmpMessage = msg;
boolean messageCloned = false;
if (msgBodyCompressed) {
//If msg body was compressed, msgbody should be reset using prevBody.
//Clone new message using commpressed message body and recover origin massage.
//Fix bug:https://github.com/apache/rocketmq-externals/issues/66
tmpMessage = MessageAccessor.cloneMessage(msg);
messageCloned = true;
msg.setBody(prevBody);
}
if (topicWithNamespace) {
if (!messageCloned) {
tmpMessage = MessageAccessor.cloneMessage(msg);
messageCloned = true;
}
msg.setTopic(NamespaceUtil.withoutNamespace(msg.getTopic(),
this.defaultMQProducer.getNamespace()));
}
long costTimeAsync = System.currentTimeMillis() - beginStartTime;
if (timeout < costTimeAsync) {
throw new RemotingTooMuchRequestException("sendKernelImpl call timeout");
}
sendResult = this.mQClientFactory.getMQClientAPIImpl().sendMessage(
brokerAddr,
mq.getBrokerName(),
tmpMessage,
requestHeader,
timeout - costTimeAsync,
communicationMode,
sendCallback,
topicPublishInfo,
this.mQClientFactory,
this.defaultMQProducer.getRetryTimesWhenSendAsyncFailed(),
context,
this);
break;
case ONEWAY:
case SYNC: //同步發送
long costTimeSync = System.currentTimeMillis() - beginStartTime;
if (timeout < costTimeSync) {
throw new RemotingTooMuchRequestException("sendKernelImpl call timeout");
}
sendResult = this.mQClientFactory.getMQClientAPIImpl().sendMessage(
brokerAddr,
mq.getBrokerName(),
msg,
requestHeader,
timeout - costTimeSync,
communicationMode,
context,
this);
break;
default:
assert false;
break;
}
//如果註冊了鉤子函數,則發送完畢後執行鉤子函數
if (this.hasSendMessageHook()) {
context.setSendResult(sendResult);
this.executeSendMessageHookAfter(context);
}
2.3.4 批量消息發送
批量消息發送是將同一個主題的多條消息一起打包發送到消息服務端,減少網絡調用次數,提高網絡傳輸效率。當然,並不是在同一批次中發送的消息數量越多越好,其判斷依據是單條消息的長度,如果單條消息內容比較長,則打包多條消息發送會影響其他線程發送消息的響應時間,並且單批次消息總長度不能超過DefaultMQProducer#maxMessageSize。
批量消息發送要解決的問題是如何將這些消息編碼以便服務端能夠正確解碼出每條消息的消息內容。
代碼:DefaultMQProducer#send
public SendResult send(Collection<Message> msgs)
throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
//壓縮消息集合成一條消息,然後發送出去
return this.defaultMQProducerImpl.send(batch(msgs));
}
代碼:DefaultMQProducer#batch
private MessageBatch batch(Collection<Message> msgs) throws MQClientException {
MessageBatch msgBatch;
try {
//將集合消息封裝到MessageBatch
msgBatch = MessageBatch.generateFromList(msgs);
//遍歷消息集合,檢查消息合法性,設置消息ID,設置Topic
for (Message message : msgBatch) {
Validators.checkMessage(message, this);
MessageClientIDSetter.setUniqID(message);
message.setTopic(withNamespace(message.getTopic()));
}
//壓縮消息,設置消息body
msgBatch.setBody(msgBatch.encode());
} catch (Exception e) {
throw new MQClientException("Failed to initiate the MessageBatch", e);
}
//設置msgBatch的topic
msgBatch.setTopic(withNamespace(msgBatch.getTopic()));
return msgBatch;
}
2.4 消息存儲
2.4.1 消息存儲核心類
private final MessageStoreConfig messageStoreConfig; //消息配置屬性
private final CommitLog commitLog; //CommitLog文件存儲的實現類
private final ConcurrentMap<String/* topic */, ConcurrentMap<Integer/* queueId */, ConsumeQueue>> consumeQueueTable; //消息隊列存儲緩存表,按照消息主題分組
private final FlushConsumeQueueService flushConsumeQueueService; //消息隊列文件刷盤線程
private final CleanCommitLogService cleanCommitLogService; //清除CommitLog文件服務
private final CleanConsumeQueueService cleanConsumeQueueService; //清除ConsumerQueue隊列文件服務
private final IndexService indexService; //索引實現類
private final AllocateMappedFileService allocateMappedFileService; //MappedFile分配服務
private final ReputMessageService reputMessageService;//CommitLog消息分發,根據CommitLog文件構建ConsumerQueue、IndexFile文件
private final HAService haService; //存儲HA機制
private final ScheduleMessageService scheduleMessageService; //消息服務調度線程
private final StoreStatsService storeStatsService; //消息存儲服務
private final TransientStorePool transientStorePool; //消息堆外內存緩存
private final BrokerStatsManager brokerStatsManager; //Broker狀態管理器
private final MessageArrivingListener messageArrivingListener; //消息拉取長輪詢模式消息達到監聽器
private final BrokerConfig brokerConfig; //Broker配置類
private StoreCheckpoint storeCheckpoint; //文件刷盤監測點
private final LinkedList<CommitLogDispatcher> dispatcherList; //CommitLog文件轉發請求
2.4.2 消息存儲流程
消息存儲入口:DefaultMessageStore#putMessage
//判斷Broker角色如果是從節點,則無需寫入
if (BrokerRole.SLAVE == this.messageStoreConfig.getBrokerRole()) {
long value = this.printTimes.getAndIncrement();
if ((value % 50000) == 0) {
log.warn("message store is slave mode, so putMessage is forbidden ");
}
return new PutMessageResult(PutMessageStatus.SERVICE_NOT_AVAILABLE, null);
}
//判斷當前寫入狀態如果是正在寫入,則不能繼續
if (!this.runningFlags.isWriteable()) {
long value = this.printTimes.getAndIncrement();
return new PutMessageResult(PutMessageStatus.SERVICE_NOT_AVAILABLE, null);
} else {
this.printTimes.set(0);
}
//判斷消息主題長度是否超過最大限制
if (msg.getTopic().length() > Byte.MAX_VALUE) {
log.warn("putMessage message topic length too long " + msg.getTopic().length());
return new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, null);
}
//判斷消息屬性長度是否超過限制
if (msg.getPropertiesString() != null && msg.getPropertiesString().length() > Short.MAX_VALUE) {
log.warn("putMessage message properties length too long " + msg.getPropertiesString().length());
return new PutMessageResult(PutMessageStatus.PROPERTIES_SIZE_EXCEEDED, null);
}
//判斷系統PageCache緩存去是否佔用
if (this.isOSPageCacheBusy()) {
return new PutMessageResult(PutMessageStatus.OS_PAGECACHE_BUSY, null);
}
//將消息寫入CommitLog文件
PutMessageResult result = this.commitLog.putMessage(msg);
代碼:CommitLog#putMessage
//記錄消息存儲時間
msg.setStoreTimestamp(beginLockTimestamp);
//判斷如果mappedFile如果爲空或者已滿,創建新的mappedFile文件
if (null == mappedFile || mappedFile.isFull()) {
mappedFile = this.mappedFileQueue.getLastMappedFile(0);
}
//如果創建失敗,直接返回
if (null == mappedFile) {
log.error("create mapped file1 error, topic: " + msg.getTopic() + " clientAddr: " + msg.getBornHostString());
beginTimeInLock = 0;
return new PutMessageResult(PutMessageStatus.CREATE_MAPEDFILE_FAILED, null);
}
//寫入消息到mappedFile中
result = mappedFile.appendMessage(msg, this.appendMessageCallback);
代碼:MappedFile#appendMessagesInner
//獲得文件的寫入指針
int currentPos = this.wrotePosition.get();
//如果指針大於文件大小則直接返回
if (currentPos < this.fileSize) {
//通過writeBuffer.slice()創建一個與MappedFile共享的內存區,並設置position爲當前指針
ByteBuffer byteBuffer = writeBuffer != null ? writeBuffer.slice() : this.mappedByteBuffer.slice();
byteBuffer.position(currentPos);
AppendMessageResult result = null;
if (messageExt instanceof MessageExtBrokerInner) {
//通過回調方法寫入
result = cb.doAppend(this.getFileFromOffset(), byteBuffer, this.fileSize - currentPos, (MessageExtBrokerInner) messageExt);
} else if (messageExt instanceof MessageExtBatch) {
result = cb.doAppend(this.getFileFromOffset(), byteBuffer, this.fileSize - currentPos, (MessageExtBatch) messageExt);
} else {
return new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR);
}
this.wrotePosition.addAndGet(result.getWroteBytes());
this.storeTimestamp = result.getStoreTimestamp();
return result;
}
代碼:CommitLog#doAppend
//文件寫入位置
long wroteOffset = fileFromOffset + byteBuffer.position();
//設置消息ID
this.resetByteBuffer(hostHolder, 8);
String msgId = MessageDecoder.createMessageId(this.msgIdMemory, msgInner.getStoreHostBytes(hostHolder), wroteOffset);
//獲得該消息在消息隊列中的偏移量
keyBuilder.setLength(0);
keyBuilder.append(msgInner.getTopic());
keyBuilder.append('-');
keyBuilder.append(msgInner.getQueueId());
String key = keyBuilder.toString();
Long queueOffset = CommitLog.this.topicQueueTable.get(key);
if (null == queueOffset) {
queueOffset = 0L;
CommitLog.this.topicQueueTable.put(key, queueOffset);
}
//獲得消息屬性長度
final byte[] propertiesData =msgInner.getPropertiesString() == null ? null : msgInner.getPropertiesString().getBytes(MessageDecoder.CHARSET_UTF8);
final int propertiesLength = propertiesData == null ? 0 : propertiesData.length;
if (propertiesLength > Short.MAX_VALUE) {
log.warn("putMessage message properties length too long. length={}", propertiesData.length);
return new AppendMessageResult(AppendMessageStatus.PROPERTIES_SIZE_EXCEEDED);
}
//獲得消息主題大小
final byte[] topicData = msgInner.getTopic().getBytes(MessageDecoder.CHARSET_UTF8);
final int topicLength = topicData.length;
//獲得消息體大小
final int bodyLength = msgInner.getBody() == null ? 0 : msgInner.getBody().length;
//計算消息總長度
final int msgLen = calMsgLength(bodyLength, topicLength, propertiesLength);
代碼:CommitLog#calMsgLength
protected static int calMsgLength(int bodyLength, int topicLength, int propertiesLength) {
final int msgLen = 4 //TOTALSIZE
+ 4 //MAGICCODE
+ 4 //BODYCRC
+ 4 //QUEUEID
+ 4 //FLAG
+ 8 //QUEUEOFFSET
+ 8 //PHYSICALOFFSET
+ 4 //SYSFLAG
+ 8 //BORNTIMESTAMP
+ 8 //BORNHOST
+ 8 //STORETIMESTAMP
+ 8 //STOREHOSTADDRESS
+ 4 //RECONSUMETIMES
+ 8 //Prepared Transaction Offset
+ 4 + (bodyLength > 0 ? bodyLength : 0) //BODY
+ 1 + topicLength //TOPIC
+ 2 + (propertiesLength > 0 ? propertiesLength : 0) //propertiesLength
+ 0;
return msgLen;
}
代碼:CommitLog#doAppend
//消息長度不能超過4M
if (msgLen > this.maxMessageSize) {
CommitLog.log.warn("message size exceeded, msg total size: " + msgLen + ", msg body size: " + bodyLength
+ ", maxMessageSize: " + this.maxMessageSize);
return new AppendMessageResult(AppendMessageStatus.MESSAGE_SIZE_EXCEEDED);
}
//消息是如果沒有足夠的存儲空間則新創建CommitLog文件
if ((msgLen + END_FILE_MIN_BLANK_LENGTH) > maxBlank) {
this.resetByteBuffer(this.msgStoreItemMemory, maxBlank);
// 1 TOTALSIZE
this.msgStoreItemMemory.putInt(maxBlank);
// 2 MAGICCODE
this.msgStoreItemMemory.putInt(CommitLog.BLANK_MAGIC_CODE);
// 3 The remaining space may be any value
// Here the length of the specially set maxBlank
final long beginTimeMills = CommitLog.this.defaultMessageStore.now();
byteBuffer.put(this.msgStoreItemMemory.array(), 0, maxBlank);
return new AppendMessageResult(AppendMessageStatus.END_OF_FILE, wroteOffset, maxBlank, msgId, msgInner.getStoreTimestamp(),
queueOffset, CommitLog.this.defaultMessageStore.now() - beginTimeMills);
}
//將消息存儲到ByteBuffer中,返回AppendMessageResult
final long beginTimeMills = CommitLog.this.defaultMessageStore.now();
// Write messages to the queue buffer
byteBuffer.put(this.msgStoreItemMemory.array(), 0, msgLen);
AppendMessageResult result = new AppendMessageResult(AppendMessageStatus.PUT_OK, wroteOffset,
msgLen, msgId,msgInner.getStoreTimestamp(),
queueOffset,
CommitLog.this.defaultMessageStore.now()
-beginTimeMills);
switch (tranType) {
case MessageSysFlag.TRANSACTION_PREPARED_TYPE:
case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE:
break;
case MessageSysFlag.TRANSACTION_NOT_TYPE:
case MessageSysFlag.TRANSACTION_COMMIT_TYPE:
//更新消息隊列偏移量
CommitLog.this.topicQueueTable.put(key, ++queueOffset);
break;
default:
break;
}
代碼:CommitLog#putMessage
//釋放鎖
putMessageLock.unlock();
//刷盤
handleDiskFlush(result, putMessageResult, msg);
//執行HA主從同步
handleHA(result, putMessageResult, msg);
2.4.3 存儲文件
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-o6q8fvcN-1593423765112)(…/%E6%96%87%E6%A1%A3/img/%E5%AD%98%E5%82%A8%E6%96%87%E4%BB%B6.png)]
- commitLog:消息存儲目錄
- config:運行期間一些配置信息
- consumerqueue:消息消費隊列存儲目錄
- index:消息索引文件存儲目錄
- abort:如果存在改文件壽命Broker非正常關閉
- checkpoint:文件檢查點,存儲CommitLog文件最後一次刷盤時間戳、consumerquueue最後一次刷盤時間,index索引文件最後一次刷盤時間戳。
2.4.4 存儲文件內存映射
RocketMQ通過使用內存映射文件提高IO訪問性能,無論是CommitLog、ConsumerQueue還是IndexFile,單個文件都被設計爲固定長度,如果一個文件寫滿以後再創建一個新文件,文件名就爲該文件第一條消息對應的全局物理偏移量。
1)MappedFileQueue
String storePath; //存儲目錄
int mappedFileSize; // 單個文件大小
CopyOnWriteArrayList<MappedFile> mappedFiles; //MappedFile文件集合
AllocateMappedFileService allocateMappedFileService; //創建MapFile服務類
long flushedWhere = 0; //當前刷盤指針
long committedWhere = 0; //當前數據提交指針,內存中ByteBuffer當前的寫指針,該值大於等於flushWhere
- 根據存儲時間查詢MappedFile
public MappedFile getMappedFileByTime(final long timestamp) {
Object[] mfs = this.copyMappedFiles(0);
if (null == mfs)
return null;
//遍歷MappedFile文件數組
for (int i = 0; i < mfs.length; i++) {
MappedFile mappedFile = (MappedFile) mfs[i];
//MappedFile文件的最後修改時間大於指定時間戳則返回該文件
if (mappedFile.getLastModifiedTimestamp() >= timestamp) {
return mappedFile;
}
}
return (MappedFile) mfs[mfs.length - 1];
}
- 根據消息偏移量offset查找MappedFile
public MappedFile findMappedFileByOffset(final long offset, final boolean returnFirstOnNotFound) {
try {
//獲得第一個MappedFile文件
MappedFile firstMappedFile = this.getFirstMappedFile();
//獲得最後一個MappedFile文件
MappedFile lastMappedFile = this.getLastMappedFile();
//第一個文件和最後一個文件均不爲空,則進行處理
if (firstMappedFile != null && lastMappedFile != null) {
if (offset < firstMappedFile.getFileFromOffset() ||
offset >= lastMappedFile.getFileFromOffset() + this.mappedFileSize) {
} else {
//獲得文件索引
int index = (int) ((offset / this.mappedFileSize)
- (firstMappedFile.getFileFromOffset() / this.mappedFileSize));
MappedFile targetFile = null;
try {
//根據索引返回目標文件
targetFile = this.mappedFiles.get(index);
} catch (Exception ignored) {
}
if (targetFile != null && offset >= targetFile.getFileFromOffset()
&& offset < targetFile.getFileFromOffset() + this.mappedFileSize) {
return targetFile;
}
for (MappedFile tmpMappedFile : this.mappedFiles) {
if (offset >= tmpMappedFile.getFileFromOffset()
&& offset < tmpMappedFile.getFileFromOffset() + this.mappedFileSize) {
return tmpMappedFile;
}
}
}
if (returnFirstOnNotFound) {
return firstMappedFile;
}
}
} catch (Exception e) {
log.error("findMappedFileByOffset Exception", e);
}
return null;
}
- 獲取存儲文件最小偏移量
public long getMinOffset() {
if (!this.mappedFiles.isEmpty()) {
try {
return this.mappedFiles.get(0).getFileFromOffset();
} catch (IndexOutOfBoundsException e) {
//continue;
} catch (Exception e) {
log.error("getMinOffset has exception.", e);
}
}
return -1;
}
- 獲取存儲文件最大偏移量
public long getMaxOffset() {
MappedFile mappedFile = getLastMappedFile();
if (mappedFile != null) {
return mappedFile.getFileFromOffset() + mappedFile.getReadPosition();
}
return 0;
}
- 返回存儲文件當前寫指針
public long getMaxWrotePosition() {
MappedFile mappedFile = getLastMappedFile();
if (mappedFile != null) {
return mappedFile.getFileFromOffset() + mappedFile.getWrotePosition();
}
return 0;
}
2)MappedFile
int OS_PAGE_SIZE = 1024 * 4; //操作系統每頁大小,默認4K
AtomicLong TOTAL_MAPPED_VIRTUAL_MEMORY = new AtomicLong(0); //當前JVM實例中MappedFile虛擬內存
AtomicInteger TOTAL_MAPPED_FILES = new AtomicInteger(0); //當前JVM實例中MappedFile對象個數
AtomicInteger wrotePosition = new AtomicInteger(0); //當前文件的寫指針
AtomicInteger committedPosition = new AtomicInteger(0); //當前文件的提交指針
AtomicInteger flushedPosition = new AtomicInteger(0); //刷寫到磁盤指針
int fileSize; //文件大小
FileChannel fileChannel; //文件通道
ByteBuffer writeBuffer = null; //堆外內存ByteBuffer
TransientStorePool transientStorePool = null; //堆外內存池
String fileName; //文件名稱
long fileFromOffset; //該文件的處理偏移量
File file; //物理文件
MappedByteBuffer mappedByteBuffer; //物理文件對應的內存映射Buffer
volatile long storeTimestamp = 0; //文件最後一次內容寫入時間
boolean firstCreateInQueue = false; //是否是MappedFileQueue隊列中第一個文件
MappedFile初始化
- 未開啓
transientStorePoolEnable
。transientStorePoolEnable=true
爲true
表示數據先存儲到堆外內存,然後通過Commit
線程將數據提交到內存映射Buffer中,再通過Flush
線程將內存映射Buffer
中數據持久化磁盤。
private void init(final String fileName, final int fileSize) throws IOException {
this.fileName = fileName;
this.fileSize = fileSize;
this.file = new File(fileName);
this.fileFromOffset = Long.parseLong(this.file.getName());
boolean ok = false;
ensureDirOK(this.file.getParent());
try {
this.fileChannel = new RandomAccessFile(this.file, "rw").getChannel();
this.mappedByteBuffer = this.fileChannel.map(MapMode.READ_WRITE, 0, fileSize);
TOTAL_MAPPED_VIRTUAL_MEMORY.addAndGet(fileSize);
TOTAL_MAPPED_FILES.incrementAndGet();
ok = true;
} catch (FileNotFoundException e) {
log.error("create file channel " + this.fileName + " Failed. ", e);
throw e;
} catch (IOException e) {
log.error("map file " + this.fileName + " Failed. ", e);
throw e;
} finally {
if (!ok && this.fileChannel != null) {
this.fileChannel.close();
}
}
}
開啓transientStorePoolEnable
public void init(final String fileName, final int fileSize,
final TransientStorePool transientStorePool) throws IOException {
init(fileName, fileSize);
this.writeBuffer = transientStorePool.borrowBuffer(); //初始化writeBuffer
this.transientStorePool = transientStorePool;
}
MappedFile提交
提交數據到FileChannel,commitLeastPages爲本次提交最小的頁數,如果待提交數據不滿commitLeastPages,則不執行本次提交操作。如果writeBuffer如果爲空,直接返回writePosition指針,無需執行commit操作,表名commit操作主體是writeBuffer。
public int commit(final int commitLeastPages) {
if (writeBuffer == null) {
//no need to commit data to file channel, so just regard wrotePosition as committedPosition.
return this.wrotePosition.get();
}
//判斷是否滿足提交條件
if (this.isAbleToCommit(commitLeastPages)) {
if (this.hold()) {
commit0(commitLeastPages);
this.release();
} else {
log.warn("in commit, hold failed, commit offset = " + this.committedPosition.get());
}
}
// 所有數據提交後,清空緩衝區
if (writeBuffer != null && this.transientStorePool != null && this.fileSize == this.committedPosition.get()) {
this.transientStorePool.returnBuffer(writeBuffer);
this.writeBuffer = null;
}
return this.committedPosition.get();
}
MappedFile#isAbleToCommit
判斷是否執行commit操作,如果文件已滿返回true;如果commitLeastpages大於0,則比較writePosition與上一次提交的指針commitPosition的差值,除以OS_PAGE_SIZE得到當前髒頁的數量,如果大於commitLeastPages則返回true,如果commitLeastpages小於0表示只要存在髒頁就提交。
protected boolean isAbleToCommit(final int commitLeastPages) {
//已經刷盤指針
int flush = this.committedPosition.get();
//文件寫指針
int write = this.wrotePosition.get();
//寫滿刷盤
if (this.isFull()) {
return true;
}
if (commitLeastPages > 0) {
//文件內容達到commitLeastPages頁數,則刷盤
return ((write / OS_PAGE_SIZE) - (flush / OS_PAGE_SIZE)) >= commitLeastPages;
}
return write > flush;
}
MappedFile#commit0
具體提交的實現,首先創建WriteBuffer區共享緩存區,然後將新創建的position回退到上一次提交的位置(commitPosition),設置limit爲wrotePosition(當前最大有效數據指針),然後把commitPosition到wrotePosition的數據寫入到FileChannel中,然後更新committedPosition指針爲wrotePosition。commit的作用就是將MappedFile的writeBuffer中數據提交到文件通道FileChannel中。
protected void commit0(final int commitLeastPages) {
//寫指針
int writePos = this.wrotePosition.get();
//上次提交指針
int lastCommittedPosition = this.committedPosition.get();
if (writePos - this.committedPosition.get() > 0) {
try {
//複製共享內存區域
ByteBuffer byteBuffer = writeBuffer.slice();
//設置提交位置是上次提交位置
byteBuffer.position(lastCommittedPosition);
//最大提交數量
byteBuffer.limit(writePos);
//設置fileChannel位置爲上次提交位置
this.fileChannel.position(lastCommittedPosition);
//將lastCommittedPosition到writePos的數據複製到FileChannel中
this.fileChannel.write(byteBuffer);
//重置提交位置
this.committedPosition.set(writePos);
} catch (Throwable e) {
log.error("Error occurred when commit data to FileChannel.", e);
}
}
}
MappedFile#flush
刷寫磁盤,直接調用MappedByteBuffer或fileChannel的force方法將內存中的數據持久化到磁盤,那麼flushedPosition應該等於MappedByteBuffer中的寫指針;如果writeBuffer不爲空,則flushPosition應該等於上一次的commit指針;因爲上一次提交的數據就是進入到MappedByteBuffer中的數據;如果writeBuffer爲空,數據時直接進入到MappedByteBuffer,wrotePosition代表的是MappedByteBuffer中的指針,故設置flushPosition爲wrotePosition。
public int flush(final int flushLeastPages) {
//數據達到刷盤條件
if (this.isAbleToFlush(flushLeastPages)) {
//加鎖,同步刷盤
if (this.hold()) {
//獲得讀指針
int value = getReadPosition();
try {
//數據從writeBuffer提交數據到fileChannel再刷新到磁盤
if (writeBuffer != null || this.fileChannel.position() != 0) {
this.fileChannel.force(false);
} else {
//從mmap刷新數據到磁盤
this.mappedByteBuffer.force();
}
} catch (Throwable e) {
log.error("Error occurred when force data to disk.", e);
}
//更新刷盤位置
this.flushedPosition.set(value);
this.release();
} else {
log.warn("in flush, hold failed, flush offset = " + this.flushedPosition.get());
this.flushedPosition.set(getReadPosition());
}
}
return this.getFlushedPosition();
}
MappedFile#getReadPosition
獲取當前文件最大可讀指針。如果writeBuffer爲空,則直接返回當前的寫指針;如果writeBuffer不爲空,則返回上一次提交的指針。在MappedFile設置中,只有提交了的數據(寫入到MappedByteBuffer或FileChannel中的數據)纔是安全的數據
public int getReadPosition() {
//如果writeBuffer爲空,刷盤的位置就是應該等於上次commit的位置,如果爲空則爲mmap的寫指針
return this.writeBuffer == null ? this.wrotePosition.get() : this.committedPosition.get();
}
MappedFile#selectMappedBuffer
查找pos到當前最大可讀之間的數據,由於在整個寫入期間都未曾改MappedByteBuffer的指針,如果mappedByteBuffer.slice()方法返回的共享緩存區空間爲整個MappedFile,然後通過設置ByteBuffer的position爲待查找的值,讀取字節長度當前可讀最大長度,最終返回的ByteBuffer的limit爲size。整個共享緩存區的容量爲(MappedFile#fileSize-pos)。故在操作SelectMappedBufferResult不能對包含在裏面的ByteBuffer調用filp方法。
public SelectMappedBufferResult selectMappedBuffer(int pos) {
//獲得最大可讀指針
int readPosition = getReadPosition();
//pos小於最大可讀指針,並且大於0
if (pos < readPosition && pos >= 0) {
if (this.hold()) {
//複製mappedByteBuffer讀共享區
ByteBuffer byteBuffer = this.mappedByteBuffer.slice();
//設置讀指針位置
byteBuffer.position(pos);
//獲得可讀範圍
int size = readPosition - pos;
//設置最大刻度範圍
ByteBuffer byteBufferNew = byteBuffer.slice();
byteBufferNew.limit(size);
return new SelectMappedBufferResult(this.fileFromOffset + pos, byteBufferNew, size, this);
}
}
return null;
}
MappedFile#shutdown
MappedFile文件銷燬的實現方法爲public boolean destory(long intervalForcibly),intervalForcibly表示拒絕被銷燬的最大存活時間。
public void shutdown(final long intervalForcibly) {
if (this.available) {
//關閉MapedFile
this.available = false;
//設置當前關閉時間戳
this.firstShutdownTimestamp = System.currentTimeMillis();
//釋放資源
this.release();
} else if (this.getRefCount() > 0) {
if ((System.currentTimeMillis() - this.firstShutdownTimestamp) >= intervalForcibly) {
this.refCount.set(-1000 - this.getRefCount());
this.release();
}
}
}
3)TransientStorePool
短暫的存儲池。RocketMQ單獨創建一個MappedByteBuffer內存緩存池,用來臨時存儲數據,數據先寫入該內存映射中,然後由commit線程定時將數據從該內存複製到與目標物理文件對應的內存映射中。RocketMQ引入該機制主要的原因是提供一種內存鎖定,將當前堆外內存一直鎖定在內存中,避免被進程將內存交換到磁盤。
private final int poolSize; //availableBuffers個數
private final int fileSize; //每隔ByteBuffer大小
private final Deque<ByteBuffer> availableBuffers; //ByteBuffer容器。雙端隊列
初始化
public void init() {
//創建poolSize個堆外內存
for (int i = 0; i < poolSize; i++) {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(fileSize);
final long address = ((DirectBuffer) byteBuffer).address();
Pointer pointer = new Pointer(address);
//使用com.sun.jna.Library類庫將該批內存鎖定,避免被置換到交換區,提高存儲性能
LibC.INSTANCE.mlock(pointer, new NativeLong(fileSize));
availableBuffers.offer(byteBuffer);
}
}
2.4.5 實時更新消息消費隊列與索引文件
消息消費隊文件、消息屬性索引文件都是基於CommitLog文件構建的,當消息生產者提交的消息存儲在CommitLog文件中,ConsumerQueue、IndexFile需要及時更新,否則消息無法及時被消費,根據消息屬性查找消息也會出現較大延遲。RocketMQ通過開啓一個線程ReputMessageService來準實時轉發CommitLog文件更新事件,相應的任務處理器根據轉發的消息及時更新ConsumerQueue、IndexFile文件。
代碼:DefaultMessageStore:start
//設置CommitLog內存中最大偏移量
this.reputMessageService.setReputFromOffset(maxPhysicalPosInLogicQueue);
//啓動
this.reputMessageService.start();
代碼:DefaultMessageStore:run
public void run() {
DefaultMessageStore.log.info(this.getServiceName() + " service started");
//每隔1毫秒就繼續嘗試推送消息到消息消費隊列和索引文件
while (!this.isStopped()) {
try {
Thread.sleep(1);
this.doReput();
} catch (Exception e) {
DefaultMessageStore.log.warn(this.getServiceName() + " service has exception. ", e);
}
}
DefaultMessageStore.log.info(this.getServiceName() + " service end");
}
代碼:DefaultMessageStore:deReput
//從result中循環遍歷消息,一次讀一條,創建DispatherRequest對象。
for (int readSize = 0; readSize < result.getSize() && doNext; ) {
DispatchRequest dispatchRequest = DefaultMessageStore.this.commitLog.checkMessageAndReturnSize(result.getByteBuffer(), false, false);
int size = dispatchRequest.getBufferSize() == -1 ? dispatchRequest.getMsgSize() : dispatchRequest.getBufferSize();
if (dispatchRequest.isSuccess()) {
if (size > 0) {
DefaultMessageStore.this.doDispatch(dispatchRequest);
}
}
}
DispatchRequest
String topic; //消息主題名稱
int queueId; //消息隊列ID
long commitLogOffset; //消息物理偏移量
int msgSize; //消息長度
long tagsCode; //消息過濾tag hashCode
long storeTimestamp; //消息存儲時間戳
long consumeQueueOffset; //消息隊列偏移量
String keys; //消息索引key
boolean success; //是否成功解析到完整的消息
String uniqKey; //消息唯一鍵
int sysFlag; //消息系統標記
long preparedTransactionOffset; //消息預處理事務偏移量
Map<String, String> propertiesMap; //消息屬性
byte[] bitMap; //位圖
1)轉發到ConsumerQueue
class CommitLogDispatcherBuildConsumeQueue implements CommitLogDispatcher {
@Override
public void dispatch(DispatchRequest request) {
final int tranType = MessageSysFlag.getTransactionValue(request.getSysFlag());
switch (tranType) {
case MessageSysFlag.TRANSACTION_NOT_TYPE:
case MessageSysFlag.TRANSACTION_COMMIT_TYPE:
//消息分發
DefaultMessageStore.this.putMessagePositionInfo(request);
break;
case MessageSysFlag.TRANSACTION_PREPARED_TYPE:
case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE:
break;
}
}
}
代碼:DefaultMessageStore#putMessagePositionInfo
public void putMessagePositionInfo(DispatchRequest dispatchRequest) {
//獲得消費隊列
ConsumeQueue cq = this.findConsumeQueue(dispatchRequest.getTopic(), dispatchRequest.getQueueId());
//消費隊列分發消息
cq.putMessagePositionInfoWrapper(dispatchRequest);
}
代碼:DefaultMessageStore#putMessagePositionInfo
//依次將消息偏移量、消息長度、tag寫入到ByteBuffer中
this.byteBufferIndex.flip();
this.byteBufferIndex.limit(CQ_STORE_UNIT_SIZE);
this.byteBufferIndex.putLong(offset);
this.byteBufferIndex.putInt(size);
this.byteBufferIndex.putLong(tagsCode);
//獲得內存映射文件
MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile(expectLogicOffset);
if (mappedFile != null) {
//將消息追加到內存映射文件,異步輸盤
return mappedFile.appendMessage(this.byteBufferIndex.array());
}
2)轉發到Index
class CommitLogDispatcherBuildIndex implements CommitLogDispatcher {
@Override
public void dispatch(DispatchRequest request) {
if (DefaultMessageStore.this.messageStoreConfig.isMessageIndexEnable()) {
DefaultMessageStore.this.indexService.buildIndex(request);
}
}
}
代碼:DefaultMessageStore#buildIndex
public void buildIndex(DispatchRequest req) {
//獲得索引文件
IndexFile indexFile = retryGetAndCreateIndexFile();
if (indexFile != null) {
//獲得文件最大物理偏移量
long endPhyOffset = indexFile.getEndPhyOffset();
DispatchRequest msg = req;
String topic = msg.getTopic();
String keys = msg.getKeys();
//如果該消息的物理偏移量小於索引文件中的最大物理偏移量,則說明是重複數據,忽略本次索引構建
if (msg.getCommitLogOffset() < endPhyOffset) {
return;
}
final int tranType = MessageSysFlag.getTransactionValue(msg.getSysFlag());
switch (tranType) {
case MessageSysFlag.TRANSACTION_NOT_TYPE:
case MessageSysFlag.TRANSACTION_PREPARED_TYPE:
case MessageSysFlag.TRANSACTION_COMMIT_TYPE:
break;
case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE:
return;
}
//如果消息ID不爲空,則添加到Hash索引中
if (req.getUniqKey() != null) {
indexFile = putKey(indexFile, msg, buildKey(topic, req.getUniqKey()));
if (indexFile == null) {
return;
}
}
//構建索引key,RocketMQ支持爲同一個消息建立多個索引,多個索引鍵空格隔開.
if (keys != null && keys.length() > 0) {
String[] keyset = keys.split(MessageConst.KEY_SEPARATOR);
for (int i = 0; i < keyset.length; i++) {
String key = keyset[i];
if (key.length() > 0) {
indexFile = putKey(indexFile, msg, buildKey(topic, key));
if (indexFile == null) {
return;
}
}
}
}
} else {
log.error("build index error, stop building index");
}
}
2.4.6 消息隊列和索引文件恢復
由於RocketMQ存儲首先將消息全量存儲在CommitLog文件中,然後異步生成轉發任務更新ConsumerQueue和Index文件。如果消息成功存儲到CommitLog文件中,轉發任務未成功執行,此時消息服務器Broker由於某個願意宕機,導致CommitLog、ConsumerQueue、IndexFile文件數據不一致。如果不加以人工修復的話,會有一部分消息即便在CommitLog中文件中存在,但由於沒有轉發到ConsumerQueue,這部分消息將永遠復發被消費者消費。
1)存儲文件加載
代碼:DefaultMessageStore#load
判斷上一次是否異常退出。實現機制是Broker在啓動時創建abort文件,在退出時通過JVM鉤子函數刪除abort文件。如果下次啓動時存在abort文件。說明Broker時異常退出的,CommitLog與ConsumerQueue數據有可能不一致,需要進行修復。
//判斷臨時文件是否存在
boolean lastExitOK = !this.isTempFileExist();
//根據臨時文件判斷當前Broker是否異常退出
private boolean isTempFileExist() {
String fileName = StorePathConfigHelper
.getAbortFile(this.messageStoreConfig.getStorePathRootDir());
File file = new File(fileName);
return file.exists();
}
代碼:DefaultMessageStore#load
//加載延時隊列
if (null != scheduleMessageService) {
result = result && this.scheduleMessageService.load();
}
// 加載CommitLog文件
result = result && this.commitLog.load();
// 加載消費隊列文件
result = result && this.loadConsumeQueue();
if (result) {
//加載存儲監測點,監測點主要記錄CommitLog文件、ConsumerQueue文件、Index索引文件的刷盤點
this.storeCheckpoint =new StoreCheckpoint(StorePathConfigHelper.getStoreCheckpoint(this.messageStoreConfig.getStorePathRootDir()));
//加載index文件
this.indexService.load(lastExitOK);
//根據Broker是否異常退出,執行不同的恢復策略
this.recover(lastExitOK);
}
代碼:MappedFileQueue#load
加載CommitLog到映射文件
//指向CommitLog文件目錄
File dir = new File(this.storePath);
//獲得文件數組
File[] files = dir.listFiles();
if (files != null) {
// 文件排序
Arrays.sort(files);
//遍歷文件
for (File file : files) {
//如果文件大小和配置文件不一致,退出
if (file.length() != this.mappedFileSize) {
return false;
}
try {
//創建映射文件
MappedFile mappedFile = new MappedFile(file.getPath(), mappedFileSize);
mappedFile.setWrotePosition(this.mappedFileSize);
mappedFile.setFlushedPosition(this.mappedFileSize);
mappedFile.setCommittedPosition(this.mappedFileSize);
//將映射文件添加到隊列
this.mappedFiles.add(mappedFile);
log.info("load " + file.getPath() + " OK");
} catch (IOException e) {
log.error("load file " + file + " error", e);
return false;
}
}
}
return true;
代碼:DefaultMessageStore#loadConsumeQueue
加載消息消費隊列
//執行消費隊列目錄
File dirLogic = new File(StorePathConfigHelper.getStorePathConsumeQueue(this.messageStoreConfig.getStorePathRootDir()));
//遍歷消費隊列目錄
File[] fileTopicList = dirLogic.listFiles();
if (fileTopicList != null) {
for (File fileTopic : fileTopicList) {
//獲得子目錄名稱,即topic名稱
String topic = fileTopic.getName();
//遍歷子目錄下的消費隊列文件
File[] fileQueueIdList = fileTopic.listFiles();
if (fileQueueIdList != null) {
//遍歷文件
for (File fileQueueId : fileQueueIdList) {
//文件名稱即隊列ID
int queueId;
try {
queueId = Integer.parseInt(fileQueueId.getName());
} catch (NumberFormatException e) {
continue;
}
//創建消費隊列並加載到內存
ConsumeQueue logic = new ConsumeQueue(
topic,
queueId,
StorePathConfigHelper.getStorePathConsumeQueue(this.messageStoreConfig.getStorePathRootDir()),
this.getMessageStoreConfig().getMapedFileSizeConsumeQueue(),
this);
this.putConsumeQueue(topic, queueId, logic);
if (!logic.load()) {
return false;
}
}
}
}
}
log.info("load logics queue all over, OK");
return true;
代碼:IndexService#load
加載索引文件
public boolean load(final boolean lastExitOK) {
//索引文件目錄
File dir = new File(this.storePath);
//遍歷索引文件
File[] files = dir.listFiles();
if (files != null) {
//文件排序
Arrays.sort(files);
//遍歷文件
for (File file : files) {
try {
//加載索引文件
IndexFile f = new IndexFile(file.getPath(), this.hashSlotNum, this.indexNum, 0, 0);
f.load();
if (!lastExitOK) {
//索引文件上次的刷盤時間小於該索引文件的消息時間戳,該文件將立即刪除
if (f.getEndTimestamp() > this.defaultMessageStore.getStoreCheckpoint()
.getIndexMsgTimestamp()) {
f.destroy(0);
continue;
}
}
//將索引文件添加到隊列
log.info("load index file OK, " + f.getFileName());
this.indexFileList.add(f);
} catch (IOException e) {
log.error("load file {} error", file, e);
return false;
} catch (NumberFormatException e) {
log.error("load file {} error", file, e);
}
}
}
return true;
}
代碼:DefaultMessageStore#recover
文件恢復,根據Broker是否正常退出執行不同的恢復策略
private void recover(final boolean lastExitOK) {
//獲得最大的物理便宜消費隊列
long maxPhyOffsetOfConsumeQueue = this.recoverConsumeQueue();
if (lastExitOK) {
//正常恢復
this.commitLog.recoverNormally(maxPhyOffsetOfConsumeQueue);
} else {
//異常恢復
this.commitLog.recoverAbnormally(maxPhyOffsetOfConsumeQueue);
}
//在CommitLog中保存每個消息消費隊列當前的存儲邏輯偏移量
this.recoverTopicQueueTable();
}
代碼:DefaultMessageStore#recoverTopicQueueTable
恢復ConsumerQueue後,將在CommitLog實例中保存每隔消息隊列當前的存儲邏輯偏移量,這也是消息中不僅存儲主題、消息隊列ID、還存儲了消息隊列的關鍵所在。
public void recoverTopicQueueTable() {
HashMap<String/* topic-queueid */, Long/* offset */> table = new HashMap<String, Long>(1024);
//CommitLog最小偏移量
long minPhyOffset = this.commitLog.getMinOffset();
//遍歷消費隊列,將消費隊列保存在CommitLog中
for (ConcurrentMap<Integer, ConsumeQueue> maps : this.consumeQueueTable.values()) {
for (ConsumeQueue logic : maps.values()) {
String key = logic.getTopic() + "-" + logic.getQueueId();
table.put(key, logic.getMaxOffsetInQueue());
logic.correctMinOffset(minPhyOffset);
}
}
this.commitLog.setTopicQueueTable(table);
}
2)正常恢復
代碼:CommitLog#recoverNormally
public void recoverNormally(long maxPhyOffsetOfConsumeQueue) {
final List<MappedFile> mappedFiles = this.mappedFileQueue.getMappedFiles();
if (!mappedFiles.isEmpty()) {
//Broker正常停止再重啓時,從倒數第三個開始恢復,如果不足3個文件,則從第一個文件開始恢復。
int index = mappedFiles.size() - 3;
if (index < 0)
index = 0;
MappedFile mappedFile = mappedFiles.get(index);
ByteBuffer byteBuffer = mappedFile.sliceByteBuffer();
long processOffset = mappedFile.getFileFromOffset();
//代表當前已校驗通過的offset
long mappedFileOffset = 0;
while (true) {
//查找消息
DispatchRequest dispatchRequest = this.checkMessageAndReturnSize(byteBuffer, checkCRCOnRecover);
//消息長度
int size = dispatchRequest.getMsgSize();
//查找結果爲true,並且消息長度大於0,表示消息正確.mappedFileOffset向前移動本消息長度
if (dispatchRequest.isSuccess() && size > 0) {
mappedFileOffset += size;
}
//如果查找結果爲true且消息長度等於0,表示已到該文件末尾,如果還有下一個文件,則重置processOffset和MappedFileOffset重複查找下一個文件,否則跳出循環。
else if (dispatchRequest.isSuccess() && size == 0) {
index++;
if (index >= mappedFiles.size()) {
// Current branch can not happen
break;
} else {
//取出每個文件
mappedFile = mappedFiles.get(index);
byteBuffer = mappedFile.sliceByteBuffer();
processOffset = mappedFile.getFileFromOffset();
mappedFileOffset = 0;
}
}
// 查找結果爲false,表明該文件未填滿所有消息,跳出循環,結束循環
else if (!dispatchRequest.isSuccess()) {
log.info("recover physics file end, " + mappedFile.getFileName());
break;
}
}
//更新MappedFileQueue的flushedWhere和committedWhere指針
processOffset += mappedFileOffset;
this.mappedFileQueue.setFlushedWhere(processOffset);
this.mappedFileQueue.setCommittedWhere(processOffset);
//刪除offset之後的所有文件
this.mappedFileQueue.truncateDirtyFiles(processOffset);
if (maxPhyOffsetOfConsumeQueue >= processOffset) {
this.defaultMessageStore.truncateDirtyLogicFiles(processOffset);
}
} else {
this.mappedFileQueue.setFlushedWhere(0);
this.mappedFileQueue.setCommittedWhere(0);
this.defaultMessageStore.destroyLogics();
}
}
代碼:MappedFileQueue#truncateDirtyFiles
public void truncateDirtyFiles(long offset) {
List<MappedFile> willRemoveFiles = new ArrayList<MappedFile>();
//遍歷目錄下文件
for (MappedFile file : this.mappedFiles) {
//文件尾部的偏移量
long fileTailOffset = file.getFileFromOffset() + this.mappedFileSize;
//文件尾部的偏移量大於offset
if (fileTailOffset > offset) {
//offset大於文件的起始偏移量
if (offset >= file.getFileFromOffset()) {
//更新wrotePosition、committedPosition、flushedPosistion
file.setWrotePosition((int) (offset % this.mappedFileSize));
file.setCommittedPosition((int) (offset % this.mappedFileSize));
file.setFlushedPosition((int) (offset % this.mappedFileSize));
} else {
//offset小於文件的起始偏移量,說明該文件是有效文件後面創建的,釋放mappedFile佔用內存,刪除文件
file.destroy(1000);
willRemoveFiles.add(file);
}
}
}
this.deleteExpiredFile(willRemoveFiles);
}
3)異常恢復
Broker異常停止文件恢復的實現爲CommitLog#recoverAbnormally。異常文件恢復步驟與正常停止文件恢復流程基本相同,其主要差別有兩個。首先,正常停止默認從倒數第三個文件開始進行恢復,而異常停止則需要從最後一個文件往前走,找到第一個消息存儲正常的文件。其次,如果CommitLog目錄沒有消息文件,如果消息消費隊列目錄下存在文件,則需要銷燬。
代碼:CommitLog#recoverAbnormally
if (!mappedFiles.isEmpty()) {
// Looking beginning to recover from which file
int index = mappedFiles.size() - 1;
MappedFile mappedFile = null;
for (; index >= 0; index--) {
mappedFile = mappedFiles.get(index);
//判斷消息文件是否是一個正確的文件
if (this.isMappedFileMatchedRecover(mappedFile)) {
log.info("recover from this mapped file " + mappedFile.getFileName());
break;
}
}
//根據索引取出mappedFile文件
if (index < 0) {
index = 0;
mappedFile = mappedFiles.get(index);
}
//...驗證消息的合法性,並將消息轉發到消息消費隊列和索引文件
}else{
//未找到mappedFile,重置flushWhere、committedWhere都爲0,銷燬消息隊列文件
this.mappedFileQueue.setFlushedWhere(0);
this.mappedFileQueue.setCommittedWhere(0);
this.defaultMessageStore.destroyLogics();
}
2.4.7 刷盤機制
RocketMQ的存儲是基於JDK NIO的內存映射機制(MappedByteBuffer)的,消息存儲首先將消息追加到內存,再根據配置的刷盤策略在不同時間進行刷寫磁盤。
同步刷盤
消息追加到內存後,立即將數據刷寫到磁盤文件
代碼:CommitLog#handleDiskFlush
//刷盤服務
final GroupCommitService service = (GroupCommitService) this.flushCommitLogService;
if (messageExt.isWaitStoreMsgOK()) {
//封裝刷盤請求
GroupCommitRequest request = new GroupCommitRequest(result.getWroteOffset() + result.getWroteBytes());
//提交刷盤請求
service.putRequest(request);
//線程阻塞5秒,等待刷盤結束
boolean flushOK = request.waitForFlush(this.defaultMessageStore.getMessageStoreConfig().getSyncFlushTimeout());
if (!flushOK) {
putMessageResult.setPutMessageStatus(PutMessageStatus.FLUSH_DISK_TIMEOUT);
}
GroupCommitRequest
long nextOffset; //刷盤點偏移量
CountDownLatch countDownLatch = new CountDownLatch(1); //倒計樹鎖存器
volatile boolean flushOK = false; //刷盤結果;默認爲false
代碼:GroupCommitService#run
public void run() {
CommitLog.log.info(this.getServiceName() + " service started");
while (!this.isStopped()) {
try {
//線程等待10ms
this.waitForRunning(10);
//執行提交
this.doCommit();
} catch (Exception e) {
CommitLog.log.warn(this.getServiceName() + " service has exception. ", e);
}
}
...
}
代碼:GroupCommitService#doCommit
private void doCommit() {
//加鎖
synchronized (this.requestsRead) {
if (!this.requestsRead.isEmpty()) {
//遍歷requestsRead
for (GroupCommitRequest req : this.requestsRead) {
// There may be a message in the next file, so a maximum of
// two times the flush
boolean flushOK = false;
for (int i = 0; i < 2 && !flushOK; i++) {
flushOK = CommitLog.this.mappedFileQueue.getFlushedWhere() >= req.getNextOffset();
//刷盤
if (!flushOK) {
CommitLog.this.mappedFileQueue.flush(0);
}
}
//喚醒發送消息客戶端
req.wakeupCustomer(flushOK);
}
//更新刷盤監測點
long storeTimestamp = CommitLog.this.mappedFileQueue.getStoreTimestamp();
if (storeTimestamp > 0) { CommitLog.this.defaultMessageStore.getStoreCheckpoint().setPhysicMsgTimestamp(storeTimestamp);
}
this.requestsRead.clear();
} else {
// Because of individual messages is set to not sync flush, it
// will come to this process
CommitLog.this.mappedFileQueue.flush(0);
}
}
}
異步刷盤
在消息追加到內存後,立即返回給消息發送端。如果開啓transientStorePoolEnable,RocketMQ會單獨申請一個與目標物理文件(commitLog)同樣大小的堆外內存,該堆外內存將使用內存鎖定,確保不會被置換到虛擬內存中去,消息首先追加到堆外內存,然後提交到物理文件的內存映射中,然後刷寫到磁盤。如果未開啓transientStorePoolEnable,消息直接追加到物理文件直接映射文件中,然後刷寫到磁盤中。
開啓transientStorePoolEnable後異步刷盤步驟:
- 將消息直接追加到ByteBuffer(堆外內存)
- CommitRealTimeService線程每隔200ms將ByteBuffer新追加內容提交到MappedByteBuffer中
- MappedByteBuffer在內存中追加提交的內容,wrotePosition指針向後移動
- commit操作成功返回,將committedPosition位置恢復
- FlushRealTimeService線程默認每500ms將MappedByteBuffer中新追加的內存刷寫到磁盤
代碼:CommitLog$CommitRealTimeService#run
提交線程工作機制
//間隔時間,默認200ms
int interval = CommitLog.this.defaultMessageStore.getMessageStoreConfig().getCommitIntervalCommitLog();
//一次提交的至少頁數
int commitDataLeastPages = CommitLog.this.defaultMessageStore.getMessageStoreConfig().getCommitCommitLogLeastPages();
//兩次真實提交的最大間隔,默認200ms
int commitDataThoroughInterval =
CommitLog.this.defaultMessageStore.getMessageStoreConfig().getCommitCommitLogThoroughInterval();
//上次提交間隔超過commitDataThoroughInterval,則忽略提交commitDataThoroughInterval參數,直接提交
long begin = System.currentTimeMillis();
if (begin >= (this.lastCommitTimestamp + commitDataThoroughInterval)) {
this.lastCommitTimestamp = begin;
commitDataLeastPages = 0;
}
//執行提交操作,將待提交數據提交到物理文件的內存映射區
boolean result = CommitLog.this.mappedFileQueue.commit(commitDataLeastPages);
long end = System.currentTimeMillis();
if (!result) {
this.lastCommitTimestamp = end; // result = false means some data committed.
//now wake up flush thread.
//喚醒刷盤線程
flushCommitLogService.wakeup();
}
if (end - begin > 500) {
log.info("Commit data to file costs {} ms", end - begin);
}
this.waitForRunning(interval);
代碼:CommitLog$FlushRealTimeService#run
刷盤線程工作機制
//表示await方法等待,默認false
boolean flushCommitLogTimed = CommitLog.this.defaultMessageStore.getMessageStoreConfig().isFlushCommitLogTimed();
//線程執行時間間隔
int interval = CommitLog.this.defaultMessageStore.getMessageStoreConfig().getFlushIntervalCommitLog();
//一次刷寫任務至少包含頁數
int flushPhysicQueueLeastPages = CommitLog.this.defaultMessageStore.getMessageStoreConfig().getFlushCommitLogLeastPages();
//兩次真實刷寫任務最大間隔
int flushPhysicQueueThoroughInterval =
CommitLog.this.defaultMessageStore.getMessageStoreConfig().getFlushCommitLogThoroughInterval();
...
//距離上次提交間隔超過flushPhysicQueueThoroughInterval,則本次刷盤任務將忽略flushPhysicQueueLeastPages,直接提交
long currentTimeMillis = System.currentTimeMillis();
if (currentTimeMillis >= (this.lastFlushTimestamp + flushPhysicQueueThoroughInterval)) {
this.lastFlushTimestamp = currentTimeMillis;
flushPhysicQueueLeastPages = 0;
printFlushProgress = (printTimes++ % 10) == 0;
}
...
//執行一次刷盤前,先等待指定時間間隔
if (flushCommitLogTimed) {
Thread.sleep(interval);
} else {
this.waitForRunning(interval);
}
...
long begin = System.currentTimeMillis();
//刷寫磁盤
CommitLog.this.mappedFileQueue.flush(flushPhysicQueueLeastPages);
long storeTimestamp = CommitLog.this.mappedFileQueue.getStoreTimestamp();
if (storeTimestamp > 0) {
//更新存儲監測點文件的時間戳
CommitLog.this.defaultMessageStore.getStoreCheckpoint().setPhysicMsgTimestamp(storeTimestamp);
2.4.8 過期文件刪除機制
由於RocketMQ操作CommitLog、ConsumerQueue文件是基於內存映射機制並在啓動的時候回加載CommitLog、ConsumerQueue目錄下的所有文件,爲了避免內存與磁盤的浪費,不可能將消息永久存儲在消息服務器上,所以要引入一種機制來刪除已過期的文件。RocketMQ順序寫CommitLog、ConsumerQueue文件,所有寫操作全部落在最後一個CommitLog或者ConsumerQueue文件上,之前的文件在下一個文件創建後將不會再被更新。RocketMQ清除過期文件的方法時:如果當前文件在在一定時間間隔內沒有再次被消費,則認爲是過期文件,可以被刪除,RocketMQ不會關注這個文件上的消息是否全部被消費。默認每個文件的過期時間爲72小時,通過在Broker配置文件中設置fileReservedTime來改變過期時間,單位爲小時。
代碼:DefaultMessageStore#addScheduleTask
private void addScheduleTask() {
//每隔10s調度一次清除文件
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
DefaultMessageStore.this.cleanFilesPeriodically();
}
}, 1000 * 60, this.messageStoreConfig.getCleanResourceInterval(), TimeUnit.MILLISECONDS);
...
}
代碼:DefaultMessageStore#cleanFilesPeriodically
private void cleanFilesPeriodically() {
//清除存儲文件
this.cleanCommitLogService.run();
//清除消息消費隊列文件
this.cleanConsumeQueueService.run();
}
代碼:DefaultMessageStore#deleteExpiredFiles
private void deleteExpiredFiles() {
//刪除的數量
int deleteCount = 0;
//文件保留的時間
long fileReservedTime = DefaultMessageStore.this.getMessageStoreConfig().getFileReservedTime();
//刪除物理文件的間隔
int deletePhysicFilesInterval = DefaultMessageStore.this.getMessageStoreConfig().getDeleteCommitLogFilesInterval();
//線程被佔用,第一次拒絕刪除後能保留的最大時間,超過該時間,文件將被強制刪除
int destroyMapedFileIntervalForcibly = DefaultMessageStore.this.getMessageStoreConfig().getDestroyMapedFileIntervalForcibly();
boolean timeup = this.isTimeToDelete();
boolean spacefull = this.isSpaceToDelete();
boolean manualDelete = this.manualDeleteFileSeveralTimes > 0;
if (timeup || spacefull || manualDelete) {
...執行刪除邏輯
}else{
...無作爲
}
刪除文件操作的條件
- 指定刪除文件的時間點,RocketMQ通過deleteWhen設置一天的固定時間執行一次刪除過期文件操作,默認4點
- 磁盤空間如果不充足,刪除過期文件
- 預留,手工觸發。
代碼:CleanCommitLogService#isSpaceToDelete
當磁盤空間不足時執行刪除過期文件
private boolean isSpaceToDelete() {
//磁盤分區的最大使用量
double ratio = DefaultMessageStore.this.getMessageStoreConfig().getDiskMaxUsedSpaceRatio() / 100.0;
//是否需要立即執行刪除過期文件操作
cleanImmediately = false;
{
String storePathPhysic = DefaultMessageStore.this.getMessageStoreConfig().getStorePathCommitLog();
//當前CommitLog目錄所在的磁盤分區的磁盤使用率
double physicRatio = UtilAll.getDiskPartitionSpaceUsedPercent(storePathPhysic);
//diskSpaceWarningLevelRatio:磁盤使用率警告閾值,默認0.90
if (physicRatio > diskSpaceWarningLevelRatio) {
boolean diskok = DefaultMessageStore.this.runningFlags.getAndMakeDiskFull();
if (diskok) {
DefaultMessageStore.log.error("physic disk maybe full soon " + physicRatio + ", so mark disk full");
}
//diskSpaceCleanForciblyRatio:強制清除閾值,默認0.85
cleanImmediately = true;
} else if (physicRatio > diskSpaceCleanForciblyRatio) {
cleanImmediately = true;
} else {
boolean diskok = DefaultMessageStore.this.runningFlags.getAndMakeDiskOK();
if (!diskok) {
DefaultMessageStore.log.info("physic disk space OK " + physicRatio + ", so mark disk ok");
}
}
if (physicRatio < 0 || physicRatio > ratio) {
DefaultMessageStore.log.info("physic disk maybe full soon, so reclaim space, " + physicRatio);
return true;
}
}
代碼:MappedFileQueue#deleteExpiredFileByTime
執行文件銷燬和刪除
for (int i = 0; i < mfsLength; i++) {
//遍歷每隔文件
MappedFile mappedFile = (MappedFile) mfs[i];
//計算文件存活時間
long liveMaxTimestamp = mappedFile.getLastModifiedTimestamp() + expiredTime;
//如果超過72小時,執行文件刪除
if (System.currentTimeMillis() >= liveMaxTimestamp || cleanImmediately) {
if (mappedFile.destroy(intervalForcibly)) {
files.add(mappedFile);
deleteCount++;
if (files.size() >= DELETE_FILES_BATCH_MAX) {
break;
}
if (deleteFilesInterval > 0 && (i + 1) < mfsLength) {
try {
Thread.sleep(deleteFilesInterval);
} catch (InterruptedException e) {
}
}
} else {
break;
}
} else {
//avoid deleting files in the middle
break;
}
}
2.4.9 小結
RocketMQ的存儲文件包括消息文件(Commitlog)、消息消費隊列文件(ConsumerQueue)、Hash索引文件(IndexFile)、監測點文件(checkPoint)、abort(關閉異常文件)。單個消息存儲文件、消息消費隊列文件、Hash索引文件長度固定以便使用內存映射機制進行文件的讀寫操作。RocketMQ組織文件以文件的起始偏移量來命令文件,這樣根據偏移量能快速定位到真實的物理文件。RocketMQ基於內存映射文件機制提供了同步刷盤和異步刷盤兩種機制,異步刷盤是指在消息存儲時先追加到內存映射文件,然後啓動專門的刷盤線程定時將內存中的文件數據刷寫到磁盤。
CommitLog,消息存儲文件,RocketMQ爲了保證消息發送的高吞吐量,採用單一文件存儲所有主題消息,保證消息存儲是完全的順序寫,但這樣給文件讀取帶來了不便,爲此RocketMQ爲了方便消息消費構建了消息消費隊列文件,基於主題與隊列進行組織,同時RocketMQ爲消息實現了Hash索引,可以爲消息設置索引鍵,根據所以能夠快速從CommitLog文件中檢索消息。
當消息達到CommitLog後,會通過ReputMessageService線程接近實時地將消息轉發給消息消費隊列文件與索引文件。爲了安全起見,RocketMQ引入abort文件,記錄Broker的停機是否是正常關閉還是異常關閉,在重啓Broker時爲了保證CommitLog文件,消息消費隊列文件與Hash索引文件的正確性,分別採用不同策略來恢復文件。
RocketMQ不會永久存儲消息文件、消息消費隊列文件,而是啓動文件過期機制並在磁盤空間不足或者默認凌晨4點刪除過期文件,文件保存72小時並且在刪除文件時並不會判斷該消息文件上的消息是否被消費。
2.5 Consumer
2.5.1 消息消費概述
消息消費以組的模式開展,一個消費組內可以包含多個消費者,每一個消費者組可訂閱多個主題,消費組之間有ff式和廣播模式兩種消費模式。集羣模式,主題下的同一條消息只允許被其中一個消費者消費。廣播模式,主題下的同一條消息,將被集羣內的所有消費者消費一次。消息服務器與消費者之間的消息傳遞也有兩種模式:推模式、拉模式。所謂的拉模式,是消費端主動拉起拉消息請求,而推模式是消息達到消息服務器後,推送給消息消費者。RocketMQ消息推模式的實現基於拉模式,在拉模式上包裝一層,一個拉取任務完成後開始下一個拉取任務。
集羣模式下,多個消費者如何對消息隊列進行負載呢?消息隊列負載機制遵循一個通用思想:一個消息隊列同一個時間只允許被一個消費者消費,一個消費者可以消費多個消息隊列。
RocketMQ支持局部順序消息消費,也就是保證同一個消息隊列上的消息順序消費。不支持消息全局順序消費,如果要實現某一個主題的全局順序消費,可以將該主題的隊列數設置爲1,犧牲高可用性。
2.5.2 消息消費初探
消息推送模式
消息消費重要方法
void sendMessageBack(final MessageExt msg, final int delayLevel, final String brokerName):發送消息確認
Set<MessageQueue> fetchSubscribeMessageQueues(final String topic) :獲取消費者對主題分配了那些消息隊列
void registerMessageListener(final MessageListenerConcurrently messageListener):註冊併發事件監聽器
void registerMessageListener(final MessageListenerOrderly messageListener):註冊順序消息事件監聽器
void subscribe(final String topic, final String subExpression):基於主題訂閱消息,消息過濾使用表達式
void subscribe(final String topic, final String fullClassName,final String filterClassSource):基於主題訂閱消息,消息過濾使用類模式
void subscribe(final String topic, final MessageSelector selector) :訂閱消息,並指定隊列選擇器
void unsubscribe(final String topic):取消消息訂閱
DefaultMQPushConsumer
//消費者組
private String consumerGroup;
//消息消費模式
private MessageModel messageModel = MessageModel.CLUSTERING;
//指定消費開始偏移量(最大偏移量、最小偏移量、啓動時間戳)開始消費
private ConsumeFromWhere consumeFromWhere = ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET;
//集羣模式下的消息隊列負載策略
private AllocateMessageQueueStrategy allocateMessageQueueStrategy;
//訂閱信息
private Map<String /* topic */, String /* sub expression */> subscription = new HashMap<String, String>();
//消息業務監聽器
private MessageListener messageListener;
//消息消費進度存儲器
private OffsetStore offsetStore;
//消費者最小線程數量
private int consumeThreadMin = 20;
//消費者最大線程數量
private int consumeThreadMax = 20;
//併發消息消費時處理隊列最大跨度
private int consumeConcurrentlyMaxSpan = 2000;
//每1000次流控後打印流控日誌
private int pullThresholdForQueue = 1000;
//推模式下任務間隔時間
private long pullInterval = 0;
//推模式下任務拉取的條數,默認32條
private int pullBatchSize = 32;
//每次傳入MessageListener#consumerMessage中消息的數量
private int consumeMessageBatchMaxSize = 1;
//是否每次拉取消息都訂閱消息
private boolean postSubscriptionWhenPull = false;
//消息重試次數,-1代表16次
private int maxReconsumeTimes = -1;
//消息消費超時時間
private long consumeTimeout = 15;
2.5.3 消費者啓動流程
代碼:DefaultMQPushConsumerImpl#start
public synchronized void start() throws MQClientException {
switch (this.serviceState) {
case CREATE_JUST:
this.defaultMQPushConsumer.getMessageModel(), this.defaultMQPushConsumer.isUnitMode());
this.serviceState = ServiceState.START_FAILED;
//檢查消息者是否合法
this.checkConfig();
//構建主題訂閱信息
this.copySubscription();
//設置消費者客戶端實例名稱爲進程ID
if (this.defaultMQPushConsumer.getMessageModel() == MessageModel.CLUSTERING) {
this.defaultMQPushConsumer.changeInstanceNameToPID();
}
//創建MQClient實例
this.mQClientFactory = MQClientManager.getInstance().getAndCreateMQClientInstance(this.defaultMQPushConsumer, this.rpcHook);
//構建rebalanceImpl
this.rebalanceImpl.setConsumerGroup(this.defaultMQPushConsumer.getConsumerGroup());
this.rebalanceImpl.setMessageModel(this.defaultMQPushConsumer.getMessageModel());
this.rebalanceImpl.setAllocateMessageQueueStrategy(this.defaultMQPushConsumer.getAllocateMessageQueueStrategy());
this.rebalanceImpl.setmQClientFactory(this.mQClientFactor
this.pullAPIWrapper = new PullAPIWrapper(
mQClientFactory,
this.defaultMQPushConsumer.getConsumerGroup(), isUnitMode());
this.pullAPIWrapper.registerFilterMessageHook(filterMessageHookLis
if (this.defaultMQPushConsumer.getOffsetStore() != null) {
this.offsetStore = this.defaultMQPushConsumer.getOffsetStore();
} else {
switch (this.defaultMQPushConsumer.getMessageModel()) {
case BROADCASTING: //消息消費廣播模式,將消費進度保存在本地
this.offsetStore = new LocalFileOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
break;
case CLUSTERING: //消息消費集羣模式,將消費進度保存在遠端Broker
this.offsetStore = new RemoteBrokerOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
break;
default:
break;
}
this.defaultMQPushConsumer.setOffsetStore(this.offsetStore);
}
this.offsetStore.load
//創建順序消息消費服務
if (this.getMessageListenerInner() instanceof MessageListenerOrderly) {
this.consumeOrderly = true;
this.consumeMessageService =
new ConsumeMessageOrderlyService(this, (MessageListenerOrderly) this.getMessageListenerInner());
//創建併發消息消費服務
} else if (this.getMessageListenerInner() instanceof MessageListenerConcurrently) {
this.consumeOrderly = false;
this.consumeMessageService =
new ConsumeMessageConcurrentlyService(this, (MessageListenerConcurrently) this.getMessageListenerInner());
}
//消息消費服務啓動
this.consumeMessageService.start();
//註冊消費者實例
boolean registerOK = mQClientFactory.registerConsumer(this.defaultMQPushConsumer.getConsumerGroup(), this);
if (!registerOK) {
this.serviceState = ServiceState.CREATE_JUST;
this.consumeMessageService.shutdown();
throw new MQClientException("The consumer group[" + this.defaultMQPushConsumer.getConsumerGroup()
+ "] has been created before, specify another name please." + FAQUrl.suggestTodo(FAQUrl.GROUP_NAME_DUPLICATE_URL),
null);
//啓動消費者客戶端
mQClientFactory.start();
log.info("the consumer [{}] start OK.", this.defaultMQPushConsumer.getConsumerGroup());
this.serviceState = ServiceState.RUNNING;
break;
case RUNNING:
case START_FAILED:
case SHUTDOWN_ALREADY:
throw new MQClientException("The PushConsumer service state not OK, maybe started once, "
+ this.serviceState
+ FAQUrl.suggestTodo(FAQUrl.CLIENT_SERVICE_NOT_OK),
null);
default:
break;
}
this.updateTopicSubscribeInfoWhenSubscriptionChanged();
this.mQClientFactory.checkClientInBroker();
this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();
this.mQClientFactory.rebalanceImmediately();
}
2.5.4 消息拉取
消息消費模式有兩種模式:廣播模式與集羣模式。廣播模式比較簡單,每一個消費者需要拉取訂閱主題下所有隊列的消息。本文重點講解集羣模式。在集羣模式下,同一個消費者組內有多個消息消費者,同一個主題存在多個消費隊列,消費者通過負載均衡的方式消費消息。
消息隊列負載均衡,通常的作法是一個消息隊列在同一個時間只允許被一個消費消費者消費,一個消息消費者可以同時消費多個消息隊列。
1)PullMessageService實現機制
從MQClientInstance的啓動流程中可以看出,RocketMQ使用一個單獨的線程PullMessageService來負責消息的拉取。
代碼:PullMessageService#run
public void run() {
log.info(this.getServiceName() + " service started");
//循環拉取消息
while (!this.isStopped()) {
try {
//從請求隊列中獲取拉取消息請求
PullRequest pullRequest = this.pullRequestQueue.take();
//拉取消息
this.pullMessage(pullRequest);
} catch (InterruptedException ignored) {
} catch (Exception e) {
log.error("Pull Message Service Run Method exception", e);
}
}
log.info(this.getServiceName() + " service end");
}
PullRequest
private String consumerGroup; //消費者組
private MessageQueue messageQueue; //待拉取消息隊列
private ProcessQueue processQueue; //消息處理隊列
private long nextOffset; //待拉取的MessageQueue偏移量
private boolean lockedFirst = false; //是否被鎖定
代碼:PullMessageService#pullMessage
private void pullMessage(final PullRequest pullRequest) {
//獲得消費者實例
final MQConsumerInner consumer = this.mQClientFactory.selectConsumer(pullRequest.getConsumerGroup());
if (consumer != null) {
//強轉爲推送模式消費者
DefaultMQPushConsumerImpl impl = (DefaultMQPushConsumerImpl) consumer;
//推送消息
impl.pullMessage(pullRequest);
} else {
log.warn("No matched consumer for the PullRequest {}, drop it", pullRequest);
}
}
####2)ProcessQueue實現機制
ProcessQueue是MessageQueue在消費端的重現、快照。PullMessageService從消息服務器默認每次拉取32條消息,按照消息的隊列偏移量順序存放在ProcessQueue中,PullMessageService然後將消息提交到消費者消費線程池,消息成功消費後從ProcessQueue中移除。
屬性
//消息容器
private final TreeMap<Long, MessageExt> msgTreeMap = new TreeMap<Long, MessageExt>();
//讀寫鎖
private final ReadWriteLock lockTreeMap = new ReentrantReadWriteLock();
//ProcessQueue總消息樹
private final AtomicLong msgCount = new AtomicLong();
//ProcessQueue隊列最大偏移量
private volatile long queueOffsetMax = 0L;
//當前ProcessQueue是否被丟棄
private volatile boolean dropped = false;
//上一次拉取時間戳
private volatile long lastPullTimestamp = System.currentTimeMillis();
//上一次消費時間戳
private volatile long lastConsumeTimestamp = System.currentTimeMillis();
方法
//移除消費超時消息
public void cleanExpiredMsg(DefaultMQPushConsumer pushConsumer)
//添加消息
public boolean putMessage(final List<MessageExt> msgs)
//獲取消息最大間隔
public long getMaxSpan()
//移除消息
public long removeMessage(final List<MessageExt> msgs)
//將consumingMsgOrderlyTreeMap中消息重新放在msgTreeMap,並清空consumingMsgOrderlyTreeMap
public void rollback()
//將consumingMsgOrderlyTreeMap消息清除,表示成功處理該批消息
public long commit()
//重新處理該批消息
public void makeMessageToCosumeAgain(List<MessageExt> msgs)
//從processQueue中取出batchSize條消息
public List<MessageExt> takeMessags(final int batchSize)
3)消息拉取基本流程
1.客戶端發起拉取請求
代碼:DefaultMQPushConsumerImpl#pullMessage
public void pullMessage(final PullRequest pullRequest) {
//從pullRequest獲得ProcessQueue
final ProcessQueue processQueue = pullRequest.getProcessQueue();
//如果處理隊列被丟棄,直接返回
if (processQueue.isDropped()) {
log.info("the pull request[{}] is dropped.", pullRequest.toString());
return;
}
//如果處理隊列未被丟棄,更新時間戳
pullRequest.getProcessQueue().setLastPullTimestamp(System.currentTimeMillis());
try {
this.makeSureStateOK();
} catch (MQClientException e) {
log.warn("pullMessage exception, consumer state not ok", e);
this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_EXCEPTION);
return;
}
//如果處理隊列被掛起,延遲1s後再執行
if (this.isPause()) {
log.warn("consumer was paused, execute pull request later. instanceName={}, group={}", this.defaultMQPushConsumer.getInstanceName(), this.defaultMQPushConsumer.getConsumerGroup());
this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_SUSPEND);
return;
}
//獲得最大待處理消息數量
long cachedMessageCount = processQueue.getMsgCount().get();
//獲得最大待處理消息大小
long cachedMessageSizeInMiB = processQueue.getMsgSize().get() / (1024 * 1024);
//從數量進行流控
if (cachedMessageCount > this.defaultMQPushConsumer.getPullThresholdForQueue()) {
this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL);
if ((queueFlowControlTimes++ % 1000) == 0) {
log.warn(
"the cached message count exceeds the threshold {}, so do flow control, minOffset={}, maxOffset={}, count={}, size={} MiB, pullRequest={}, flowControlTimes={}",
this.defaultMQPushConsumer.getPullThresholdForQueue(), processQueue.getMsgTreeMap().firstKey(), processQueue.getMsgTreeMap().lastKey(), cachedMessageCount, cachedMessageSizeInMiB, pullRequest, queueFlowControlTimes);
}
return;
}
//從消息大小進行流控
if (cachedMessageSizeInMiB > this.defaultMQPushConsumer.getPullThresholdSizeForQueue()) {
this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL);
if ((queueFlowControlTimes++ % 1000) == 0) {
log.warn(
"the cached message size exceeds the threshold {} MiB, so do flow control, minOffset={}, maxOffset={}, count={}, size={} MiB, pullRequest={}, flowControlTimes={}",
this.defaultMQPushConsumer.getPullThresholdSizeForQueue(), processQueue.getMsgTreeMap().firstKey(), processQueue.getMsgTreeMap().lastKey(), cachedMessageCount, cachedMessageSizeInMiB, pullRequest, queueFlowControlTimes);
}
return;
}
//獲得訂閱信息
final SubscriptionData subscriptionData = this.rebalanceImpl.getSubscriptionInner().get(pullRequest.getMessageQueue().getTopic());
if (null == subscriptionData) {
this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_EXCEPTION);
log.warn("find the consumer's subscription failed, {}", pullRequest);
return;
//與服務端交互,獲取消息
this.pullAPIWrapper.pullKernelImpl(
pullRequest.getMessageQueue(),
subExpression,
subscriptionData.getExpressionType(),
subscriptionData.getSubVersion(),
pullRequest.getNextOffset(),
this.defaultMQPushConsumer.getPullBatchSize(),
sysFlag,
commitOffsetValue,
BROKER_SUSPEND_MAX_TIME_MILLIS,
CONSUMER_TIMEOUT_MILLIS_WHEN_SUSPEND,
CommunicationMode.ASYNC,
pullCallback
);
}
2.消息服務端Broker組裝消息
代碼:PullMessageProcessor#processRequest
//構建消息過濾器
MessageFilter messageFilter;
if (this.brokerController.getBrokerConfig().isFilterSupportRetry()) {
messageFilter = new ExpressionForRetryMessageFilter(subscriptionData, consumerFilterData,
this.brokerController.getConsumerFilterManager());
} else {
messageFilter = new ExpressionMessageFilter(subscriptionData, consumerFilterData,
this.brokerController.getConsumerFilterManager());
}
//調用MessageStore.getMessage查找消息
final GetMessageResult getMessageResult =
this.brokerController.getMessageStore().getMessage(
requestHeader.getConsumerGroup(), //消費組名稱
requestHeader.getTopic(), //主題名稱
requestHeader.getQueueId(), //隊列ID
requestHeader.getQueueOffset(), //待拉取偏移量
requestHeader.getMaxMsgNums(), //最大拉取消息條數
messageFilter //消息過濾器
);
代碼:DefaultMessageStore#getMessage
GetMessageStatus status = GetMessageStatus.NO_MESSAGE_IN_QUEUE;
long nextBeginOffset = offset; //查找下一次隊列偏移量
long minOffset = 0; //當前消息隊列最小偏移量
long maxOffset = 0; //當前消息隊列最大偏移量
GetMessageResult getResult = new GetMessageResult();
final long maxOffsetPy = this.commitLog.getMaxOffset(); //當前commitLog最大偏移量
//根據主題名稱和隊列編號獲取消息消費隊列
ConsumeQueue consumeQueue = findConsumeQueue(topic, queueId);
...
minOffset = consumeQueue.getMinOffsetInQueue();
maxOffset = consumeQueue.getMaxOffsetInQueue();
//消息偏移量異常情況校對下一次拉取偏移量
if (maxOffset == 0) { //表示當前消息隊列中沒有消息
status = GetMessageStatus.NO_MESSAGE_IN_QUEUE;
nextBeginOffset = nextOffsetCorrection(offset, 0);
} else if (offset < minOffset) { //待拉取消息的偏移量小於隊列的其實偏移量
status = GetMessageStatus.OFFSET_TOO_SMALL;
nextBeginOffset = nextOffsetCorrection(offset, minOffset);
} else if (offset == maxOffset) { //待拉取偏移量爲隊列最大偏移量
status = GetMessageStatus.OFFSET_OVERFLOW_ONE;
nextBeginOffset = nextOffsetCorrection(offset, offset);
} else if (offset > maxOffset) { //偏移量越界
status = GetMessageStatus.OFFSET_OVERFLOW_BADLY;
if (0 == minOffset) {
nextBeginOffset = nextOffsetCorrection(offset, minOffset);
} else {
nextBeginOffset = nextOffsetCorrection(offset, maxOffset);
}
}
...
//根據偏移量從CommitLog中拉取32條消息
SelectMappedBufferResult selectResult = this.commitLog.getMessage(offsetPy, sizePy);
代碼:PullMessageProcessor#processRequest
//根據拉取結果填充responseHeader
response.setRemark(getMessageResult.getStatus().name());
responseHeader.setNextBeginOffset(getMessageResult.getNextBeginOffset());
responseHeader.setMinOffset(getMessageResult.getMinOffset());
responseHeader.setMaxOffset(getMessageResult.getMaxOffset());
//判斷如果存在主從同步慢,設置下一次拉取任務的ID爲主節點
switch (this.brokerController.getMessageStoreConfig().getBrokerRole()) {
case ASYNC_MASTER:
case SYNC_MASTER:
break;
case SLAVE:
if (!this.brokerController.getBrokerConfig().isSlaveReadEnable()) {
response.setCode(ResponseCode.PULL_RETRY_IMMEDIATELY);
responseHeader.setSuggestWhichBrokerId(MixAll.MASTER_ID);
}
break;
}
...
//GetMessageResult與Response的Code轉換
switch (getMessageResult.getStatus()) {
case FOUND: //成功
response.setCode(ResponseCode.SUCCESS);
break;
case MESSAGE_WAS_REMOVING: //消息存放在下一個commitLog中
response.setCode(ResponseCode.PULL_RETRY_IMMEDIATELY); //消息重試
break;
case NO_MATCHED_LOGIC_QUEUE: //未找到隊列
case NO_MESSAGE_IN_QUEUE: //隊列中未包含消息
if (0 != requestHeader.getQueueOffset()) {
response.setCode(ResponseCode.PULL_OFFSET_MOVED);
requestHeader.getQueueOffset(),
getMessageResult.getNextBeginOffset(),
requestHeader.getTopic(),
requestHeader.getQueueId(),
requestHeader.getConsumerGroup()
);
} else {
response.setCode(ResponseCode.PULL_NOT_FOUND);
}
break;
case NO_MATCHED_MESSAGE: //未找到消息
response.setCode(ResponseCode.PULL_RETRY_IMMEDIATELY);
break;
case OFFSET_FOUND_NULL: //消息物理偏移量爲空
response.setCode(ResponseCode.PULL_NOT_FOUND);
break;
case OFFSET_OVERFLOW_BADLY: //offset越界
response.setCode(ResponseCode.PULL_OFFSET_MOVED);
// XXX: warn and notify me
log.info("the request offset: {} over flow badly, broker max offset: {}, consumer: {}",
requestHeader.getQueueOffset(), getMessageResult.getMaxOffset(), channel.remoteAddress());
break;
case OFFSET_OVERFLOW_ONE: //offset在隊列中未找到
response.setCode(ResponseCode.PULL_NOT_FOUND);
break;
case OFFSET_TOO_SMALL: //offset未在隊列中
response.setCode(ResponseCode.PULL_OFFSET_MOVED);
requestHeader.getConsumerGroup(),
requestHeader.getTopic(),
requestHeader.getQueueOffset(),
getMessageResult.getMinOffset(), channel.remoteAddress());
break;
default:
assert false;
break;
}
...
//如果CommitLog標記可用,並且當前Broker爲主節點,則更新消息消費進度
boolean storeOffsetEnable = brokerAllowSuspend;
storeOffsetEnable = storeOffsetEnable && hasCommitOffsetFlag;
storeOffsetEnable = storeOffsetEnable
&& this.brokerController.getMessageStoreConfig().getBrokerRole() != BrokerRole.SLAVE;
if (storeOffsetEnable) {
this.brokerController.getConsumerOffsetManager().commitOffset(RemotingHelper.parseChannelRemoteAddr(channel),
requestHeader.getConsumerGroup(), requestHeader.getTopic(), requestHeader.getQueueId(), requestHeader.getCommitOffset());
}
3.消息拉取客戶端處理消息
代碼:MQClientAPIImpl#processPullResponse
private PullResult processPullResponse(
final RemotingCommand response) throws MQBrokerException, RemotingCommandException {
PullStatus pullStatus = PullStatus.NO_NEW_MSG;
//判斷響應結果
switch (response.getCode()) {
case ResponseCode.SUCCESS:
pullStatus = PullStatus.FOUND;
break;
case ResponseCode.PULL_NOT_FOUND:
pullStatus = PullStatus.NO_NEW_MSG;
break;
case ResponseCode.PULL_RETRY_IMMEDIATELY:
pullStatus = PullStatus.NO_MATCHED_MSG;
break;
case ResponseCode.PULL_OFFSET_MOVED:
pullStatus = PullStatus.OFFSET_ILLEGAL;
break;
default:
throw new MQBrokerException(response.getCode(), response.getRemark());
}
//解碼響應頭
PullMessageResponseHeader responseHeader =
(PullMessageResponseHeader) response.decodeCommandCustomHeader(PullMessageResponseHeader.class);
//封裝PullResultExt返回
return new PullResultExt(pullStatus, responseHeader.getNextBeginOffset(), responseHeader.getMinOffset(),
responseHeader.getMaxOffset(), null, responseHeader.getSuggestWhichBrokerId(), response.getBody());
}
PullResult類
private final PullStatus pullStatus; //拉取結果
private final long nextBeginOffset; //下次拉取偏移量
private final long minOffset; //消息隊列最小偏移量
private final long maxOffset; //消息隊列最大偏移量
private List<MessageExt> msgFoundList; //拉取的消息列表
代碼:DefaultMQPushConsumerImpl$PullCallback#OnSuccess
//將拉取到的消息存入processQueue
boolean dispatchToConsume = processQueue.putMessage(pullResult.getMsgFoundList());
//將processQueue提交到consumeMessageService中供消費者消費
DefaultMQPushConsumerImpl.this.consumeMessageService.submitConsumeRequest(
pullResult.getMsgFoundList(),
processQueue,
pullRequest.getMessageQueue(),
dispatchToConsume);
//如果pullInterval大於0,則等待pullInterval毫秒後將pullRequest對象放入到PullMessageService中的pullRequestQueue隊列中
if (DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval() > 0) {
DefaultMQPushConsumerImpl.this.executePullRequestLater(pullRequest,
DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval());
} else {
DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
}
4.消息拉取總結
4)消息拉取長輪詢機制分析
RocketMQ未真正實現消息推模式,而是消費者主動向消息服務器拉取消息,RocketMQ推模式是循環向消息服務端發起消息拉取請求,如果消息消費者向RocketMQ拉取消息時,消息未到達消費隊列時,如果不啓用長輪詢機制,則會在服務端等待shortPollingTimeMills時間後(掛起)再去判斷消息是否已經到達指定消息隊列,如果消息仍未到達則提示拉取消息客戶端PULL—NOT—FOUND(消息不存在);如果開啓長輪詢模式,RocketMQ一方面會每隔5s輪詢檢查一次消息是否可達,同時一有消息達到後立馬通知掛起線程再次驗證消息是否是自己感興趣的消息,如果是則從CommitLog文件中提取消息返回給消息拉取客戶端,否則直到掛起超時,超時時間由消息拉取方在消息拉取是封裝在請求參數中,PUSH模式爲15s,PULL模式通過DefaultMQPullConsumer#setBrokerSuspendMaxTimeMillis設置。RocketMQ通過在Broker客戶端配置longPollingEnable爲true來開啓長輪詢模式。
代碼:PullMessageProcessor#processRequest
//當沒有拉取到消息時,通過長輪詢方式繼續拉取消息
case ResponseCode.PULL_NOT_FOUND:
if (brokerAllowSuspend && hasSuspendFlag) {
long pollingTimeMills = suspendTimeoutMillisLong;
if (!this.brokerController.getBrokerConfig().isLongPollingEnable()) {
pollingTimeMills = this.brokerController.getBrokerConfig().getShortPollingTimeMills();
}
String topic = requestHeader.getTopic();
long offset = requestHeader.getQueueOffset();
int queueId = requestHeader.getQueueId();
//構建拉取請求對象
PullRequest pullRequest = new PullRequest(request, channel, pollingTimeMills,
this.brokerController.getMessageStore().now(), offset, subscriptionData, messageFilter);
//處理拉取請求
this.brokerController.getPullRequestHoldService().suspendPullRequest(topic, queueId, pullRequest);
response = null;
break;
}
PullRequestHoldService方式實現長輪詢
代碼:PullRequestHoldService#suspendPullRequest
//將拉取消息請求,放置在ManyPullRequest集合中
public void suspendPullRequest(final String topic, final int queueId, final PullRequest pullRequest) {
String key = this.buildKey(topic, queueId);
ManyPullRequest mpr = this.pullRequestTable.get(key);
if (null == mpr) {
mpr = new ManyPullRequest();
ManyPullRequest prev = this.pullRequestTable.putIfAbsent(key, mpr);
if (prev != null) {
mpr = prev;
}
}
mpr.addPullRequest(pullRequest);
}
代碼:PullRequestHoldService#run
public void run() {
log.info("{} service started", this.getServiceName());
while (!this.isStopped()) {
try {
//如果開啓長輪詢每隔5秒判斷消息是否到達
if (this.brokerController.getBrokerConfig().isLongPollingEnable()) {
this.waitForRunning(5 * 1000);
} else {
//沒有開啓長輪詢,每隔1s再次嘗試
this.waitForRunning(this.brokerController.getBrokerConfig().getShortPollingTimeMills());
}
long beginLockTimestamp = this.systemClock.now();
this.checkHoldRequest();
long costTime = this.systemClock.now() - beginLockTimestamp;
if (costTime > 5 * 1000) {
log.info("[NOTIFYME] check hold request cost {} ms.", costTime);
}
} catch (Throwable e) {
log.warn(this.getServiceName() + " service has exception. ", e);
}
}
log.info("{} service end", this.getServiceName());
}
代碼:PullRequestHoldService#checkHoldRequest
//遍歷拉取任務
private void checkHoldRequest() {
for (String key : this.pullRequestTable.keySet()) {
String[] kArray = key.split(TOPIC_QUEUEID_SEPARATOR);
if (2 == kArray.length) {
String topic = kArray[0];
int queueId = Integer.parseInt(kArray[1]);
//獲得消息偏移量
final long offset = this.brokerController.getMessageStore().getMaxOffsetInQueue(topic, queueId);
try {
//通知有消息達到
this.notifyMessageArriving(topic, queueId, offset);
} catch (Throwable e) {
log.error("check hold request failed. topic={}, queueId={}", topic, queueId, e);
}
}
}
}
代碼:PullRequestHoldService#notifyMessageArriving
//如果拉取消息偏移大於請求偏移量,如果消息匹配調用executeRequestWhenWakeup處理消息
if (newestOffset > request.getPullFromThisOffset()) {
boolean match = request.getMessageFilter().isMatchedByConsumeQueue(tagsCode,
new ConsumeQueueExt.CqExtUnit(tagsCode, msgStoreTime, filterBitMap));
// match by bit map, need eval again when properties is not null.
if (match && properties != null) {
match = request.getMessageFilter().isMatchedByCommitLog(null, properties);
}
if (match) {
try {
this.brokerController.getPullMessageProcessor().executeRequestWhenWakeup(request.getClientChannel(),
request.getRequestCommand());
} catch (Throwable e) {
log.error("execute request when wakeup failed.", e);
}
continue;
}
}
//如果過期時間超時,則不繼續等待將直接返回給客戶端消息未找到
if (System.currentTimeMillis() >= (request.getSuspendTimestamp() + request.getTimeoutMillis())) {
try {
this.brokerController.getPullMessageProcessor().executeRequestWhenWakeup(request.getClientChannel(),
request.getRequestCommand());
} catch (Throwable e) {
log.error("execute request when wakeup failed.", e);
}
continue;
}
如果開啓了長輪詢機制,PullRequestHoldService會每隔5s被喚醒去嘗試檢測是否有新的消息的到來纔給客戶端響應,或者直到超時纔給客戶端進行響應,消息實時性比較差,爲了避免這種情況,RocketMQ引入另外一種機制:當消息到達時喚醒掛起線程觸發一次檢查。
DefaultMessageStore$ReputMessageService機制
代碼:DefaultMessageStore#start
//長輪詢入口
this.reputMessageService.setReputFromOffset(maxPhysicalPosInLogicQueue);
this.reputMessageService.start();
代碼:DefaultMessageStore$ReputMessageService#run
public void run() {
DefaultMessageStore.log.info(this.getServiceName() + " service started");
while (!this.isStopped()) {
try {
Thread.sleep(1);
//長輪詢核心邏輯代碼入口
this.doReput();
} catch (Exception e) {
DefaultMessageStore.log.warn(this.getServiceName() + " service has exception. ", e);
}
}
DefaultMessageStore.log.info(this.getServiceName() + " service end");
}
代碼:DefaultMessageStore$ReputMessageService#deReput
//當新消息達到是,進行通知監聽器進行處理
if (BrokerRole.SLAVE != DefaultMessageStore.this.getMessageStoreConfig().getBrokerRole()
&& DefaultMessageStore.this.brokerConfig.isLongPollingEnable()) {
DefaultMessageStore.this.messageArrivingListener.arriving(dispatchRequest.getTopic(),
dispatchRequest.getQueueId(), dispatchRequest.getConsumeQueueOffset() + 1,
dispatchRequest.getTagsCode(), dispatchRequest.getStoreTimestamp(),
dispatchRequest.getBitMap(), dispatchRequest.getPropertiesMap());
}
代碼:NotifyMessageArrivingListener#arriving
public void arriving(String topic, int queueId, long logicOffset, long tagsCode,
long msgStoreTime, byte[] filterBitMap, Map<String, String> properties) {
this.pullRequestHoldService.notifyMessageArriving(topic, queueId, logicOffset, tagsCode,
msgStoreTime, filterBitMap, properties);
}
2.5.5 消息隊列負載與重新分佈機制
RocketMQ消息隊列重新分配是由RebalanceService線程來實現。一個MQClientInstance持有一個RebalanceService實現,並隨着MQClientInstance的啓動而啓動。
代碼:RebalanceService#run
public void run() {
log.info(this.getServiceName() + " service started");
//RebalanceService線程默認每隔20s執行一次mqClientFactory.doRebalance方法
while (!this.isStopped()) {
this.waitForRunning(waitInterval);
this.mqClientFactory.doRebalance();
}
log.info(this.getServiceName() + " service end");
}
代碼:MQClientInstance#doRebalance
public void doRebalance() {
//MQClientInstance遍歷以註冊的消費者,對消費者執行doRebalance()方法
for (Map.Entry<String, MQConsumerInner> entry : this.consumerTable.entrySet()) {
MQConsumerInner impl = entry.getValue();
if (impl != null) {
try {
impl.doRebalance();
} catch (Throwable e) {
log.error("doRebalance exception", e);
}
}
}
}
代碼:RebalanceImpl#doRebalance
//遍歷訂閱消息對每個主題的訂閱的隊列進行重新負載
public void doRebalance(final boolean isOrder) {
Map<String, SubscriptionData> subTable = this.getSubscriptionInner();
if (subTable != null) {
for (final Map.Entry<String, SubscriptionData> entry : subTable.entrySet()) {
final String topic = entry.getKey();
try {
this.rebalanceByTopic(topic, isOrder);
} catch (Throwable e) {
if (!topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
log.warn("rebalanceByTopic Exception", e);
}
}
}
}
this.truncateMessageQueueNotMyTopic();
}
代碼:RebalanceImpl#rebalanceByTopic
//從主題訂閱消息緩存表中獲取主題的隊列信息
Set<MessageQueue> mqSet = this.topicSubscribeInfoTable.get(topic);
//查找該主題訂閱組所有的消費者ID
List<String> cidAll = this.mQClientFactory.findConsumerIdList(topic, consumerGroup);
//給消費者重新分配隊列
if (mqSet != null && cidAll != null) {
List<MessageQueue> mqAll = new ArrayList<MessageQueue>();
mqAll.addAll(mqSet);
Collections.sort(mqAll);
Collections.sort(cidAll);
AllocateMessageQueueStrategy strategy = this.allocateMessageQueueStrategy;
List<MessageQueue> allocateResult = null;
try {
allocateResult = strategy.allocate(
this.consumerGroup,
this.mQClientFactory.getClientId(),
mqAll,
cidAll);
} catch (Throwable e) {
log.error("AllocateMessageQueueStrategy.allocate Exception. allocateMessageQueueStrategyName={}", strategy.getName(),
e);
return;
}
RocketMQ默認提供5中負載均衡分配算法
AllocateMessageQueueAveragely:平均分配
舉例:8個隊列q1,q2,q3,q4,q5,a6,q7,q8,消費者3個:c1,c2,c3
分配如下:
c1:q1,q2,q3
c2:q4,q5,a6
c3:q7,q8
AllocateMessageQueueAveragelyByCircle:平均輪詢分配
舉例:8個隊列q1,q2,q3,q4,q5,a6,q7,q8,消費者3個:c1,c2,c3
分配如下:
c1:q1,q4,q7
c2:q2,q5,a8
c3:q3,q6
注意:消息隊列的分配遵循一個消費者可以分配到多個隊列,但同一個消息隊列只會分配給一個消費者,故如果出現消費者個數大於消息隊列數量,則有些消費者無法消費消息。
2.5.6 消息消費過程
PullMessageService負責對消息隊列進行消息拉取,從遠端服務器拉取消息後將消息存儲ProcessQueue消息隊列處理隊列中,然後調用ConsumeMessageService#submitConsumeRequest方法進行消息消費,使用線程池來消費消息,確保了消息拉取與消息消費的解耦。ConsumeMessageService支持順序消息和併發消息,核心類圖如下:
併發消息消費
代碼:ConsumeMessageConcurrentlyService#submitConsumeRequest
//消息批次單次
final int consumeBatchSize = this.defaultMQPushConsumer.getConsumeMessageBatchMaxSize();
//msgs.size()默認最多爲32條。
//如果msgs.size()小於consumeBatchSize,則直接將拉取到的消息放入到consumeRequest,然後將consumeRequest提交到消費者線程池中
if (msgs.size() <= consumeBatchSize) {
ConsumeRequest consumeRequest = new ConsumeRequest(msgs, processQueue, messageQueue);
try {
this.consumeExecutor.submit(consumeRequest);
} catch (RejectedExecutionException e) {
this.submitConsumeRequestLater(consumeRequest);
}
}else{ //如果拉取的消息條數大於consumeBatchSize,則對拉取消息進行分頁
for (int total = 0; total < msgs.size(); ) {
List<MessageExt> msgThis = new ArrayList<MessageExt>(consumeBatchSize);
for (int i = 0; i < consumeBatchSize; i++, total++) {
if (total < msgs.size()) {
msgThis.add(msgs.get(total));
} else {
break;
}
ConsumeRequest consumeRequest = new ConsumeRequest(msgThis, processQueue, messageQueue);
try {
this.consumeExecutor.submit(consumeRequest);
} catch (RejectedExecutionException e) {
for (; total < msgs.size(); total++) {
msgThis.add(msgs.get(total));
this.submitConsumeRequestLater(consumeRequest);
}
}
}
代碼:ConsumeMessageConcurrentlyService$ConsumeRequest#run
//檢查processQueue的dropped,如果爲true,則停止該隊列消費。
if (this.processQueue.isDropped()) {
log.info("the message queue not be able to consume, because it's dropped. group={} {}", ConsumeMessageConcurrentlyService.this.consumerGroup, this.messageQueue);
return;
}
...
//執行消息處理的鉤子函數
if (ConsumeMessageConcurrentlyService.this.defaultMQPushConsumerImpl.hasHook()) {
consumeMessageContext = new ConsumeMessageContext();
consumeMessageContext.setNamespace(defaultMQPushConsumer.getNamespace());
consumeMessageContext.setConsumerGroup(defaultMQPushConsumer.getConsumerGroup());
consumeMessageContext.setProps(new HashMap<String, String>());
consumeMessageContext.setMq(messageQueue);
consumeMessageContext.setMsgList(msgs);
consumeMessageContext.setSuccess(false);
ConsumeMessageConcurrentlyService.this.defaultMQPushConsumerImpl.executeHookBefore(consumeMessageContext);
}
...
//調用應用程序消息監聽器的consumeMessage方法,進入到具體的消息消費業務處理邏輯
status = listener.consumeMessage(Collections.unmodifiableList(msgs), context);
//執行消息處理後的鉤子函數
if (ConsumeMessageConcurrentlyService.this.defaultMQPushConsumerImpl.hasHook()) {
consumeMessageContext.setStatus(status.toString());
consumeMessageContext.setSuccess(ConsumeConcurrentlyStatus.CONSUME_SUCCESS == status);
ConsumeMessageConcurrentlyService.this.defaultMQPushConsumerImpl.executeHookAfter(consumeMessageContext);
}
2.5.7 定時消息機制
定時消息是消息發送到Broker後,並不立即被消費者消費而是要等到特定的時間後才能被消費,RocketMQ並不支持任意的時間精度,如果要支持任意時間精度定時調度,不可避免地需要在Broker層做消息排序,再加上持久化方面的考量,將不可避免的帶來巨大的性能消耗,所以RocketMQ只支持特定級別的延遲消息。消息延遲級別在Broker端通過messageDelayLevel配置,默認爲“1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h”,delayLevel=1表示延遲消息1s,delayLevel=2表示延遲5s,依次類推。
RocketMQ定時消息實現類爲ScheduleMessageService,該類在DefaultMessageStore中創建。通過在DefaultMessageStore中調用load方法加載該類並調用start方法啓動。
代碼:ScheduleMessageService#load
//加載延遲消息消費進度的加載與delayLevelTable的構造。延遲消息的進度默認存儲路徑爲/store/config/delayOffset.json
public boolean load() {
boolean result = super.load();
result = result && this.parseDelayLevel();
return result;
}
代碼:ScheduleMessageService#start
//遍歷延遲隊列創建定時任務,遍歷延遲級別,根據延遲級別level從offsetTable中獲取消費隊列的消費進度。如果不存在,則使用0
for (Map.Entry<Integer, Long> entry : this.delayLevelTable.entrySet()) {
Integer level = entry.getKey();
Long timeDelay = entry.getValue();
Long offset = this.offsetTable.get(level);
if (null == offset) {
offset = 0L;
}
if (timeDelay != null) {
this.timer.schedule(new DeliverDelayedMessageTimerTask(level, offset), FIRST_DELAY_TIME);
}
}
//每隔10s持久化一次延遲隊列的消息消費進度
this.timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
try {
if (started.get()) ScheduleMessageService.this.persist();
} catch (Throwable e) {
log.error("scheduleAtFixedRate flush exception", e);
}
}
}, 10000, this.defaultMessageStore.getMessageStoreConfig().getFlushDelayOffsetInterval());
調度機制
ScheduleMessageService的start方法啓動後,會爲每一個延遲級別創建一個調度任務,每一個延遲級別對應SCHEDULE_TOPIC_XXXX主題下的一個消息消費隊列。定時調度任務的實現類爲DeliverDelayedMessageTimerTask,核心實現方法爲executeOnTimeup
代碼:ScheduleMessageService$DeliverDelayedMessageTimerTask#executeOnTimeup
//根據隊列ID與延遲主題查找消息消費隊列
ConsumeQueue cq =
ScheduleMessageService.this.defaultMessageStore.findConsumeQueue(SCHEDULE_TOPIC,
delayLevel2QueueId(delayLevel));
...
//根據偏移量從消息消費隊列中獲取當前隊列中所有有效的消息
SelectMappedBufferResult bufferCQ = cq.getIndexBuffer(this.offset);
...
//遍歷ConsumeQueue,解析消息隊列中消息
for (; i < bufferCQ.getSize(); i += ConsumeQueue.CQ_STORE_UNIT_SIZE) {
long offsetPy = bufferCQ.getByteBuffer().getLong();
int sizePy = bufferCQ.getByteBuffer().getInt();
long tagsCode = bufferCQ.getByteBuffer().getLong();
if (cq.isExtAddr(tagsCode)) {
if (cq.getExt(tagsCode, cqExtUnit)) {
tagsCode = cqExtUnit.getTagsCode();
} else {
//can't find ext content.So re compute tags code.
log.error("[BUG] can't find consume queue extend file content!addr={}, offsetPy={}, sizePy={}",
tagsCode, offsetPy, sizePy);
long msgStoreTime = defaultMessageStore.getCommitLog().pickupStoreTimestamp(offsetPy, sizePy);
tagsCode = computeDeliverTimestamp(delayLevel, msgStoreTime);
}
}
long now = System.currentTimeMillis();
long deliverTimestamp = this.correctDeliverTimestamp(now, tagsCode);
...
//根據消息偏移量與消息大小,從CommitLog中查找消息.
MessageExt msgExt =
ScheduleMessageService.this.defaultMessageStore.lookMessageByOffset(
offsetPy, sizePy);
}
2.5.8 順序消息
順序消息實現類是org.apache.rocketmq.client.impl.consumer.ConsumeMessageOrderlyService
代碼:ConsumeMessageOrderlyService#start
public void start() {
//如果消息模式爲集羣模式,啓動定時任務,默認每隔20s執行一次鎖定分配給自己的消息消費隊列
if (MessageModel.CLUSTERING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel())) {
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
ConsumeMessageOrderlyService.this.lockMQPeriodically();
}
}, 1000 * 1, ProcessQueue.REBALANCE_LOCK_INTERVAL, TimeUnit.MILLISECONDS);
}
}
代碼:ConsumeMessageOrderlyService#submitConsumeRequest
//構建消息任務,並提交消費線程池中
public void submitConsumeRequest(
final List<MessageExt> msgs,
final ProcessQueue processQueue,
final MessageQueue messageQueue,
final boolean dispathToConsume) {
if (dispathToConsume) {
ConsumeRequest consumeRequest = new ConsumeRequest(processQueue, messageQueue);
this.consumeExecutor.submit(consumeRequest);
}
}
代碼:ConsumeMessageOrderlyService$ConsumeRequest#run
//如果消息隊列爲丟棄,則停止本次消費任務
if (this.processQueue.isDropped()) {
log.warn("run, the message queue not be able to consume, because it's dropped. {}", this.messageQueue);
return;
}
//從消息隊列中獲取一個對象。然後消費消息時先申請獨佔objLock鎖。順序消息一個消息消費隊列同一時刻只會被一個消費線程池處理
final Object objLock = messageQueueLock.fetchLockObject(this.messageQueue);
synchronized (objLock) {
...
}
2.5.9 小結
RocketMQ消息消費方式分別爲集羣模式、廣播模式。
消息隊列負載由RebalanceService線程默認每隔20s進行一次消息隊列負載,根據當前消費者組內消費者個數與主題隊列數量按照某一種負載算法進行隊列分配,分配原則爲同一個消費者可以分配多個消息消費隊列,同一個消息消費隊列同一個時間只會分配給一個消費者。
消息拉取由PullMessageService線程根據RebalanceService線程創建的拉取任務進行拉取,默認每次拉取32條消息,提交給消費者消費線程後繼續下一次消息拉取。如果消息消費過慢產生消息堆積會觸發消息消費拉取流控。
併發消息消費指消費線程池中的線程可以併發對同一個消息隊列的消息進行消費,消費成功後,取出消息隊列中最小的消息偏移量作爲消息消費進度偏移量存儲在於消息消費進度存儲文件中,集羣模式消息消費進度存儲在Broker(消息服務器),廣播模式消息消費進度存儲在消費者端。
RocketMQ不支持任意精度的定時調度消息,只支持自定義的消息延遲級別,例如1s、2s、5s等,可通過在broker配置文件中設置messageDelayLevel。
順序消息一般使用集羣模式,是指對消息消費者內的線程池中的線程對消息消費隊列只能串行消費。並併發消息消費最本質的區別是消息消費時必須成功鎖定消息消費隊列,在Broker端會存儲消息消費隊列的鎖佔用情況。
更多前沿技術,面試技巧,內推信息請掃碼關注公衆號“雲計算平臺技術”