你應該知道的RocketMQ

咖啡拿鐵發表於2022-12-05

1.概述

在很久之前寫過一篇Kafka相關的文章,你需要知道的Kafka,那個時候在業務上更多的是使用的是Kafka,而現在換了公司之後,更多的使用的是Rocketmq,本篇文章會盡力全面的介紹RocketMQ和Kafka各個關鍵點的比較,希望大家讀完能有所收穫。

RocketMQ前身叫做MetaQ, 在MeataQ釋出3.0版本的時候改名為RocketMQ,其本質上的設計思路和Kafka類似,但是和Kafka不同的是其使用Java進行開發,由於在國內的Java受眾群體遠遠多於Scala,所以RocketMQ是很多以Java語言為主的公司的首選。同樣的RocketMQ和Kafka都是Apache基金會中的頂級專案,他們社群的活躍度都非常高,專案更新迭代也非常快。

2.入門例項

2.1 生產者

public class Producer {
    public static void main(String[] args) throws MQClientException, InterruptedException {

        DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName");
        producer.start();

        for (int i = 0; i < 128; i++)
            try {
                {
                    Message msg = new Message("TopicTest",
                        "TagA",
                        "OrderID188",
                        "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));
                    SendResult sendResult = producer.send(msg);
                    System.out.printf("%s%n", sendResult);
                }

            } catch (Exception e) {
                e.printStackTrace();
            }

        producer.shutdown();
    }
}

直接定義好一個producer,建立好Message,呼叫send方法即可。

2.2 消費者

public class PushConsumer {

    public static void main(String[] args) throws InterruptedException, MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("CID_JODIE_1");
        consumer.subscribe("TopicTest""*");
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
        //wrong time format 2017_0422_221800
        consumer.setConsumeTimestamp("20181109221800");
        consumer.registerMessageListener(new MessageListenerConcurrently() {

            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context
{
                System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();
        System.out.printf("Consumer Started.%n");
    }
}

3.RocketMQ架構原理

對於RocketMQ先丟擲幾個問題:

  • RocketMQ的topic和佇列是什麼樣的,和Kafka的分割槽有什麼不同?

  • RocketMQ網路模型是什麼樣的,和Kafka對比如何?

  • RocketMQ訊息儲存模型是什麼樣的,如何保證高可靠的儲存,和Kafka對比如何?

3.1 RocketMQ架構圖

你應該知道的RocketMQ

對於RocketMQ的架構圖,在大體上來看和Kafka並沒有太多的差別,但是在很多細節上是有很多差別的,接下來會一一進行講述。

3.2 RocketMQ名詞解釋

在3.1的架構中我們有多個Producer,多個主Broker,多個從Broker,每個Producer可以對應多個Topic,每個Consumer也可以消費多個Topic。

Broker資訊會上報至NameServer,Consumer會從NameServer中拉取Broker和Topic的資訊。

  • Producer:訊息生產者,向Broker傳送訊息的客戶端

  • Consumer:訊息消費者,從Broker讀取訊息的客戶端

  • Broker:訊息中間的處理節點,這裡和kafka不同,kafka的Broker沒有主從的概念,都可以寫入請求以及備份其他節點資料,RocketMQ只有主Broker節點才能寫,一般也透過主節點讀,當主節點有故障或者一些其他特殊情況才會使用從節點讀,有點類似- 於mysql的主從架構。

  • Topic:訊息主題,一級訊息型別,生產者向其傳送訊息, 消費者讀取其訊息。

  • Group:分為ProducerGroup,ConsumerGroup,代表某一類的生產者和消費者,一般來說同一個服務可以作為Group,同一個Group一般來說傳送和消費的訊息都是一樣的。

  • Tag:Kafka中沒有這個概念,Tag是屬於二級訊息型別,一般來說業務有關聯的可以使用同一個Tag,比如訂單訊息佇列,使用Topic_Order,Tag可以分為Tag_食品訂單,Tag_服裝訂單等等。

  • Queue: 在kafka中叫Partition,每個Queue內部是有序的,在RocketMQ中分為讀和寫兩種佇列,一般來說讀寫佇列數量一致,如果不一致就會出現很多問題。

  • NameServer:Kafka中使用的是ZooKeeper儲存Broker的地址資訊,以及Broker的Leader的選舉,在RocketMQ中並沒有採用選舉Broker的策略,所以採用了無狀態的NameServer來儲存,由於NameServer是無狀態的,叢集節點之間並不會通訊,所以上傳資料的時候都需要向所有節點進行傳送。

很多朋友都在問什麼是無狀態呢?狀態的有無實際上就是資料是否會做儲存,有狀態的話資料會被持久化,無狀態的服務可以理解就是一個記憶體服務,NameServer本身也是一個記憶體服務,所有資料都儲存在記憶體中,重啟之後都會丟失。

3.3 Topic和Queue

在RocketMQ中的每一條訊息,都有一個Topic,用來區分不同的訊息。一個主題一般會有多個訊息的訂閱者,當生產者釋出訊息到某個主題時,訂閱了這個主題的消費者都可以接收到生產者寫入的新訊息。

在Topic中有分為了多個Queue,這其實是我們傳送/讀取訊息通道的最小單位,我們傳送訊息都需要指定某個寫入某個Queue,拉取訊息的時候也需要指定拉取某個Queue,所以我們的順序訊息可以基於我們的Queue維度保持佇列有序,如果想做到全域性有序那麼需要將Queue大小設定為1,這樣所有的資料都會在Queue中有序。

你應該知道的RocketMQ

在上圖中我們的Producer會透過一些策略進行Queue的選擇:

  • 非順序訊息:非順序訊息一般直接採用輪訓傳送的方式進行傳送。

  • 順序訊息:根據某個Key比如我們常見的訂單Id,使用者Id,進行Hash,將同一類資料放在同一個佇列中,保證我們的順序性。

我們同一組Consumer也會根據一些策略來選Queue,常見的比如平均分配或者一致性Hash分配。

要注意的是當Consumer出現下線或者上線的時候,這裡需要做重平衡,也就是Rebalance,RocketMQ的重平衡機制如下:

  • 定時拉取broker,topic的最新資訊

  • 每隔20s做重平衡

  • 隨機選取當前Topic的一個主Broker,這裡要注意的是不是每次重平衡所有主Broker都會被選中,因為會存在一個Broker再多個Broker的情況。

  • 獲取當前Broker,當前ConsumerGroup的所有機器ID。

  • 然後進行策略分配。

由於重平衡是定時做的,所以這裡有可能會出現某個Queue同時被兩個Consumer消費,所以會出現訊息重複投遞。

Kafka的重平衡機制和RocketMQ不同,Kafka的重平衡是透過Consumer和Coordinator聯絡來完成的,當Coordinator感知到消費組的變化,會在心跳過程中傳送重平衡的訊號,然後由一個ConsumerLeader進行重平衡選擇,然後再由Coordinator將結果通知給所有的消費者。

3.3.1 Queue讀寫數量不一致

在RocketMQ中Queue被分為讀和寫兩種,在最開始接觸RocketMQ的時候一直以為讀寫佇列數量配置不一致不會出現什麼問題的,比如當消費者機器很多的時候我們配置很多讀的佇列,但是實際過程中發現會出現訊息無法消費和根本沒有訊息消費的情況。

  • 當寫的佇列數量大於讀的佇列的數量,當大於讀佇列這部分ID的寫佇列的資料會無法消費,因為不會將其分配給消費者。

  • 當讀的佇列數量大於寫的佇列數量,那麼多的佇列數量就不會有訊息被投遞進來。

這個功能在RocketMQ在我看來明顯沒什麼用,因為基本上都會設定為讀寫佇列大小一樣,這個為啥不直接將其進行統一,反而容易讓使用者配置不一樣出現錯誤。

這個問題在RocketMQ的Issue裡也沒有收到好的答案。

3.4 消費模型

一般來說訊息佇列的消費模型分為兩種,基於推送的訊息(push)模型和基於拉取(poll)的訊息模型。

基於推送模型的訊息系統,由訊息代理記錄消費狀態。訊息代理將訊息推送到消費者後,標記這條訊息為已經被消費,但是這種方式無法很好地保證消費的處理語義。比如當我們把已經把訊息傳送給消費者之後,由於消費程式掛掉或者由於網路原因沒有收到這條訊息,如果我們在消費代理將其標記為已消費,這個訊息就永久丟失了。如果我們利用生產者收到訊息後回覆這種方法,訊息代理需要記錄消費狀態,這種不可取。

用過RocketMQ的同學肯定不禁會想到,在RocketMQ中不是提供了兩種消費者嗎?
MQPullConsumerMQPushConsumer,其中MQPushConsumer不就是我們的推模型嗎?其實這兩種模型都是客戶端主動去拉訊息,其中的實現區別如下:

  • MQPullConsumer:每次拉取訊息需要傳入拉取訊息的offset和每次拉取多少訊息量,具體拉取哪裡的訊息,拉取多少是由客戶端控制。

  • MQPushConsumer:同樣也是客戶端主動拉取訊息,但是訊息進度是由服務端儲存,Consumer會定時上報自己消費到哪裡,所以Consumer下次消費的時候是可以找到上次消費的點,一般來說使用PushConsumer我們不需要關心offset和拉取多少資料,直接使用即可。

3.4.1 叢集消費和廣播消費

消費模式我們分為兩種,叢集消費,廣播消費:

  • 叢集消費: 同一個GroupId都屬於一個叢集,一般來說一條訊息只會被任意一個消費者處理。

  • 廣播消費:廣播消費的訊息會被叢集中所有消費者進行訊息,但是要注意一下因為廣播消費的offset在服務端儲存成本太高,所以客戶端每一次重啟都會從最新訊息消費,而不是上次儲存的offset。

3.5 網路模型

在Kafka中使用的原生的socket實現網路通訊,而RocketMQ使用的是Netty網路框架,現在越來越多的中介軟體都不會直接選擇原生的socket,而是使用的Netty框架,主要得益於下面幾個原因:

  • API使用簡單,不需要關心過多的網路細節,更專注於中介軟體邏輯。

  • 效能高。

  • 成熟穩定,jdk nio的bug都被修復了。

選擇框架是一方面,而想要保證網路通訊的高效,網路執行緒模型也是一方面,我們常見的有1+N(1個Acceptor執行緒,N個IO執行緒),1+N+M(1個acceptor執行緒,N個IO執行緒,M個worker執行緒)等模型,RocketMQ使用的是1+N1+N2+M的模型,如下圖所示:

你應該知道的RocketMQ

1個acceptor執行緒,N1個IO執行緒,N2個執行緒用來做Shake-hand,SSL驗證,編解碼;M個執行緒用來做業務處理。這樣的好處將編解碼,和SSL驗證等一些可能耗時的操作放在了一個單獨的執行緒池,不會佔據我們業務執行緒和IO執行緒。


3.6 高可靠的分散式儲存模型

做為一個好的訊息系統,高效能的儲存,高可用都不可少。

3.6.1 高效能日誌儲存

RocketMQ和Kafka的儲存核心設計有很大的不同,所以其在寫入效能方面也有很大的差別,這是16年阿里中介軟體團隊對RocketMQ和Kafka不同Topic下做的效能測試:

你應該知道的RocketMQ

從圖上可以看出:


  • Kafka在Topic數量由64增長到256時,吞吐量下降了98.37%。

  • RocketMQ在Topic數量由64增長到256時,吞吐量只下降了16%。
    這是為什麼呢?kafka一個topic下面的所有訊息都是以partition的方式分散式的儲存在多個節點上。同時在kafka的機器上,每個Partition其實都會對應一個日誌目錄,在目錄下面會對應多個日誌分段。所以如果Topic很多的時候Kafka雖然寫檔案是順序寫,但實際上檔案過多,會造成磁碟IO競爭非常激烈。

那RocketMQ為什麼在多Topic的情況下,依然還能很好的保持較多的吞吐量呢?我們首先來看一下RocketMQ中比較關鍵的檔案:

你應該知道的RocketMQ

這裡有四個目錄(這裡的解釋就直接用RocketMQ官方的了):


  • commitLog:訊息主體以及後設資料的儲存主體,儲存Producer端寫入的訊息主體內容,訊息內容不是定長的。單個檔案大小預設1G ,檔名長度為20位,左邊補零,剩餘為起始偏移量,比如00000000000000000000代表了第一個檔案,起始偏移量為0,檔案大小為1G=1073741824;當第一個檔案寫滿了,第二個檔案為00000000001073741824,起始偏移量為1073741824,以此類推。訊息主要是順序寫入日誌檔案,當檔案滿了,寫入下一個檔案;

  • config:儲存一些配置資訊,包括一些Group,Topic以及Consumer消費offset等資訊。

  • consumeQueue:訊息消費佇列,引入的目的主要是提高訊息消費的效能,由於RocketMQ是基於主題topic的訂閱模式,訊息消費是針對主題進行的,如果要遍歷commitlog檔案中根據topic檢索訊息是非常低效的。Consumer即可根據ConsumeQueue來查詢待消費的訊息。其中,ConsumeQueue(邏輯消費佇列)作為消費訊息的索引,儲存了指定Topic下的佇列訊息在CommitLog中的起始物理偏移量offset,訊息大小size和訊息Tag的HashCode值。consumequeue檔案可以看成是基於topic的commitlog索引檔案,故consumequeue資料夾的組織方式如下:topic/queue/file三層組織結構,具體儲存路徑為:你應該知道的RocketMQHOME \store\index\${fileName},檔名fileName是以建立時的時間戳命名的,固定的單個IndexFile檔案大小約為400M,一個IndexFile可以儲存 2000W個索引,IndexFile的底層儲存設計為在檔案系統中實現HashMap結構,故rocketmq的索引檔案其底層實現為hash索引。

我們發現我們的訊息主體資料並沒有像Kafka一樣寫入多個檔案,而是寫入一個檔案,這樣我們的寫入IO競爭就非常小,可以在很多Topic的時候依然保持很高的吞吐量。有同學說這裡的ConsumeQueue寫是在不停的寫入呢,並且ConsumeQueue是以Queue維度來建立檔案,那麼檔案數量依然很多,在這裡ConsumeQueue的寫入的資料量很小,每條訊息只有20個位元組,30W條資料也才6M左右,所以其實對我們的影響相對Kafka的Topic之間影響是要小很多的。我們整個的邏輯可以如下:

你應該知道的RocketMQ

Producer不斷的再往CommitLog新增新的訊息,有一個定時任務ReputService會不斷的掃描新新增進來的CommitLog,然後不斷的去構建ConsumerQueue和Index。

注意:這裡指的都是普通的硬碟,在SSD上面多個檔案併發寫入和單個檔案寫入影響不大。

讀取訊息

Kafka中每個Partition都會是一個單獨的檔案,所以當消費某個訊息的時候,會很好的出現順序讀,我們知道OS從物理磁碟上訪問讀取檔案的同時,會順序對其他相鄰塊的資料檔案進行預讀取,將資料放入PageCache,所以Kafka的讀取訊息效能比較好。

RocketMQ讀取流程如下:

  • 先讀取ConsumerQueue中的offset對應CommitLog物理的offset

  • 根據offset讀取CommitLog

ConsumerQueue也是每個Queue一個單獨的檔案,並且其檔案體積小,所以很容易利用PageCache提高效能。而CommitLog,由於同一個Queue的連續訊息在CommitLog其實是不連續的,所以會造成隨機讀,RocketMQ對此做了幾個最佳化:

  • Mmap對映讀取,Mmap的方式減少了傳統IO將磁碟檔案資料在作業系統核心地址空間的緩衝區和使用者應用程式地址空間的緩衝區之間來回進行複製的效能開銷

  • 使用DeadLine排程演算法+SSD儲存盤

  • 由於Mmap對映受到記憶體限制,當不在Mmmap對映這部分資料的時候(也就是訊息堆積過多),預設是記憶體的40%,會將請求傳送到SLAVE,減緩Master的壓力

3.6.2 可用性

3.6.2.1 叢集模式

我們首先需要選擇一種叢集模式,來適應我們可忍耐的可用程度,一般來說分為三種:

  • 單Master:這種模式,可用性最低,但是成本也是最低,一旦當機,所有都不可用。這種一般只適用於本地測試。

  • 單Master多SLAVE:這種模式,可用性一般,如果主當機,那麼所有寫入都不可用,讀取依然可用,如果master磁碟損壞,可以依賴slave的資料。

  • 多Master:這種模式,可用性一般,如果出現部分master當機,那麼這部分master上的訊息都不可消費,也不可寫資料,如果一個Topic的佇列在多個Master上都有,那麼可以保證沒有當機的那部分可以正常消費,寫入。如果master的磁碟損壞會導致訊息丟失。

  • 多Master多Slave:這種模式,可用性最高,但是維護成本也最高,當master當機了之後,只會出現在這部分master上的佇列不可寫入,但是讀取依然是可以的,並且如果master磁碟損壞,可以依賴slave的資料。

一般來說投入生產環境的話都會選擇第四種,來保證最高的可用性。

3.6.2.2 訊息的可用性

當我們選擇好了叢集模式之後,那麼我們需要關心的就是怎麼去儲存和複製這個資料,rocketMQ對訊息的刷盤提供了同步和非同步的策略來滿足我們的,當我們選擇同步刷盤之後,如果刷盤超時會給返回FLUSH_DISK_TIMEOUT,如果是非同步刷盤不會返回刷盤相關資訊,選擇同步刷盤可以盡最大程度滿足我們的訊息不會丟失。

除了儲存有選擇之後,我們的主從同步提供了同步和非同步兩種模式來進行復制,當然選擇同步可以提升可用性,但是訊息的傳送RT時間會下降10%左右。

3.6.3 Dleger

我們上面對於master-slave部署模式已經做了很多分析,我們發現,當master出現問題的時候,我們的寫入怎麼都會不可用,除非恢復master,或者手動將我們的slave切換成master,導致了我們的Slave在多數情況下只有讀取的作用。RocketMQ在最近的幾個版本中推出了Dleger-RocketMQ,使用Raft協議複製CommitLog,並且自動進行選主,這樣master當機的時候,寫入依然保持可用。

有關Dleger-RocketMQ的資訊更多的可以檢視這篇文章:Dledger-RocketMQ 基於Raft協議的commitlog儲存庫。

3.7 定時/延時訊息

定時訊息和延時訊息在實際業務場景中使用的比較多,比如下面的一些場景:

  • 訂單超時未支付自動關閉,因為在很多場景中下單之後庫存就被鎖定了,這裡需要將其進行超時關閉。

  • 需要一些延時的操作,比如一些兜底的邏輯,當做完某個邏輯之後,可以傳送延時訊息比如延時半個小時,進行兜底檢查補償。

  • 在某個時間給使用者傳送訊息,同樣也可以使用延時訊息。

在開源版本的RocketMQ中延時訊息並不支援任意時間的延時,需要設定幾個固定的延時等級,目前預設設定為:1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h,從1s到2h分別對應著等級1到18,而阿里雲中的版本(要付錢)是可以支援40天內的任何時刻(毫秒級別)。我們先看下在RocketMQ中定時任務原理圖:

你應該知道的RocketMQ
  • Step1:Producer在自己傳送的訊息上設定好需要延時的級別。

  • Step2: Broker發現此訊息是延時訊息,將Topic進行替換成延時Topic,每個延時級別都會作為一個單獨的queue,將自己的Topic作為額外資訊儲存。

  • Step3: 構建ConsumerQueue

  • Step4: 定時任務定時掃描每個延時級別的ConsumerQueue。

  • Step5: 拿到ConsumerQueue中的CommitLog的Offset,獲取訊息,判斷是否已經達到執行時間

  • Step6: 如果達到,那麼將訊息的Topic恢復,進行重新投遞。如果沒有達到則延遲沒有達到的這段時間執行任務。

可以看見延時訊息是利用新建單獨的Topic和Queue來實現的,如果我們要實現40天之內的任意時間度,基於這種方案,那麼需要402460601000個queue,這樣的成本是非常之高的,那阿里雲上面的支援任意時間是怎麼實現的呢?這裡猜測是持久化二級TimeWheel時間輪,二級時間輪用於替代我們的ConsumeQueue,儲存Commitlog-Offset,然後透過時間輪不斷的取出當前已經到了的時間,然後再次投遞訊息。具體的實現邏輯需要後續會單獨寫一篇文章。

3.8 事務訊息

事務訊息同樣的也是RocketMQ中的一大特色,其可以幫助我們完成分散式事務的最終一致性,有關分散式事務相關的可以看我以前的很多文章都有很多詳細的介紹,這裡直接關注公眾號:咖啡拿鐵。

你應該知道的RocketMQ

具體使用事務訊息步驟如下:

  • Step1:呼叫sendMessageInTransaction傳送事務訊息

  • Step2:  如果傳送成功,則執行本地事務。

  • Step3:  如果執行本地事務成功則傳送commit,如果失敗則傳送rollback。

  • Step4:  如果其中某個階段比如commit傳送失敗,rocketMQ會進行定時從Broker回查,本地事務的狀態。

事務訊息的使用整個流程相對之前幾種訊息使用比較複雜,下面是事務訊息實現的原理圖:

你應該知道的RocketMQ
  • Step1: 傳送事務訊息,這裡也叫做halfMessage,會將Topic替換為HalfMessage的Topic。

  • Step2: 傳送commit或者rollback,如果是commit這裡會查詢出之前的訊息,然後將訊息復原成原Topic,並且傳送一個OpMessage用於記錄當前訊息可以刪除。如果是rollback這裡會直接傳送一個OpMessage刪除。

  • Step3: 在Broker有個處理事務訊息的定時任務,定時對比halfMessage和OpMessage,如果有OpMessage且狀態為刪除,那麼該條訊息必定commit或者rollback,所以就可以刪除這條訊息。

  • Step4: 如果事務超時(預設是6s),還沒有opMessage,那麼很有可能commit資訊丟了,這裡會去反查我們的Producer本地事務狀態。

  • Step5: 根據查詢出來的資訊做Step2。

我們發現RocketMQ實現事務訊息也是透過修改原Topic資訊,和延遲訊息一樣,然後模擬成消費者進行消費,做一些特殊的業務邏輯。當然我們還可以利用這種方式去做RocketMQ更多的擴充套件。

4.總結

這裡讓我們在回到文章中提到的幾個問題:

  • RocketMQ的topic和佇列是什麼樣的,和Kafka的分割槽有什麼不同?

  • RocketMQ網路模型是什麼樣的,和Kafka對比如何?

  • RocketMQ訊息儲存模型是什麼樣的,如何保證高可靠的儲存,和Kafka對比如何?

想必讀完這篇文章,你心中已經有答案。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31555607/viewspace-2665781/,如需轉載,請註明出處,否則將追究法律責任。

相關文章