RocketMQ基礎原理 1.MQ的作用、優缺點及對比 2.RocketMQ的結構 3.生產者 4.消費者 5.broker 6.消息類型 7.RocketMQ使用中常見的問題 參考

1.MQ的作用、優缺點及對比

MQ的作用主要有以下三個方面:

  • 異步
    作用:異步能提高系統的響應速度、吞吐量。
  • 解耦
    作用:
    1、服務之間進行解耦,纔可以減少服務之間的影響。提高系統整體的穩定性以及可擴展性。
    2、另外,解耦後可以實現數據分發。生產者發送一個消息後,可以由一個或者多個消費者進行消費,並且消費者的增加或者減少對生產者沒有影響。
  • 削峯
    作用:以穩定的系統資源應對突發的流量衝擊。

引入MQ的缺點:

  • 系統可用性降低
    系統引入的外部依賴增多,系統的穩定性就會變差。一旦MQ宕機,對業務會產生影響。這就需要考慮如何保證MQ的高可用。
  • 系統複雜度提高
    引入MQ後系統的複雜度會大大提高。以前服務之間可以進行同步的服務調用,引入MQ後,會變爲異步調用,數據的鏈路就會變得更復雜。並且還會帶來其他一些問題。比如:如何保證消費不會丟失?不會被重複調用?怎麼保證消息的順序性等問題。
  • 消息一致性問題
    A系統處理完業務,通過MQ發送消息給B、C系統進行後續的業務處理。如果B系統處理成功,C系統處理失敗怎麼辦?這就需要考慮如何保證消息數據處理的一致性。

2.RocketMQ的結構

RocketMQ由以下這幾個組件組成

  • NameServer : 提供輕量級的Broker路由服務。
  • Broker:實際處理消息存儲、轉發等服務的核心組件。
  • Producer:消息生產者集羣。通常是業務系統中的一個功能模塊。
  • Consumer:消息消費者集羣。通常也是業務系統中的一個功能模塊。

3.生產者

  • 消息發送者的固定步驟
    1.創建消息生產者producer,並制定生產者組名
    2.指定Nameserver地址
    3.啓動producer
    4.創建消息對象,指定主題Topic、Tag和消息體
    5.發送消息
    6.關閉生產者producer

消息生產者分別通過三種方式發送消息,同步發送、異步發送以及單向發送。

  • 1、同步發送消息Producer
  • 2、異步發送AsyncProducer
  • 3、單向發送消息producer.sendOneWay

3.1 生產者負載均衡

Producer發送消息時,默認會輪詢目標Topic下的所有MessageQueue,並採用遞增取模的方式往不同的MessageQueue上發送消息,以達到讓消息平均落在不同的queue上的目的。而由於MessageQueue是分佈在不同的Broker上的,所以消息也會發送到不同的broker上。

同時生產者在發送消息時,可以指定一個MessageQueueSelector。通過這個對象來將消息發送到自己指定的MessageQueue上。這樣可以保證消息局部有序。

4.消費者

  • 消息消費者的固定步驟
    1.創建消費者Consumer,制定消費者組名
    2.指定Nameserver地址
    3.訂閱主題Topic和Tag
    4.設置回調函數,處理消息
    5.啓動消費者consumer

消費者消費消息有兩種模式,一種是消費者主動去Broker上拉取消息的拉模式,另一種是消費者等待Broker把消息推送過來的推模式。

  • 拉模式:DefaultMQPullConsumerImpl這個消費者類已標記爲過期,但是還是可以使用的。替換的類是DefaultLitePullConsumerImpl。
  • 推模式:實際上RocketMQ的推模式也是由拉模式封裝出來的。

4.1 消費者負載均衡

Consumer也是以MessageQueue爲單位來進行負載均衡。分爲集羣模式和廣播模式。

1、集羣模式

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

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

每次分配時,都會將MessageQueue和消費者ID進行排序後,再用不同的分配算法進行分配。內置的分配的算法共有六種,分別對應AllocateMessageQueueStrategy下的六種實現類,可以在consumer中直接set來指定。默認情況下使用的是最簡單的平均分配策略。

  • AllocateMachineRoomNearby: 將同機房的Consumer和Broker優先分配在一起。
    這個策略可以通過一個machineRoomResolver對象來定製Consumer和Broker的機房解析規則。然後還需要引入另外一個分配策略來對同機房的Broker和Consumer進行分配。一般也就用簡單的平均分配策略或者輪詢分配策略。
  • AllocateMessageQueueAveragely:平均分配。將所有MessageQueue平均分給每一個消費者
  • AllocateMessageQueueAveragelyByCircle: 輪詢分配。輪流的給一個消費者分配一個MessageQueue。
  • AllocateMessageQueueByConfig: 不分配,直接指定一個messageQueue列表。類似於廣播模式,直接指定所有隊列。
  • AllocateMessageQueueByMachineRoom:按邏輯機房的概念進行分配。又是對BrokerName和ConsumerIdc有定製化的配置。
  • AllocateMessageQueueConsistentHash。源碼中有測試代碼AllocateMessageQueueConsitentHashTest。這個一致性哈希策略只需要指定一個虛擬節點數,是用的一個哈希環的算法,虛擬節點是爲了讓Hash數據在換上分佈更爲均勻。

2、廣播模式

廣播模式下,每一條消息都會投遞給訂閱了Topic的所有消費者實例,所以也就沒有消息分配這一說。而在實現上,就是在Consumer分配Queue時,所有Consumer都分到所有的Queue。

廣播模式實現的關鍵是將消費者的消費偏移量不再保存到broker當中,而是保存到客戶端當中,由客戶端自行維護自己的消費偏移量。

4.2 消息重試

首先對於廣播模式的消息, 是不存在消息重試的機制的,即消息消費失敗後,不會再重新進行發送,而只是繼續消費新的消息。而對於普通的消息,當消費者消費消息失敗後,你可以通過設置返回狀態達到消息重試的結果。

如何讓消息進行重試

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

  • 返回Action.ReconsumeLater-推薦
  • 返回null
  • 拋出異常

重試消息如何處理
重試的消息會進入一個 “%RETRY%”+ConsumeGroup 的隊列中。
然後RocketMQ默認允許每條消息最多重試16次,每次重試的間隔時間如下

重試次數:
如果消息重試16次後仍然失敗,消息將不再投遞。轉爲進入死信隊列。
另外一條消息無論重試多少次,這些重試消息的MessageId始終都是一樣的。
然後關於這個重試次數,RocketMQ可以進行定製。例如通過consumer.setMaxReconsumeTimes(20);將重試次數設定爲20次。當定製的重試次數超過16次後,消息的重試時間間隔均爲2小時。

關於MessageId:
在老版本的RocketMQ中,一條消息無論重試多少次,這些重試消息的MessageId始終都是一樣的
但是在4.9.1版本中,每次重試MessageId都會重建。

配置覆蓋:
消息最大重試次數的設置對相同GroupID下的所有Consumer實例有效。並且最後啓動的Consumer會覆蓋之前啓動的Consumer的配置。

4.3 死信隊列

當一條消息消費失敗,RocketMQ就會自動進行消息重試。而如果消息超過最大重試次數,RocketMQ就會認爲這個消息有問題。但是此時,RocketMQ不會立刻將這個有問題的消息丟棄,而會將其發送到這個消費者組對應的一種特殊隊列:死信隊列。

RocketMQ默認的重試次數是16次。見源碼org.apache.rocketmq.common.subscription.SubscriptionGroupConfig中的retryMaxTimes屬性。
這個重試次數可以在消費者端進行配置。 例如 DefaultMQPushConsumer實例中有個setMaxReconsumeTimes方法指定重試次數。

死信隊列的名稱是%DLQ%+ConsumGroup

死信隊列的特徵:

  • 一個死信隊列對應一個ConsumGroup,而不是對應某個消費者實例。
  • 如果一個ConsumeGroup沒有產生死信隊列,RocketMQ就不會爲其創建相應的死信隊列。
  • 一個死信隊列包含了這個ConsumeGroup裏的所有死信消息,而不區分該消息屬於哪個Topic。
  • 死信隊列中的消息不會再被消費者正常消費。
  • 死信隊列的有效期跟正常消息相同。默認3天,對應broker.conf中的fileReservedTime屬性。超過這個最長時間的消息都會被刪除,而不管消息是否消費過。

通常,一條消息進入了死信隊列,意味着消息在消費處理的過程中出現了比較嚴重的錯誤,並且無法自行恢復。此時,一般需要人工去查看死信隊列中的消息,對錯誤原因進行排查。然後對死信消息進行處理,比如轉發到正常的Topic重新進行消費,或者丟棄。

注:默認創建出來的死信隊列,他裏面的消息是無法讀取的,在控制檯和消費者中都無法讀取。這是因爲這些默認的死信隊列,他們的權限perm被設置成了2:禁讀(這個權限有三種 2:禁讀,4:禁寫,6:可讀可寫)。需要手動將死信隊列的權限配置成6,才能被消費(可以通過mqadmin指定或者web控制檯)。

4.4 消息冪等

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

  • 發送時消息重複
    當一條消息已被成功發送到服務端並完成持久化,此時出現了網絡閃斷或者客戶端宕機,導致服務端對客戶端應答失敗。 如果此時生產者意識到消息發送失敗並嘗試再次發送消息,消費者後續會收到兩條內容相同並且 Message ID 也相同的消息。
  • 投遞時消息重複
    消息消費的場景下,消息已投遞到消費者並完成業務處理,當客戶端給服務端反饋應答的時候網絡閃斷。 爲了保證消息至少被消費一次,消息隊列 RocketMQ 的服務端將在網絡恢復後再次嘗試投遞之前已被處理過的消息,消費者後續會收到兩條內容相同並且 Message ID 也相同的消息。
  • 負載均衡時消息重複(包括但不限於網絡抖動、Broker 重啓以及訂閱方應用重啓)
    當消息隊列 RocketMQ 的 Broker 或客戶端重啓、擴容或縮容時,會觸發 Rebalance,此時消費者可能會收到重複消息。

從上面的分析中,我們知道,在RocketMQ中,是無法保證每個消息只被投遞一次的,所以要在業務上自行來保證消息消費的冪等性。

而要處理這個問題,RocketMQ的每條消息都有一個唯一的MessageId,這個參數在多次投遞的過程中是不會改變的,所以業務上可以用這個MessageId來作爲判斷冪等的關鍵依據。

但是,這個MessageId是無法保證全局唯一的,也會有衝突的情況。所以在一些對冪等性要求嚴格的場景,最好是使用業務上唯一的一個標識比較靠譜。例如訂單ID。而這個業務標識可以使用Message的Key來進行傳遞。

5.broker

5.1 讀隊列與寫隊列

在RocketMQ的管理控制檯創建Topic時,可以看到要單獨設置讀隊列和寫隊列。通常在運行時,都需要設置讀隊列=寫隊列。

perm字段表示Topic的權限。有三個可選項。 2:禁寫禁訂閱,4:可訂閱,不能寫,6:可寫可訂閱

這其中,寫隊列會真實的創建對應的存儲文件,負責消息寫入。而讀隊列會記錄Consumer的Offset,負責消息讀取。這其實是一種讀寫分離的思想。RocketMQ在配置MessageQueue的路由策略時,就可以通過指向不同的隊列來實現讀寫分離。

5.2 消息持久化

RocketMQ消息直接採用磁盤文件保存消息,默認路徑在${user_home}/store目錄。這些存儲目錄可以在broker.conf中自行指定。

存儲文件主要分爲三個部分:

  • CommitLog:存儲消息的元數據。所有消息都會順序存入到CommitLog文件當中。CommitLog由多個文件組成,每個文件固定大小1G。以第一條消息的偏移量爲文件名。
  • ConsumerQueue:存儲消息在CommitLog的索引。一個MessageQueue一個文件,記錄當前MessageQueue被哪些消費者組消費到了哪一條CommitLog。
  • IndexFile:爲了消息查詢提供了一種通過key或時間區間來查詢消息的方法,這種通過IndexFile來查找消息的方法不影響發送與消費消息的主流程

另外,還有幾個輔助的存儲文件:

  • checkpoint:數據存盤檢查點。裏面主要記錄commitlog文件、ConsumeQueue文件以及IndexFile文件最後一次刷盤的時間戳。
  • config/*.json:這些文件是將RocketMQ的一些關鍵配置信息進行存盤保存。例如Topic配置、消費者組配置、消費者組消息偏移量Offset 等等一些信息。
  • abort:這個文件是RocketMQ用來判斷程序是否正常關閉的一個標識文件。正常情況下,會在啓動時創建,而關閉服務時刪除。但是如果遇到一些服務器宕機,或者kill -9這樣一些非正常關閉服務的情況,這個abort文件就不會刪除,因此RocketMQ就可以判斷上一次服務是非正常關閉的,後續就會做一些數據恢復的操作。

整體的消息存儲結構如下圖:



1、CommitLog文件存儲所有消息實體。所有生產者發過來的消息,都會無差別的依次存儲到Commitlog文件當中。這樣的好處是可以減少查找目標文件的時間,讓消息以最快的速度落盤。對比Kafka存文件時,需要尋找消息所屬的Partition文件,再完成寫入,當Topic比較多時,這樣的Partition尋址就會浪費比較多的時間,所以Kafka不太適合多Topic的場景。而RocketMQ的這種快速落盤的方式在多Topic場景下,優勢就比較明顯。

**文件結構:**CommitLog的文件大小是固定的,但是其中存儲的每個消息單元長度是不固定的,具體格式可以參考org.apache.rocketmq.store.CommitLog

正因爲消息的記錄大小不固定,所以RocketMQ在每次存CommitLog文件時,都會去檢查當前CommitLog文件空間是否足夠,如果不夠的話,就重新創建一個CommitLog文件。文件名爲當前消息的偏移量。

2、ConsumeQueue文件主要是加速消費者的消息索引。他的每個文件夾對應RocketMQ中的一個MessageQueue,文件夾下的文件記錄了每個MessageQueue中的消息在CommitLog文件當中的偏移量。這樣,消費者通過ComsumeQueue文件,就可以快速找到CommitLog文件中感興趣的消息記錄。而消費者在ConsumeQueue文件當中的消費進度,會保存在config/consumerOffset.json文件當中。

文件結構:每個ConsumeQueue文件固定由30萬個固定大小20byte的數據塊組成,數據塊的內容包括:msgPhyOffset(8byte,消息在文件中的起始位置)+msgSize(4byte,消息在文件中佔用的長度)+msgTagCode(8byte,消息的tag的Hash值)。

在ConsumeQueue.java當中有一個常量CQ_STORE_UNIT_SIZE=20,這個常量就表示一個數據塊的大小。

3、IndexFile文件主要是輔助消息檢索。消費者進行消息消費時,通過ConsumeQueue文件就足夠完成消息檢索了,但是如果要按照MeessageId或者MessageKey來檢索文件,比如RocketMQ管理控制檯的消息軌跡功能,ConsumeQueue文件就不夠用了。IndexFile文件就是用來輔助這類消息檢索的。他的文件名比較特殊,不是以消息偏移量命名,而是用的時間命名。但是其實,他也是一個固定大小的文件。

文件結構:他的文件結構由 indexHeader(固定40byte)+ slot(固定500W個,每個固定20byte) + index(最多500W*4個,每個固定20byte) 三個部分組成。

indexFile的詳細結構有大廠之前面試過,可以參考一下我的博文: https://blog.csdn.net/roykingw/article/details/120086520

5.3 過期文件刪除

消息既然要持久化,就必須有對應的刪除機制。RocketMQ內置了一套過期文件的刪除機制。

首先:如何判斷過期文件

RocketMQ中,CommitLog文件和ConsumeQueue文件都是以偏移量命名,對於非當前寫的文件,如果超過了一定的保留時間,那麼這些文件都會被認爲是過期文件,隨時可以刪除。這個保留時間就是在broker.conf中配置的fileReservedTime屬性。

注意,RocketMQ判斷文件是否過期的唯一標準就是非當前寫文件的保留時間,而並不關心文件當中的消息是否被消費過。所以,RocketMQ的消息堆積也是有時間限度的。

然後:何時刪除過期文件

RocketMQ內部有一個定時任務,對文件進行掃描,並且觸發文件刪除的操作。用戶可以指定文件刪除操作的執行時間。在broker.conf中deleteWhen屬性指定。默認是凌晨四點。

另外,RocketMQ還會檢查服務器的磁盤空間是否足夠,如果磁盤空間的使用率達到一定的閾值,也會觸發過期文件刪除。所以RocketMQ官方就特別建議,broker的磁盤空間不要少於4G。

5.4 高效文件寫

5.4.1 零拷貝技術加速文件讀寫

mmap
以一次文件的讀寫操作爲例,應用程序對磁盤文件的讀與寫,都需要經過內核態與用戶態之間的狀態切換,每次狀態切換的過程中,就需要有大量的數據複製。


在這個過程中,總共需要進行四次數據拷貝。而磁盤與內核態之間的數據拷貝,在操作系統層面已經由CPU拷貝優化成了DMA拷貝。而內核態與用戶態之間的拷貝依然是CPU拷貝。所以,在這個場景下,零拷貝技術優化的重點,就是內核態與用戶態之間的這兩次拷貝。

而mmap文件映射的方式,就是在用戶態不再保存文件的內容,而只保存文件的映射,包括文件的內存起始地址,文件大小等。真實的數據,也不需要在用戶態留存,可以直接通過操作映射,在內核態完成數據複製。

mmap的映射機制由於還是需要用戶態保存文件的映射信息,數據複製的過程也需要用戶態的參與,這其中的變數還是非常多的。所以,mmap機制適合操作小文件,如果文件太大,映射信息也會過大,容易造成很多問題。通常mmap機制建議的映射文件大小不要超過2G 。而RocketMQ做大的CommitLog文件保持在1G固定大小,也是爲了方便文件映射。

sendfile
早期的sendfile實現機制其實還是依靠CPU進行頁緩存與socket緩存區之間的數據拷貝。但是,在後期的不斷改進過程中,sendfile優化了實現機制,在拷貝過程中,並不直接拷貝文件的內容,而是隻拷貝一個帶有文件位置和長度等信息的文件描述符FD,這樣就大大減少了需要傳遞的數據。而真實的數據內容,會交由DMA控制器,從頁緩存中打包異步發送到socket中。

sendfile機制在內核態直接完成了數據的複製,不需要用戶態的參與,所以這種機制的傳輸效率是非常穩定的。sendfile機制非常適合大數據的複製轉移。

5.4.2 順序寫加速文件寫入磁盤

通常應用程序往磁盤寫文件時,由於磁盤空間不是連續的,會有很多碎片。所以我們去寫一個文件時,也就無法把一個文件寫在一塊連續的磁盤空間中,而需要在磁盤多個扇區之間進行大量的隨機寫。這個過程中有大量的尋址操作,會嚴重影響寫數據的性能。而順序寫機制是在磁盤中提前申請一塊連續的磁盤空間,每次寫數據時,就可以避免這些尋址操作,直接在之前寫入的地址後面接着寫就行。

Kafka官方詳細分析過順序寫的性能提升問題。Kafka官方曾說明,順序寫的性能基本能夠達到內存級別。而如果配備固態硬盤,順序寫的性能甚至有可能超過寫內存。而RocketMQ很大程度上借鑑了Kafka的這種思想。

例如可以看下org.apache.rocketmq.store.CommitLog#DefaultAppendMessageCallback中的doAppend方法。在這個方法中,會以追加的方式將消息先寫入到一個堆外內存byteBuffer中,然後再通過fileChannel寫入到磁盤。

5.4.3 刷盤

在操作系統層面,當應用程序寫入一個文件時,文件內容並不會直接寫入到硬件當中,而是會先寫入到操作系統中的一個緩存PageCache中。PageCache緩存以4K大小爲單位,緩存文件的具體內容。這些寫入到PageCache中的文件,在應用程序看來,是已經完全落盤保存好了的,可以正常修改、複製等等。但是,本質上,PageCache依然是內存狀態,所以一斷電就會丟失。因此,需要將內存狀態的數據寫入到磁盤當中,這樣數據才能真正完成持久化,斷電也不會丟失。這個過程就稱爲刷盤。
  • 同步刷盤:

    在返回寫成功狀態時,消息已經被寫入磁盤。具體流程是,消息寫入內存的PAGECACHE後,立刻通知刷盤線程刷盤, 然後等待刷盤完成,刷盤線程執行完成後喚醒等待的線程,返回消息寫 成功的狀態。RocketMQ是有個定時任務,10ms刷一次盤。並不是完全的同步。

  • 異步刷盤:

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

  • 配置方式:

    刷盤方式是通過Broker配置文件裏的flushDiskType 參數設置的,這個參數被配置成SYNC_FLUSH、ASYNC_FLUSH中的 一個。

    同步刷盤機制會更頻繁的調用fsync,所以吞吐量相比異步刷盤會降低,但是數據的安全性會得到提高。
    

5.5 消息主從複製

果Broker以一個集羣的方式部署,會有一個master節點和多個slave節點,消息需要從Master複製到Slave上。而消息複製的方式分爲同步複製和異步複製。

  • 同步複製:

同步複製是等Master和Slave都寫入消息成功後才反饋給客戶端寫入成功的狀態。

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

  • 異步複製:

異步複製是隻要master寫入消息成功,就反饋給客戶端寫入成功的狀態。然後再異步的將消息複製給Slave節點。

在異步複製下,系統擁有較低的延遲和較高的吞吐量。但是如果master節點故障,而有些數據沒有完成複製,就會造成數據丟失。

  • 配置方式:

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

5.6 Dledger集羣

Dledger是RocketMQ自4.5版本引入的實現高可用集羣的一項技術。他基於Raft算法進行構建,在RocketMQ的主從集羣基礎上,增加了自動選舉的功能。當master節點掛了之後,會在集羣內自動選舉出一個新的master節點。雖然Dledger機制目前還在不斷驗證改進的階段,但是作爲基礎的Raft算法,已經是目前互聯網行業非常認可的一種高可用算法了。Kafka目前也在基於Raft算法,構建擺脫Zookeeper的集羣化方案。

RocketMQ中的Dledger集羣主要包含兩個功能:1、從集羣中選舉產生master節點。2、優化master節點往slave節點的消息同步機制。

先來看第一個功能:Dledger是使用Raft算法來進行節點選舉的。

首先:每個節點有三個狀態,Leader,follower和candidate(候選人)。正常運行的情況下,集羣中會有一個leader,其他都是follower,follower只響應Leader和Candidate的請求,而客戶端的請求全部由Leader處理,即使有客戶端請求到了一個follower,也會將請求轉發到leader。

集羣剛啓動時,每個節點都是follower狀態,之後集羣內部會發送一個timeout信號,所有follower就轉成candidate去拉取選票,獲得大多數選票的節點選爲leader,其他候選人轉爲follower。如果一個timeout信號發出時,沒有選出leader,將會重新開始一次新的選舉。而Leader節點會往其他節點發送心跳信號,確認他的leader狀態。然後會啓動定時器,如果在指定時間內沒有收到Leader的心跳,就會轉爲Candidate狀態,然後向其他成員發起投票請求,如果收到半數以上成員的投票,則Candidate會晉升爲Leader。然後leader也有可能會退化成follower。

然後,在Raft協議中,會將時間分爲一些任意時間長度的時間片段,叫做term。term會使用一個全局唯一,連續遞增的編號作爲標識,也就是起到了一個邏輯時鐘的作用。

在每一個term時間片裏,都會進行新的選舉,每一個Candidate都會努力爭取成爲leader。獲得票數最多的節點就會被選舉爲Leader。被選爲Leader的這個節點,在一個term時間片裏就會保持leader狀態。這樣,就會保證在同一時間段內,集羣中只會有一個Leader。在某些情況下,選票可能會被各個節點瓜分,形成不了多數派,那這個term可能直到結束都沒有leader,直到下一個term再重新發起選舉,這也就沒有了Zookeeper中的腦裂問題。而在每次重新選舉的過程中, leader也有可能會退化成爲follower。也就是說,在這個集羣中, leader節點是會不斷變化的。

然後,每次選舉的過程中,每個節點都會存儲當前term編號,並在節點之間進行交流時,都會帶上自己的term編號。如果一個節點發現他的編號比另外一個小,那麼他就會將自己的編號更新爲較大的那一個。而如果leader或者candidate發現自己的編號不是最新的,他就會自動轉成follower。如果接收到的請求term編號小於自己的編號,term將會拒絕執行。

在選舉過程中,Raft協議會通過心跳機制發起leader選舉。節點都是從follower狀態開始的,如果收到了來自leader或者candidate的心跳RPC請求,那他就會保持follower狀態,避免爭搶成爲candidate。而leader會往其他節點發送心跳信號,來確認自己的地位。如果follower一段時間(兩個timeout信號)內沒有收到Leader的心跳信號,他就會認爲leader掛了,發起新一輪選舉。

選舉開始後,每個follower會增加自己當前的term,並將自己轉爲candidate。然後向其他節點發起投票請求,請求時會帶上自己的編號和term,也就是說都會默認投自己一票。之後candidate狀態可能會發生以下三種變化:

  • 贏得選舉,成爲leader: 如果它在一個term內收到了大多數的選票,將會在接下的剩餘term時間內稱爲leader,然後就可以通過發送心跳確立自己的地位。(每一個server在一個term內只能投一張選票,並且按照先到先得的原則投出)
  • 其他節點成爲leader: 在等待投票時,可能會收到其他server發出心跳信號,說明其他leader已經產生了。這時通過比較自己的term編號和RPC過來的term編號,如果比對方大,說明leader的term過期了,就會拒絕該RPC,並繼續保持候選人身份; 如果對方編號不比自己小,則承認對方的地位,轉爲follower。
  • 選票被瓜分,選舉失敗: 如果沒有candidate獲取大多數選票, 則沒有leader產生, candidate們等待超時後發起另一輪選舉. 爲了防止下一次選票還被瓜分,必須採取一些額外的措施, raft採用隨機election timeout(隨機休眠時間)的機制防止選票被持續瓜分。通過將timeout隨機設爲一段區間上的某個值, 因此很大概率會有某個candidate率先超時然後贏得大部分選票。

所以以三個節點的集羣爲例,選舉過程會是這樣的:

  1. 集羣啓動時,三個節點都是follower,發起投票後,三個節點都會給自己投票。這樣一輪投票下來,三個節點的term都是1,是一樣的,這樣是選舉不出Leader的。
  2. 當一輪投票選舉不出Leader後,三個節點會進入隨機休眠,例如A休眠1秒,B休眠3秒,C休眠2秒。
  3. 一秒後,A節點醒來,會把自己的term加一票,投爲2。然後2秒時,C節點醒來,發現A的term已經是2,比自己的1大,就會承認A是Leader,把自己的term也更新爲2。實際上這個時候,A已經獲得了集羣中的多數票,2票,A就會被選舉成Leader。這樣,一般經過很短的幾輪選舉,就會選舉出一個Leader來。
  4. 到3秒時,B節點會醒來,他也同樣會承認A的term最大,他是Leader,自己的term也會更新爲2。這樣集羣中的所有Candidate就都確定成了leader和follower.
  5. 然後在一個任期內,A會不斷髮心跳給另外兩個節點。當A掛了後,另外的節點沒有收到A的心跳,就會都轉化成Candidate狀態,重新發起選舉。

然後,Dledger還會採用Raft協議進行多副本的消息同步

使用Dledger集羣后,數據主從同步會分爲兩個階段,一個是uncommitted階段,一個是commited階段。

Leader Broker上的Dledger收到一條數據後,會標記爲uncommitted狀態,然後他通過自己的DledgerServer組件把這個uncommitted數據發給Follower Broker的DledgerServer組件。

接着Follower Broker的DledgerServer收到uncommitted消息之後,必須返回一個ack給Leader Broker的Dledger。然後如果Leader Broker收到超過半數的Follower Broker返回的ack之後,就會把消息標記爲committed狀態。

再接下來, Leader Broker上的DledgerServer就會發送committed消息給Follower Broker上的DledgerServer,讓他們把消息也標記爲committed狀態。這樣,就基於Raft協議完成了兩階段的數據同步。

6.消息類型

6.1 順序消息

一個訂單產生了三條消息分別是訂單創建、訂單付款、訂單完成。消費時要按照這個順序消費纔能有意義,但是同時訂單之間是可以並行消費的。

順序消息分爲全局順序消息與部分順序消息,全局順序是指某個Topic下的所有消息都要保證順序;部分順序消息只要保證每一組消息被順序消費即可。

如果想要實現全局順序消息,那麼只能使用一個隊列,以及單個生產者,這是會嚴重影響性能。

順序消費實際上有兩個核心點,一個是生產者有序存儲,另一個是消費者有序消費。

生產者有序發送:

  • 只需要保證一組相同的消息按照給定的順序存入同一個隊列中,就能保證生產者有序存儲。
  • RocketMQ支持生產者在投放消息的時候自定義投放策略,實現一個MessageQueueSelector接口,使用Hash取模法來保證同一個訂單在同一個隊列中就行了,即通過訂單ID%隊列數量得到該ID的訂單所投放的隊列在隊列列表中的索引,然後該訂單的所有消息都會被投放到這個隊列中。
  • 順序消息必須使用同步發送的方式才能保證生產者發送的消息有序。
  • 實際上,採用隊列選擇器的方法不能保證消息的嚴格順序,我們的目的是將消息發送到同一個隊列中,如果某個broker掛了,那麼隊列就會減少一部分,如果採用取餘的方式投遞,將可能導致同一個業務中的不同消息被髮送到不同的隊列中,導致同一個業務的不同消息被存入不同的隊列中,短暫的造成部分消息無序。同樣的,如果增加了服務器,那麼也會造成短暫的造成部分消息無序。

消費者有序消費:

  • RockerMQ的MessageListener回調函數提供了兩種消費模式,有序消費模式MessageListenerOrderly和併發消費模式MessageListenerConcurrently。在消費的時候,還需要保證消費者註冊MessageListenerOrderly類型的回調接口實現順序消費,如果消費者採用Concurrently並行消費,則仍然不能保證消息消費順序。
  • 實際上,每一個消費者的的消費端都是採用線程池實現多線程消費的模式,即消費端是多線程消費。雖然MessageListenerOrderly被稱爲有序消費模式,但是仍然是使用的線程池去消費消息。
  • MessageListenerConcurrently是拉取到新消息之後就提交到線程池去消費,而MessageListenerOrderly則是通過加分佈式鎖和本地鎖保證同時只有一條線程去消費一個隊列上的數據。

順序消費模式使用3把鎖來保證消費的順序性:

  • broker端的分佈式鎖:
    1)在負載均衡的處理新分配隊列的updateProcessQueueTableInRebalance方法,以及ConsumeMessageOrderlyService服務啓動時的start方法中,都會嘗試向broker申請當前消費者客戶端分配到的messageQueue的分佈式鎖。
    2)broker端的分佈式鎖存儲結構爲ConcurrentMap<String, ConcurrentHashMap<MessageQueue, LockEntry>>,該分佈式鎖保證同一個consumerGroup下同一個messageQueue只會被分配給一個consumerClient。
    3)獲取到的broker端的分佈式鎖,在client端的表現形式爲processQueue. locked屬性爲true,且該分佈式鎖在broker端默認60s過期,而在client端默認30s過期,因此ConsumeMessageOrderlyService#start會啓動一個定時任務,每過20s向broker申請分佈式鎖,刷新過期時間。而負載均衡服務也是每20s進行一次負載均衡。
    4)broker端的分佈式鎖最先被獲取到,如果沒有獲取到,那麼在負載均衡的時候就不會創建processQueue了也不會提交對應的消費請求了。
  • messageQueue的本地synchronized鎖:
    1)在執行消費任務的開頭,便會獲取該messageQueue的本地鎖對象objLock,它是一個Object對象,然後通過synchronized實現鎖定。
    2)這個鎖的鎖對象存儲在MessageQueueLock.mqLockTable屬性中,結構爲ConcurrentMap<MessageQueue, Object>,所以說,一個MessageQueue對應一個鎖,不同的MessageQueue有不同的鎖。
    3)因爲順序消費也是通過線程池消費的,所以這個synchronized鎖用來保證同一時刻對於同一個隊列只有一個線程去消費它。
  • ProcessQueue的本地consumeLock:
    1)在獲取到broker端的分佈式鎖以及messageQueue的本地synchronized鎖的之後,在執行真正的消息消費的邏輯messageListener#consumeMessage之前,會獲取ProcessQueue的consumeLock,這個本地鎖是一個ReentrantLock。
    2)那麼這把鎖有什麼作用呢?
     2-1)在負載均衡時,如果某個隊列C被分配給了新的消費者,那麼當前客戶端消費者需要對該隊列進行釋放,它會調用removeUnnecessaryMessageQueue方法對該隊列C請求broker端分佈式鎖的解鎖。
     2-2)而在請求broker分佈式鎖解鎖的時候,一個重要的操作就是首先嚐試獲取這個messageQueue對應的ProcessQueue的本地consumeLock。只有獲取了這個鎖,才能嘗試請求broker端對該messageQueue的分佈式鎖解鎖。
     2-3)如果consumeLock加鎖失敗,表示當前消息隊列正在消息,不能解鎖。那麼本次就放棄解鎖了,移除消息隊列失敗,只有等待下次重新分配消費隊列時,再進行移除。
    3)如果沒有這把鎖,假設該消息隊列因爲負載均衡而被分配給其他客戶端B,但是由於客戶端A正在對於拉取的一批消費消息進行消費,還沒有提交消費點位,如果此時客戶端A能夠直接請求broker對該messageQueue解鎖,這將導致客戶端B獲取該messageQueue的分佈式鎖,進而消費消息,而這些沒有commit的消息將會發送重複消費。
    4)所以說這把鎖的作用,就是防止在消費消息的過程中,該消息隊列因爲發生負載均衡而被分配給其他客戶端,進而導致的兩個客戶端重複消費消息的行爲。

消費者使用MessageListenerOrderly順序消費有個兩個問題:

  • 使用了很多的鎖,降低了吞吐量。
  • 前一個消息消費阻塞時後面消息都會被阻塞。如果遇到消費失敗的消息,會自動對當前消息進行重試(每次間隔時間爲1秒),無法自動跳過,重試最大次數是Integer.MAX_VALUE,這將導致當前隊列消費暫停,因此通常需要設定有一個最大消費次數,以及處理好所有可能的異常情況。RocketMQ的消費者消息重試和生產者消息重投。

6.2 廣播消息

廣播消息並沒有特定的消息消費者樣例,這是因爲這涉及到消費者的集羣消費模式。在集羣狀態(MessageModel.CLUSTERING)下,每一條消息只會被同一個消費者組中的一個實例消費到(這跟kafka和rabbitMQ的集羣模式是一樣的)。而廣播模式則是把消息發給了所有訂閱了對應主題的消費者,而不管消費者是不是同一個消費者組。

6.3 延遲消息

延遲時間的設置就是在Message消息對象上設置一個延遲級別message.setDelayTimeLevel(3);

開源版本的RocketMQ中,對延遲消息並不支持任意時間的延遲設定(商業版本中支持),而是隻支持18個固定的延遲級別,1到18分別對應messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h。這從哪裏看出來的?其實從rocketmq-console控制檯就能看出來。而這18個延遲級別也支持自行定義,不過一般情況下最好不要自定義修改。

6.4 批量消息

批量消息是指將多條消息合併成一個批量消息,一次發送出去。這樣的好處是可以減少網絡IO,提升吞吐量。

如果批量消息大於1MB就不要用一個批次發送,而要拆分成多個批次消息發送。也就是說,一個批次消息的大小不要超過1MB。

實際使用時,這個1MB的限制可以稍微擴大點,實際最大的限制是4194304字節,大概4MB。但是使用批量消息時,這個消息長度確實是必須考慮的一個問題。而且批量消息的使用是有一定限制的,這些消息應該有相同的Topic,相同的waitStoreMsgOK。而且不能是延遲消息、事務消息等。

6.5 過濾消息

兩種方式:

  • Tag過濾
    TAG是RocketMQ中特有的一個消息屬性。RocketMQ的最佳實踐中就建議,使用RocketMQ時,一個應用可以就用一個Topic,而應用中的不同業務就用TAG來區分。
  • 使用SQL表達式過濾
    sql語句是按照SQL92標準來執行的。只有推模式的消費者可以使用SQL過濾。拉模式是用不了的。

6.6 事務消息

事務消息是在分佈式系統中保證最終一致性的兩階段提交的消息實現。他可以保證本地事務執行與消息發送兩個操作的原子性,也就是這兩個操作一起成功或者一起失敗。

事務消息的關鍵是在TransactionMQProducer中指定了一個TransactionListener事務監聽器,這個事務監聽器就是事務消息的關鍵控制器。

在提交完事務消息後執行:

  • 返回COMMIT_MESSAGE狀態的消息會立即被消費者消費到。
  • 返回ROLLBACK_MESSAGE狀態的消息會被丟棄。
  • 返回UNKNOWN狀態的消息會由Broker過一段時間再來回查事務的狀態。

事務消息機制的關鍵是在發送消息時,會將消息轉爲一個half半消息,並存入RocketMQ內部的一個 RMQ_SYS_TRANS_HALF_TOPIC 這個Topic,這樣對消費者是不可見的。再經過一系列事務檢查通過後,再將消息轉存到目標Topic,這樣對消費者就可見了。

事務消息的使用限制:

  • 1、事務消息不支持延遲消息和批量消息。
  • 2、爲了避免單個消息被檢查太多次而導致半隊列消息累積,我們默認將單個消息的檢查次數限制爲 15 次,但是用戶可以通過 Broker 配置文件的 transactionCheckMax參數來修改此限制。如果已經檢查某條消息超過 N 次的話( N = transactionCheckMax ) 則 Broker 將丟棄此消息,並在默認情況下同時打印錯誤日誌。用戶可以通過重寫 AbstractTransactionCheckListener 類來修改這個行爲。
  • 3、事務消息將在 Broker 配置文件中的參數 transactionMsgTimeout 這樣的特定時間長度之後被檢查。當發送事務消息時,用戶還可以通過設置用戶屬性 CHECK_IMMUNITY_TIME_IN_SECONDS 來改變這個限制,該參數優先於 transactionMsgTimeout 參數。
  • 4、事務性消息可能不止一次被檢查或消費。
  • 5、提交給用戶的目標主題消息可能會失敗,目前這依日誌的記錄而定。它的高可用性通過 RocketMQ 本身的高可用性機制來保證,如果希望確保事務消息不丟失、並且事務完整性得到保證,建議使用同步的雙重寫入機制。
  • 6、事務消息的生產者 ID 不能與其他類型消息的生產者 ID 共享。與其他類型的消息不同,事務消息允許反向查詢、MQ服務器能通過它們的生產者 ID 查詢到消費者。

7.RocketMQ使用中常見的問題

7.1 使用RocketMQ如何保證消息不丟失?


完整方案

  • 發送端:重試機制
  • broker端:
    同步刷盤;
    主從複製改爲同步複製,或者使用Dledger主從架構保證MQ主從複製時不會丟消息。
  • 消費者端不要使用異步消費機制
  • 整個MQ掛了之後準備降級方案:多次嘗試發送RocketMQ不成功,那就只能另外找給地方(Redis、文件或者內存等)把消息緩存下來,然後起一個線程定時的掃描這些失敗的消息,嘗試往RocketMQ發送。這樣等RocketMQ的服務恢復過來後,就能第一時間把這些消息重新發送出去。

7.2 使用RocketMQ如何快速處理積壓消息?

1、如何確定RocketMQ有大量的消息積壓?

在正常情況下,使用MQ都會要儘量保證他的消息生產速度和消費速度整體上是平衡的,但是如果部分消費者系統出現故障,就會造成大量的消息積累。這類問題通常在實際工作中會出現得比較隱蔽。例如某一天一個數據庫突然掛了,大家大概率就會集中處理數據庫的問題。等好不容易把數據庫恢復過來了,這時基於這個數據庫服務的消費者程序就會積累大量的消息。或者網絡波動等情況,也會導致消息大量的積累。這在一些大型的互聯網項目中,消息積壓的速度是相當恐怖的。所以消息積壓是個需要時時關注的問題。

對於消息積壓,如果是RocketMQ或者kafka還好,他們的消息積壓不會對性能造成很大的影響。而如果是RabbitMQ的話,那就慘了,大量的消息積壓可以瞬間造成性能直線下滑。

對於RocketMQ來說,有個最簡單的方式來確定消息是否有積壓。那就是使用web控制檯,就能直接看到消息的積壓情況。

在Web控制檯的主題頁面,可以通過 Consumer管理 按鈕實時看到消息的積壓情況。

2、如何處理大量積壓的消息?

其實我們回顧下RocketMQ的負載均衡的內容就不難想到解決方案。

如果Topic下的MessageQueue配置得是足夠多的,那每個Consumer實際上會分配多個MessageQueue來進行消費。這個時候,就可以簡單的通過增加Consumer的服務節點數量來加快消息的消費,等積壓消息消費完了,再恢復成正常情況。最極限的情況是把Consumer的節點個數設置成跟MessageQueue的個數相同。但是如果此時再繼續增加Consumer的服務節點就沒有用了。

而如果Topic下的MessageQueue配置得不夠多的話,那就不能用上面這種增加Consumer節點個數的方法了。這時怎麼辦呢? 這時如果要快速處理積壓的消息,可以創建一個新的Topic,配置足夠多的MessageQueue。然後把所有消費者節點的目標Topic轉向新的Topic,並緊急上線一組新的消費者,只負責消費舊Topic中的消息,並轉儲到新的Topic中,這個速度是可以很快的。然後在新的Topic上,就可以通過增加消費者個數來提高消費速度了。之後再根據情況恢復成正常情況。

在官網中,還分析了一個特殊的情況。就是如果RocketMQ原本是採用的普通方式搭建主從架構,而現在想要中途改爲使用Dledger高可用集羣,這時候如果不想歷史消息丟失,就需要先將消息進行對齊,也就是要消費者把所有的消息都消費完,再來切換主從架構。因爲Dledger集羣會接管RocketMQ原有的CommitLog日誌,所以切換主從架構時,如果有消息沒有消費完,這些消息是存在舊的CommitLog中的,就無法再進行消費了。這個場景下也是需要儘快的處理掉積壓的消息。

7.3 RocketMQ的消息軌跡

1、RocketMQ消息軌跡數據的關鍵屬性


2、消息軌跡配置

打開消息軌跡功能,需要在broker.conf中打開一個關鍵配置:
traceTopicEnable=true

3、消息軌跡數據存儲

默認情況下,消息軌跡數據是存於一個系統級別的Topic ,RMQ_SYS_TRACE_TOPIC。這個Topic在Broker節點啓動時,會自動創建出來。


在客戶端的兩個核心對象 DefaultMQProducer和DefaultMQPushConsumer,他們的構造函數中,都有兩個可選的參數來打開消息軌跡存儲

  • enableMsgTrace:是否打開消息軌跡。默認是false。
  • customizedTraceTopic:配置將消息軌跡數據存儲到用戶指定的Topic 。

參考

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