RocketMQ - 理論篇

KerryWu發表於2022-02-03

RocketMQ是一個純Java、分散式、佇列模型的開源訊息中介軟體,是阿里參考Kafka特點研發的一個佇列模型的訊息中介軟體,後開源給apache基金會。

我之前寫過 RabbitMQ 的文章,畢竟先入為主,後續在介紹 RocketMQ 的功能時,可能會穿插地拿 RabbitMQ 做比較。當前公司內的訊息中介軟體選型,也是從 RabbitMQ 轉為了 RocketMQ,技術總監告訴我的理由也很簡單,因為 RocketMQ 的分散式叢集可用性更高,運維更簡單。

也的確,拋去為大資料而生的 Kafka 不說,這二者除了在架構和使用方式上差距很大,但在實際應用中的效能、效果上差距不大。有人說 RabbitMQ 的響應速度更快,有人說 RocketMQ 的資料吞吐量更高,但也是差距不大,各有千秋。國內也沒有多少公司有那麼大的體量,對效能那麼較真。

1. 基本元件

1.1. 名詞概念

Name Server

Name Server 是 RocketMQ 叢集的協調者,叢集的各個元件是通過 Name Server 獲取各種屬性和地址資訊的。主要功能包括兩部分:

  1. 各個 Broker 定期上報自己的狀態資訊到 Name Server,維持心跳。
  2. 各個客戶端,包括Producer、Consumer,以及命令列工具,通過 Name Server 獲取 Broker 等最新的狀態資訊。

所以,在啟動 Broker、生產者和消費者之前,必須告訴它們 Name Server 的地址。為了提高可靠性,建議啟動多個 Name Server 組成叢集,單獨部署。因此在產線中,可以動態增減 Name Server 叢集中節點的數量。

可以把 Name Server 類比成 Kafka 中的 ZooKeeper,那為什麼不直接用 ZooKeeper 呢?因為 RocketMQ 只能用到 ZooKeeper 的少部分功能,直接用會顯得太重,就自己開發了相較而言更輕量級、更滿足自身特性的 Name Server。

Broker

Broker 主要負責訊息的儲存、投遞和查詢以及服務高可用保證,說白了就是 RocketMQ 的伺服器。Broker 是中介軟體的核心,絕對不能掛,更是要保障它的可靠性,通常會搭建主從高可用架構,因此 Broker 有分 Master Broker(BrokerId 為0)和 Slave Broker(BrokerId 非0)。

每個Broker與Name Server叢集中的所有節點建立長連線,定時註冊Topic資訊到所有Name Server。Broker 啟動後需要完成一次將自己註冊至 Name Server 的操作;隨後每隔 30s 定期向 Name Server 上報 Topic 路由資訊。

Name Server 叢集節點相互不通訊,所以上報資訊時需要上報所有節點。另外 Name Server 是無狀態的,即資料並不會做持久化儲存,全部儲存在記憶體中,重啟後即消失。

Producer

與 Name Server 叢集中的其中一個節點(隨機)建立長連結(Keep-alive),定期從 Name Server 讀取 Topic 路由資訊,並向提供 Topic 服務的 Master Broker 建立長連結,且定時向 Master Broker 傳送心跳。

Consumer

與 Name Server 叢集中的其中一個節點(隨機)建立長連線,定期從 Name Server 拉取 Topic 路由資訊,並向提供 Topic 服務的 Master Broker、Slave Broker 建立長連線,且定時向 Master Broker、Slave Broker 傳送心跳。Consumer 既可以從 Master Broker 訂閱訊息,也可以從 Slave Broker 訂閱訊息,訂閱規則由 Broker 配置決定。

Topic

訊息主題,一級訊息分類,通過Topic對不同的業務訊息進行分類。

Tag

訊息標籤,用來進一步區分某個Topic下的訊息分類,訊息從生產者發出即帶上的屬性。

Queue

對於 RocketMQ 來說,Topic 是邏輯上的概念,一個 Topic 可以分佈在各個 Broker 上,把一個 Topic 分佈在一個 Broker 上的子集定義為一個 Topic 分片。在分片基礎上再等分為若干份(可指定份數)後的其中一份,就是 Queue 佇列。

Queue 是負載均衡過程中資源分配的基本單元。

1.2. Consumer Group 和 Queue

Consumer Group 消費者組

在實際消費訊息時,都需要申明 Topic名、消費者組名。在叢集消費模式下,同一個 Topic 內的訊息,會分別分發給同一個 Consumer Group 內的不同 Consumer,以達到負載均衡的效果。

怎麼理解同一個 Consumer Group 內的不同 Consumer 呢?不同業務系統基於同一個Consumer Group 消費同一個 Topic 內的訊息算是;為了提高併發量,直接將某個業務系統的程式橫向擴充(如:k8s 中增加幾個pod )也算是。

但如果只是在某個 Consumer 的程式碼中,增加幾個執行緒,如 @RocketMQMessageListener.consumeThreadMax,不算是增加 Consumer。只是作為一個 Consumer 在拉取了一批訊息後,增加執行緒去併發執行。

讀、寫佇列

在建立、更改 Topic 時,會要求設定讀佇列數、寫佇列數。
在傳送訊息時,會根據 Topic 寫佇列數返回路由資訊;在消費訊息時,會根據 Topic 讀佇列數返回路由資訊。

讀、寫佇列並非物理上完全對立的佇列,如:

  • 寫佇列8個,讀佇列4個: 會建立8個資料夾(0、1、2、3、4、5、6、7),訊息會傳送給這8個佇列,但消費時只能消費到4個佇列(0、1、2、3),另外4個佇列(4、5、6、7)中的訊息不會被消費到。
  • 寫佇列4個,讀佇列8個: 訊息只會傳送給4個佇列(0、1、2、3),消費時會從8個佇列中消費訊息,但只有(0、1、2、3)佇列中有訊息。如果某個消費者被分配了佇列(4、5、6、7),則什麼訊息也收不到。

這樣來看,最好的方式是 讀佇列數 = 寫佇列數,那 RocketMQ 為什麼還要多此一舉呢?為了方便佇列擴容、縮容。

一個topic在每個broker上建立了128個佇列,現在需要將佇列縮容到64個,怎麼做才能100%不會丟失訊息,並且無需重啟應用程式?最佳實踐:先縮容寫佇列128->64,寫佇列由0 1 2 ……127縮至 0 1 2 ……..63。等到64 65 66……127中的訊息全部消費完後,再縮容讀佇列128->64。如果同時縮容寫佇列和讀佇列,可能會導致部分訊息未被消費。

Consumer Group 和 Queue 有關負載均衡

既然是負載均衡,那麼討論的就是叢集消費模式。之前說過,Queue 是負載均衡過程中資源分配的基本單元

因此,在一個 Consumer Group 內,Consumer 和 Queue 是 1:n 的關係:

  • 一個 Queue 最多隻能分配給一個 Consumer。
  • 一個 Cosumer 可以分配得到多個 Queue。

也因此,Consumer 數量應該小於等於 Queue 數量。如果 Consumer 超過 Queue 數量,那麼多餘的 Consumer 將無法消費訊息。

2. 高可用架構

前面介紹過 RocketMQ 各個元件的名詞概念,那麼現在說說,這些元件是如何搭建成 RocketMQ 的架構的。

2.1. Name Server 叢集

是一個幾乎無狀態節點,可叢集部署,叢集節點間相互獨立沒有資訊交換。其功能主要為更新和發現 Broker 服務,生產者或消費者能夠通過其查詢到各主題相應的 Broker IP 列表

之前說過,Name Server 是獨立的,每臺 NameServer 都會有完整的叢集路由資訊,包括所有的 Broker 節點的資訊,我們的資料資訊等等。所以只要任何一臺 NamseServer 存活下來,就可以儲存 RocketMQ 資訊的正常執行,不會出現故障。

所以為了提高可用性,Name Server 的節點數至少是 2個及以上。雖然可以直接部署在 Broker 所處的機器上,但如果有條件最好單獨部署。

2.2. Broker 叢集

可以搭建的 Broker 叢集有很多種,按照功能性來分:

  • 單節點模式:就一個 Master Broker。
  • 主從模式:每個 Master Broker 配多個 Slave Broker。只有 Master 接受 Topic 建立 Queue,訊息寫入等,Slave 只是同步 Master 上的這些資料。不過有同步/非同步之分。(1)同步,只有當訊息從 Master 同步到 Slave,才算訊息傳送成功;(2)非同步,訊息傳送到 Master 就算髮送成功,後續訊息在從 Master 非同步重新整理到 Slave 上。
  • 多 Master 模式:可以單是多個Master,也可以是多個 Master-Slave 主從,多個Master 可以提高訊息併發性、高可用性,這也是為什麼 Topic 會在各個 Master Broker 上建立分片佇列。
  • Dledger 模式:在主從模式中,Slave 掛掉了影響不大,可如果 Master 掛掉了就都不能用了。除非手動的將某個 Slave Broker 切換為新的 Master Broker。而 Dledger 就解決了這個問題,它可以監控一組 Broker,當 Master 掛掉後,會從餘下的 Slave 中重新選舉新的 Master。

綜上所述,最完美的高可用叢集架構是:多 Master,每個 Master 配置多個 Slave,並且所有主從 Broker 都啟用了 Dledger。

看完這些,第一反應就是這和 Redis 的高可用叢集架構好像,前面多 Master 多 Slave 基本一樣,然後 RocketMQ 的 Dledger 模式,不就對應 Redis 中的 Sentinel 哨兵模式嘛。看來分散式發展到今天,對於高可用架構的方案逐漸穩定且統一了。

2.3. Dledger 模式

DLedger 是 OpenMessaging 中一個基於 Raft 演算法的 CommitLog 儲存庫實現,從 RocketMQ 4.5.0 版本開始,RocketMQ 引入 DLedger 模式來解決了 Broker 組內自動故障轉移的問題。現在用於部署 RocketMQ 叢集最常見的是用 RocketMQ-on-DLedger Group。

RocketMQ-on-DLedger Group 是指一組相同名稱的 Broker,至少需要 3 個節點,通過 Raft 自動選舉出一個 Leader,其餘節點 作為 Follower,並在 Leader 和 Follower 之間複製資料以保證高可用。

RocketMQ-on-DLedger Group 能自動容災切換,並保證資料一致。

RocketMQ-on-DLedger Group 是可以水平擴充套件的,也即可以部署任意多個 RocketMQ-on-DLedger Group 同時對外提供服務。

在基於 RocketMQ-on-DLedger Group 部署時,每個 Broker 節點的配置檔案需要多加一下配置,當然還是針對RocketMQ 4.5.0 以上版本 :

enableDLegerCommitLog:是否啟動 DLedger
dLegerGroup:DLedger Raft Group 的名字,建議和 brokerName 保持一致
dLegerPeers:DLedger Group 內各節點的地址與埠資訊(同一個 Group 內的各個節點配置必須要保證一致)
dLegerSelfId:節點 id, 必須屬於 dLegerPeers 中的一個;同 Group 內各個節點要唯一,例如:第一個節點配置為”n0”,第二個節點配置為”n1”,第三個節點配置為”n2”
sendMessageThreadPoolNums:傳送執行緒個數(建議配置成 CPU 核數)

2.4. 和 RabbitMQ 對比

開頭說,我們的技術總監因為高可用叢集而選擇了 RabbitMQ,那麼可以回顧一下 RabbitMQ 的叢集是什麼樣的。

普通叢集

在多臺機器上啟動多個 RabbitMQ 例項,每個機器啟動一個。建立的 Queue,只會放在一個 RabbitMQ 例項上,但是每個例項都同步 Queue 的後設資料(後設資料可以認為是 Queue 的一些配置資訊,通過後設資料,可以找到 Queue 所在例項)。消費的時候,實際上如果連線到了另外一個例項,那麼那個例項會從 Queue 所在例項上拉取資料過來。

問題在於,如果消費者讀取 Queue 所在的不是所屬例項,還要從原例項拉取資料,有效能開銷。可如果恰好是 Queue 實際所屬例項,那和單節點有啥區別,依然有單節點的效能瓶頸。

而且這種叢集模式,也只能提高了部分併發量,並沒有高可用性。因為其他例項只同步了 Queue 的後設資料,如果 Queue 所處例項當機了,依然拉取不到 Queue 中的訊息。

映象叢集

映象叢集是在前者的模式做了更改。建立的 Queue 還是隻會放在一個例項上,但其他每個例項不光同步 Queue 的後設資料,還同步訊息資料。

這樣就有了高可用性,Queue 所屬例項當機了,未消費完的訊息依然可以從其他例項中讀到。

但這相當於每個例項上,都完整儲存了所有佇列的訊息,不說效能,光對磁碟的要求有多大。

RocketMQ、RabbitMQ 對比

反觀 RocketMQ 的高可用架構則更科學:

  • Topic 在不同 Master Broker 上分片,Queue 分散在不同叢集。雖然 RocketMQ 的主從同步類似於映象,也是將 Queue 的後設資料和訊息都同步過去。但畢竟只是經過拆分過的 Topic 部分資料,量沒那麼大。
  • 讀寫分離,想擴充寫併發就擴充 Master,想擴充讀併發就擴充 Slave,更加靈活。
  • RocketMQ 每個例項各有分工,甚至還有獨立的 Name Server,所以對單個叢集例項的效能消耗不大。而對於 RabbitMQ 而言,每個例項可能會有效能瓶頸,對機器上的物理資源可能要求較高。

3. 其他

3.1. 外部外掛

控制檯 console

RocketMQ 不像 RabbitMQ 自帶 console,不過它對外提供API。目前社群有很多可接入 RocketMQ console 的前端專案,目前我用的是 rocketmq-console-ng,還挺不錯的。

接入 Prometheus

console 畢竟在監控有限,目前使用最廣泛監控解決方案的就是 Prometheus 了吧,RocketMQ 也提供接入的方案。

這裡就提到主角 RocketMQ-Exporter,目前已被 Prometheus 官方收錄,其地址為 https://github.com/apache/roc...。它首先從 RocketMQ 叢集採集資料,然後藉助 Prometheus 提供的第三方客戶端庫將採集的資料規範化成符合 Prometheus 系統要求的資料,Prometheus 定時去從 Exporter 拉取資料即可。

3.2. RocketMQ、RabbitMQ 對比

搭建運維上

在運維搭建時,RabbitMQ 要簡單的多,全程就一個 server,還包含了 console 控制檯。

同樣的情況在 RocketMQ 上則相對複雜,要分別搭建 Name Server、Broker(分 Master、Slave),要 console 也要自己搭。

但複雜有複雜的好處,在高可用叢集上就體現出來了,這裡不再贅言。

開發使用上

RabbitMQ 的 AMQP 協議,相較於 Kafka、RocketMQ 來說,學習成本要高的多。無論是傳送普通訊息,還是複雜一點的像延遲訊息,都需要理解和使用交換機和佇列之間的配合。而在 RocketMQ 上使用時則簡單的多,無法提供一個API即可,框架封裝了很多實現的細節。

有人說 RocketMQ 框架本身較重,但重有重的好處,框架封裝的越多,對於使用的人來說就越方便。像順序訊息、延遲訊息、事務訊息等,幾乎可以開箱即用。最近在學車,感覺 RabbitMQ 像是手動擋,而 RocketMQ 像是自動擋,不知道這個比喻是否貼切。

3.3. docker compose 單機安裝

docker-compose.yml

version: '3.5'
services:
  rmqnamesrv:
    image: rocketmqinc/rocketmq:4.4.0
    container_name: rmqnamesrv
    restart: always
    ports:
      - 9876:9876
    environment:
    #記憶體分配
      JAVA_OPT_EXT: "-server -Xms1g -Xmx1g"
    volumes:
      - /Volumes/rocketmq/namesrv/logs:/root/logs
    command: sh mqnamesrv
    networks:
      rmq:
        aliases:
          - rmqnamesrv
          
  rmqbroker:
    image: rocketmqinc/rocketmq:4.4.0
    container_name: rmqbroker
    restart: always
    depends_on:
      - rmqnamesrv
    ports:
      - 10909:10909
      - 10911:10911
    volumes:
      - /Volumes/rocketmq/broker/logs:/root/logs
      - /Volumes/rocketmq/broker/store:/root/store
      - /Volumes/rocketmq/broker/conf/broker.conf:/opt/rocketmq-4.4.0/conf/broker.conf
    command: sh mqbroker  -c /opt/rocketmq-4.4.0/conf/broker.conf
    environment:
      NAMESRV_ADDR: "rmqnamesrv:9876"
      JAVA_OPT_EXT: "-server -Xms1g -Xmx1g -Xmn1g"
    networks:
      rmq:
        aliases:
          - rmqbroker
          
  rmqconsole:
    image: styletang/rocketmq-console-ng
    container_name: rocketmq-console
    restart: always
    ports:
      - 9877:8080
    depends_on:
      - rmqnamesrv
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /Volumes/rocketmq/console/logs:/root/logs
    environment:
      JAVA_OPTS: "-Drocketmq.namesrv.addr=rmqnamesrv:9876 -Dcom.rocketmq.sendMessageWithVIPChannel=false"
    networks:
      rmq:
        aliases:
          - rmqconsole
          
networks:
  rmq:
    name: rmq
    driver: bridge

相關文章