RocketMQ學習3-原理

消息存儲,高可用機制,負載均衡,消息重試,死信隊列,消息冪等

消息存儲:爲了保障高可用需要持久化:

存儲介質:

1)關係型數據庫DB:Apache下開源的另外一款MQ—ActiveMQ(默認採用的KahaDB做消息存儲)可選用JDBC的方式來做消息持久化,通過簡單的xml配置信息即可實現JDBC消息存儲。

2)文件系統:(RocketMQ/Kafka/RabbitMQ)均採用的是消息刷盤至所部署虛擬機/物理機的文件系統來做持久化

存儲過程讀和寫是如何保持高速的:

寫:RocketMQ的消息用順序寫,保證了消息存儲的速度:目前的高性能磁盤,順序寫速度可以達到600MB/s, 超過了一般網卡的傳輸速度。 磁盤隨機寫的速度只有大概100KB/s ,和順序寫的性能相差6000倍!

讀:Linux操作系統分爲【用戶態】和【內核態】,文件操作、網絡操作需要涉及這兩種形態的切換 。一臺服務器 把本機磁盤文件的內容發送到客戶端,一般分爲兩個步驟:

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

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

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

  1. 從磁盤複製數據到內核態內存;

  2. 從內核態內存復 制到用戶態內存;

  3. 然後從用戶態 內存複製到網絡驅動的內核態內存;

  4. 最後是從網絡驅動的內核態內存復 制到網卡中進行傳輸。

這種mmap也就是零拷貝省去向用戶態的內存複製,提高速度。 RocketMQ充分利用了上述特性,提高消息存盤和網絡發送的速度。 這種機制在Java中是通過MappedByteBuffer實現的,但採用MappedByteBuffer這種內存映射的方式有幾個限制,其中之一是一次只能映射1.5~2G 的文件至用戶態的虛擬內存,這也是爲何RocketMQ默認設置單個CommitLog日誌數據文件爲1G的原因

消息存儲結構:

RocketMQ消息的存儲是由ConsumeQueue和CommitLog配合完成 的,消息真正的物理存儲文件是CommitLog,ConsumeQueue是消息的邏輯隊列,類似數據庫的索引文件,存儲的是指向物理存儲的地址。每 個Topic下的每個Message Queue都有一個對應的ConsumeQueue文件。還有另外一個hash索引IndexFile:爲了消息查詢提供了一種通過key或時間區間來查詢消息的方法,這種通過IndexFile來查找消息的方法不影響發送與消費消息的主流程,當消息達到CommitLog後,會通過ReputMessageService線程接近實時地將消息轉發給消息消費隊列文件與索引文件。爲了安全起見,RocketMQ引入abort文件,記錄Broker的停機是否是正常關閉還是異常關閉(正常關閉調用鉤子方法刪除abort文件,異常關閉這個文件則會存在),在重啓Broker時爲了保證CommitLog文件,消息消費隊列文件與Hash索引文件的正確性,分別採用不同策略來恢復文件。

刷盤機制:RocketMQ的消息是存儲到磁盤上的 ,有兩種寫磁盤方式,分佈式同步刷盤和異步刷盤:

1)同步刷盤
在返回寫成功狀態時,消息已經被寫入磁盤。具體流程是,消息寫入內存的PAGECACHE後,立刻通知刷盤線程刷盤, 然後等待刷盤完成,刷盤線程執行完成後喚醒等待的線程,返回消息寫 成功的狀態。
2)異步刷盤
在返回寫成功狀態時,消息可能只是被寫入了內存的PAGECACHE(同CommitLog 同等大小堆外內存,該堆外內存將使用內存鎖定,確保不會被置換到虛擬內存中去,消息首先追加到堆外內存(ByteBuffer ),然後CommitRealTimeService線程每隔200ms將ByteBuffer 提交到物理文件的內存映射(MappedByteBuffer ,MappedByteBuffer在內存中追加提交的內容,wrotePosition指針向後移動 )中,commit操作成功返回,將committedPosition位置恢復 ,然後FlushRealTimeService線程默認每500ms將MappedByteBuffer中新追加的內存刷寫到磁盤。如果未開啓transientStorePoolEnable,消息直接追加到物理文件直接映射文件中,然後刷寫到磁盤中),然後啓動專門的刷盤線程定時將內存中的文件數據刷寫到磁盤。 寫操作的返回快,吞吐量大;當內存裏的消息量積累到一定程度時,統一觸發寫磁盤動作,快速寫入。
3)配置
同步刷盤還是異步刷盤,都是通過Broker配置文件裏的flushDiskType 參數設置的,這個參數被配置成SYNC_FLUSH、ASYNC_FLUSH中的 一個。

RocketMQ分佈式集羣是通過Master和Slave的配合達到高可用性:
Master和Slave的區別:在Broker的配置文件中,參數 brokerId的值爲0表明這個Broker是Master,大於0表明這個Broker是 Slave,同時brokerRole參數也會說明這個Broker是Master還是Slave。Master角色的Broker支持讀和寫,Slave角色的Broker僅支持讀,也就是 Producer只能和Master角色的Broker連接寫入消息。讀高可用:當Master不可用或者繁忙的時候,Consumer會被自動切換到從Slave 讀。寫高可用:在創建Topic的時候,把Topic的多個MessageQueue創建在多個Broker組上(相同Broker名稱,不同brokerId的機器組成一個Broker組),這樣當一個Broker組的Master不可用後,其他組的Master仍然可用,Producer仍然可以發送消息。(開源版)RocketMQ目前還不支持把Slave自動轉成Master,如果機器資源不足,需要把Slave轉成Master,則要手動停止Slave角色的Broker,更改配置文件,用新的配置文件啓動Broker。向某一個broker發送消息失敗會設置規避機制。

主從複製:消息需要從Master複製到Slave 上,有同步和異步兩種複製方式。 同步複製方式是等Master和Slave均寫 成功後才反饋給客戶端寫成功狀態,Slave 數據完整,吞吐量低;異步複製方式是隻要Master寫成功 即可反饋給客戶端寫成功狀態,Slave 數據可能丟失。通過Broker配置文件裏的brokerRole參數進行設置的,這個參數可以被設置成ASYNC_MASTER、 SYNC_MASTER、SLAVE

負載均衡:

生產者:Producer端,每個實例在發消息的時候,默認會輪詢所有的message queue發送,以達到讓消息平均落在不同的queue上。而由於queue可以散落在不同的broker,所以消息就發送到不同的broker下。消息生產者(Producer)在發送消息時之前先從NameServer獲取Broker服務器地址列表,然後根據負載均衡算法從列表中選擇一臺服務器進行發送。

消費者:集羣模式下:在拉取的時候需要明確指定拉取哪一條message queue。而每當實例的數量有變更,都會觸發一次所有實例的負載均衡(rebalance:RebalanceService線程來實現。一個MQClientInstance持有一個RebalanceService實現,並隨着MQClientInstance的啓動而啓動,默認每隔20s進行一次消息隊列負載 ),這時候會按照queue的數量和實例的數量平均分配queue給每個實例。一個queue只分給一個consumer實例,一個consumer實例可以允許同時分到不同的queue。 但是如果consumer實例的數量比message queue的總數量還多的話,多出來的consumer實例將無法分到queue,也就無法消費到消息,也就無法起到分攤負載的作用了。所以需要控制讓queue的總數量大於等於consumer的數量。廣播模式下:要求一條消息需要投遞到一個消費組下面所有的消費者實例,所以也就沒有消息被分攤消費的說法。

消息傳遞方式:推模式、拉模式。所謂的拉模式,是消費端主動拉起拉消息請求,而推模式是消息達到消息服務器後,推送給消息消費者。RocketMQ消息推模式的實現基於拉模式,在拉模式上包裝一層,一個拉取任務完成後開始下一個拉取任務,循環向消息服務端發起消息拉取請求 。RocketMQ使用一個單獨的線程PullMessageService來負責消息的拉取 ,PullMessageService根據RebalanceService線程創建的拉取任務 從消息服務器默認每次拉取32條消息,按照消息的隊列偏移量順序存放在ProcessQueue中,PullMessageService然後將消息提交到消費者消費線程池(ConsumeMessageService 使用線程池來消費消息,確保了消息拉取與消息消費的解耦。ConsumeMessageService支持順序消息和併發消息,區別是消息消費時必須成功鎖定消息消費隊列,在Broker端會存儲消息消費隊列的鎖佔用情況 ),消息成功消費後從ProcessQueue中移除。(ProcessQueue是MessageQueue在消費端的重現、快照 )

拉取消息可設置是否開啓長輪詢,如果不啓用長輪詢機制,則會在服務端等待shortPollingTimeMills時間後(掛起)再去判斷消息是否已經到達指定消息隊列,如果消息仍未到達則提示拉取消息客戶端PULL—NOT—FOUND(消息不存在)。開啓長輪詢:每隔5s(或有消息到達時)輪詢檢查一次消息是否可達,同時一有消息達到後立馬通知掛起線程

消息重試:

順序消息失敗後會自動不斷進行消息重試,每次間隔1秒。對於無須消息可以通過設置返回狀態達到消息重試的結果。 無序消息的重試只針對集羣消費方式生效;廣播方式不提供失敗重試特性。RocketMQ 默認允許每條消息最多重試 16 次(可自定義),間隔從10s到2小時。總和最多到 4 小時 46 分鐘,超過這個時間範圍消息將不再重試投遞。

在集羣模式小消費者監聽設置返回狀態有三種選擇:
//方式1:返回 Action.ReconsumeLater,消息將重試(推薦)
return Action.ReconsumeLater;
//方式2:返回 null,消息將重試
return null;
//方式3:直接拋出異常, 消息將重試
throw new RuntimeException("XXX");

return Action.CommitMessage;則不會重試、message.getReconsumeTimes()返回重試次數。

死信隊列:

正常情況下無法被消費的消息(超過重試次數)稱爲死信消息,有效期與正常消息相同,均爲 3 天,3 天后會被自動刪除(磁盤空間不足或者默認凌晨4點刪除過期文件,文件保存72小時並且在刪除文件時並不會判斷該消息文件上的消息是否被消費。)。 一個死信隊列對應一個 Group ID, 而不是對應單個消費者實例。 排查可疑因素並解決問題後,可以在消息隊列 RocketMQ 控制檯重新發送該消息,讓消費者重新消費一次。

消息冪等:

如果網絡抖動,生產者推MQ沒有收到確認返回會再推一遍,MQ沒有收到消費者確認返回會再投遞,負載均衡時Broker 重啓以及訂閱方應用重啓 Rebalance也會導致消息重複。

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

Message message = new Message();
message.setKey("ORDERID_100");

RocketMQ支持局部順序消息消費,也就是保證同一個消息隊列上的消息順序消費。不支持消息全局順序消費,如果要實現某一個主題的全局順序消費,可以將該主題的隊列數設置爲1,犧牲高可用性。

 

定時消息是用scheduleAtFixedRate實現的,不支持任意精度的定時調度消息,只支持自定義的消息延遲級別,例如1s、2s、5s等,可通過在broker配置文件中設置messageDelayLevel。

 

原理:

nameServer啓動步驟:1)解析配置文件,填充NameServerConfig、NettyServerConfig屬性值,並創建NamesrvController 2)根據啓動屬性創建NamesrvController實例,並初始化該實例。NameServerController實例爲NameServer核心控制器 3)在JVM進程關閉之前,先將線程池關閉,及時釋放資源

一個Topic擁有多個消息隊列,一個Broker爲每一個主題創建4個讀隊列和4個寫隊列。多個Broker組成一個集羣,集羣由相同的多臺Broker組成Master-Slave架構,brokerId爲0代表Master,大於0爲Slave。BrokerLiveInfo中的lastUpdateTimestamp存儲上次收到Broker心跳包的時間。

路由註冊和維護如下圖

路由發現是非實時的,當Topic路由出現變化後,NameServer不會主動推送給客戶端,而是由客戶端定時拉取主題最新的路由。 Broker失效,移除該Broker,關閉與Broker連接,同時更新topicQueueTablebrokerAddrTablebrokerLiveTablefilterServerTable

RocketMQ有兩個觸發點來刪除路由信息:
NameServer定期掃描brokerLiveTable檢測上次心跳包與當前系統的時間差,如果時間超過120s,則需要移除broker。
Broker在正常關閉的情況下,會執行unregisterBroker指令

消息消費者:

 

 

 

 

 

 

 

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