RabbitMQ 中的分散式,普通 cluster 模式的構建

Rick.lz 發表於 2022-01-17
RabbitMQ

RabbitMQ 如何做分散式

前言

前面幾篇文章介紹了訊息佇列中遇到的問題,這篇來聊聊 RabbitMQ 的叢集搭建。

叢集配置方案

RabbitMQ 中叢集的部署方案有三種 cluster,federation,shovel。

cluster

cluster 有兩種模式,分別是普通模式和映象模式

cluster 的特點:

1、不支援跨網段,用於同一個網段內的區域網;

2、可以隨意的動態增加或者減少;

3、節點之間需要執行相同版本的 RabbitMQ 和 Erlang 。

普通模式

cluster 普通模式(預設的叢集模式),所有節點中的後設資料是一致的,RabbitMQ 中的後設資料會被複制到每一個節點上。

佇列裡面的資料只會存在建立它的節點上,其他節點除了儲存後設資料,還儲存了指向 Queue 的主節點(owner node)的指標。

叢集中節點之間沒有主從節點之分。

mq

舉個例子來說明下普通模式的訊息傳輸:

假設我們 RabbitMQ 中有是三個節點,分別是 node1,node2,node3。如果佇列 queue1 的連線建立發生在 node1 中,那麼該佇列的後設資料會被同步到所有的節點中,但是 queue1 中的訊息,只會在 node1 中。

  • 如果一個消費者通過 node2 連線,然後來消費 queue1 中的訊息?

RabbitMQ 會臨時在 node1、node2 間進行訊息傳輸,因為非 owner 節點除了儲存後設資料,還會儲存指向 Queue 的主節點(owner node)的指標。RabbitMQ 會根據這個指向,把 node1 中的訊息實體取出並經過 node2 傳送給 consumer 。

  • 如果一個生產者通過 node2 連線,然後來向 queue1 中生產資料?

同理,RabbitMQ 會根據 node2 中的主節點(owner node)的指標,把訊息轉傳送給 owner 節點 node1,最後插入的資料還是在 node1 中。

mq

同時對於佇列的建立,要平均的落在每個節點上,如果只在一個節點上建立佇列,所有的消費,最終都會落到這個節點上,會產生瓶頸。

存在的問題:

如果 node1 節點故障了,那麼 node2 節點無法取出 node1 中還未消費的訊息實體。

1、如果做了佇列的持久化,訊息不會被丟失,等到 node1 恢復了,就能接著進行消費,但是在恢復之前其他節點不能建立 node1 中已將建立的佇列。

2、如果沒有做持久化,訊息會丟失,但是 node1 中的佇列,可以在其他節點重新建立,不用等待 node1 的恢復。

普通模式不支援訊息在每個節點上的複製,當然 RabbitMQ 中也提供了支援複製的模式,就是映象模式(參見下文)。

映象模式

映象佇列會在節點中同步佇列的資料,最終的佇列資料會存在於每個節點中,而不像普通模式中只會存在於建立它的節點中。

優點很明顯,當有主機當機的時候,因為佇列資料會同步到所有節點上,避免了普通模式中的單點故障。

缺點就是效能不好,叢集內部的同步通訊會佔用大量的網路頻寬,適合一些可靠性要求比較高的場景。

針對映象模式 RabbitMQ 也提供了幾種模式,有效值為 all,exactly,nodes 預設為 all。

  • all 表示叢集中所有的節點進行映象;

  • exactly 表示指定個數的節點上進行映象,節點個數由ha-params指定;

  • nodes 表示在指定的節點上進行映象,節點名稱由ha-params指定;

所以針對普通佇列和映象佇列,我們可以選擇其中幾個佇列作為映象佇列,在效能和可靠性之間找到一個平衡。

關於映象模式中訊息的複製,這裡也用的很巧妙,值得借鑑

1、master 節點向 slave 節點同步訊息是通過組播 GM(Guaranteed Multicast) 來同步的。

2、所有的訊息經過 master 節點,master 對訊息進行處理,同時也會通過 GM 廣播給所有的 slave,slave收到訊息之後在進行資料的同步操作。

3、GM 實現的是一種可靠的組播通訊協議,該協議能保證組播訊息的原子性。具體如何實現呢?

它的實現大致為:將所有的節點形成一個迴圈連結串列,每個節點都會監控位於自己左右兩邊的節點,當有節點新增時,相鄰的節點保證當前廣播的訊息會複製到新的節點上 當有節點失效時,相鄰的節點會接管以保證本次廣播的訊息會複製到所有的節點。

因為是一個迴圈連結串列,所以 master 發出去的訊息最後也會返回到 master 中,master 如果收到了自己發出的操作命令,這時候就可以確定命令已經同步到了所有的節點。

mq

federation

federation 外掛的設計目標是使 RabbitMQ 在不同的 Broker 節點之間進行訊息傳遞而無需建立叢集。

看了定義還是很迷糊,來舉舉栗子吧

假設我們有一個 RabbitMQ 的叢集,分別部署在不同的城市,那麼我們假定分別是在北京,上海,廣州。

mq

如果一個現在有一個業務 clientA,部署的機器在北京,然後連線到北京節點的 broker1 。然後網路連通性也很好,傳送訊息到 broker1 中的 exchangeA 中,訊息能夠很快的傳送到,就算在開啟了 publisher confirm 機制或者事務機制的情況下,也能快速確認資訊,這種情況下是沒有問題的。

如果一個現在有一個業務 clientB,部署的機器在上海,然後連線到北京節點的 broker1 。然後網路連通性不好,傳送訊息到 broker1 中的 exchangeA 中,因為網路不好,所以訊息的確認有一定的延遲,這對於我們無疑使災難,訊息量大情況下,必然造成資料的阻塞,在開啟了 publisher confirm 機制或者事務機制的情況下,這種情況將會更嚴重。

當然如果把 clientB ,部署在北京的機房中,這個問題就解決了,但是多地容災就不能實現了。

針對這種情況如何解決呢,這時候 federation 就登場了。

比如位於上海的業務 clientB,連線北京節點的 broker1。然後傳送訊息到 broker1 中的 exchangeA 中。這時候是存在網路連通性的問題的。

  • 1、讓上海的業務 clientB,連線上海的節點 broker2;

  • 2、通過 Federation ,在北京節點的 broker1 和上海節點的 broker2 之間建立一條單向的 Federation link

  • 3、Federation 外掛會在上海節點的 broker2 中建立一個同名的交換器 exchangeA (具體名字可配置,預設同名), 同時也會建立一個內部交換器,通過路由鍵 rkA ,將這兩個交換器進行繫結,同時也會在 broker2 中建立一個

    1、Federation 外掛會在上海節點的 broker2 中建立一個同名的交換器 exchangeA (具體名字可配置,預設同名);

    2、Federation 外掛會在上海節點的 broker2 中建立一個內部交換器,通過路由鍵 rkA ,將 exchangeA 和內部交換器進行繫結;

    3、Federation 外掛會在上海節點的 broker2 中建立佇列,和內部交換器進行繫結,同時這個佇列會和北京節點的 broker1 中的 exchangeA,建立一條 AMQP 連結,來實時的消費佇列中的訊息了;

  • 4、經過上面的流程,就相當於在上海節點 broker2 中的 exchangeA 和北京節點 broker1 中的 exchangeA 建立了Federation link

這樣位於上海的業務 clientB 連結到上海的節點 broker2,然後傳送訊息到該節點中的 exchangeA,這個訊息會通過Federation link,傳送到北京節點 broker1 中的 exchangeA,所以可以減少網路連通性的問題。

mq

shovel

連線方式與 federation 的連線方式類似,不過 shovel 工作更低一層。federation 是從一個交換器中轉發訊息到另一個交換器中,而 shovel 只是簡單的從某個 broker 中的佇列中消費資料,然後轉發訊息到另一個 broker 上的交換器中。

shovel 主要是:保證可靠連續地將 message 從某個 broker 上的 queue (作為源端)中取出,再將其 publish 到另外一個 broker 中的相應 exchange 上(作為目的端)。

作為源的 queue 和作為目的的 exchange 可以同時位於一個 broker 上,也可以位於不同 broker 上。Shovel 行為就像優秀的客戶端應用程式能夠負責連線源和目的地、負責訊息的讀寫及負責連線失敗問題的處理。

Shovel 的主要優勢在於:

1、鬆藕合:Shovel 可以移動位於不同管理域中的 Broker (或者叢集)上的訊息,這些 Broker (或者叢集〉可以包含不同的使用者和 vhost ,也可以使用不同的 RabbitMQ 和 Erlang 版本;

2、支援廣域網:Shovel 外掛同樣基於 AMQP 協議 Broker 之間進行通訊 被設計成可以容忍時斷時續的連通情形 井且能夠保證訊息的可靠性;

3、高度定製:當 Shove 成功連線後,可以對其進行配置以執行相關的 AMQP 命令。

使用 Shove 解決訊息堆積

對於訊息堆積,如果訊息堆積的數量巨大時,訊息佇列的效能將嚴重收到影響,通常的做法是增加消費者的數量或者優化消費者來處理

如果一些訊息堆積場景不能簡單的增加消費者的數量來解決,就只能優化消費者的消費能力了,但是優化畢竟需要時間,這時候可以通過 Shove 解決

可以通過 Shove 將阻塞的訊息,移交給另一個備份佇列,等到本佇列的訊息沒有阻塞了,然後將備份佇列中的訊息重新'鏟'過來

mq

節點型別

RAM node

記憶體節點將所有的佇列、交換機、繫結、使用者、許可權和 vhost 的後設資料定義儲存在記憶體中,好處是可以使得像交換機和佇列宣告等操作更加的快速。

Disk node

後設資料儲存在磁碟中,單節點系統只允許磁碟型別的節點,防止重啟RabbitMQ的時候,丟失系統的配置資訊

RabbitMQ要求在叢集中至少有一個磁碟節點,所有其他節點可以是記憶體節點,當節點加入或者離開叢集時,必須要將該變更通知到至少一個磁碟節點。

如果叢集中唯一的一個磁碟節點崩潰的話,叢集仍然可以保持執行,但是無法進行其他操作(增刪改查),直到節點恢復。針對這種情況可以設定兩個磁碟節點、至少保證一個是可用的,就能保證後設資料的修改了。

看了很多文章,有的地方會認為所有持久化的訊息都會儲存到磁碟節點中,這是不正確的。對於記憶體節點,如果訊息進行了持久化的操作,持久化的訊息會儲存在該節點中的磁碟中,而不是磁碟節點的磁碟中。

來個栗子:

這裡構建了一個普通的 cluster 叢集(見下文),選擇其中的一個記憶體節點,推送訊息到該節點中,並且每條訊息都選擇持久化,來看下,這個節點的記憶體變化

來看下沒有訊息時,節點中的記憶體佔用

mq

這裡向rabbitmqcluster1推送了 397330 條訊息,發現磁碟記憶體從原來的 6.1GiB 變成了 3.9GiB,而磁碟節點的記憶體沒有變化

mq

對於記憶體節點,如果訊息進行了持久化的操作,持久化的訊息會儲存在該節點中的磁碟中,而不是磁碟節點的磁碟中。

叢集的搭建

這是搭建一個普通的 cluster 模式,使用 vagrant 構建三臺 centos7 虛擬機器,vagrant構建centos虛擬環境

1、區域網配置

首先配置 hostname

$ hostnamectl set-hostname rabbitmqcluster1 --static

重啟即可檢視最新的 hostname

$ hostnamectl
   Static hostname: rabbitmqcluster1
         Icon name: computer-vm
           Chassis: vm
        Machine ID: e147b422673549a3b4fda77127bd4bcd
           Boot ID: aa195e0427d74d079ea39f344719f59b
    Virtualization: oracle
  Operating System: CentOS Linux 7 (Core)
       CPE OS Name: cpe:/o:centos:centos:7
            Kernel: Linux 3.10.0-327.4.5.el7.x86_64
      Architecture: x86-64

然後在三個節點的/etc/hosts下設定相同的配置資訊

192.168.56.111 rabbitmqcluster1
192.168.56.112 rabbitmqcluster2
192.168.56.113 rabbitmqcluster3

2、每臺及其中安裝 RabbitMQ

具體的安裝過程可參見Centos7安裝RabbitMQ最新版3.8.5,史上最簡單實用安裝步驟

每臺機器中安裝 RabbitMQ ,都會生成單獨的Erlang CookieErlang Cookie是保證不同節點可以相互通訊的金鑰,要保證叢集中的不同節點相互通訊必須共享相同的Erlang Cookie。具體的目錄存放在/var/lib/rabbitmq/.erlang.cookie

所以這裡把 rabbitmqcluster1 中的Erlang Cookie,複製到其他機器中,覆蓋原來的Erlang Cookie

$ scp /var/lib/rabbitmq/.erlang.cookie 192.168.56.112:/var/lib/rabbitmq
$ scp /var/lib/rabbitmq/.erlang.cookie 192.168.56.113:/var/lib/rabbitmq

複製Erlang Cookie之後重啟 rabbitmq

$ systemctl restart rabbitmq-server

4、使用 -detached執行各節點

rabbitmqctl stop
rabbitmq-server -detached 

5、將節點加入到叢集中

rabbitmqcluster2rabbitmqcluster3 中執行

$ rabbitmqctl stop_app
$ rabbitmqctl join_cluster [email protected]
$ rabbitmqctl start_app

預設 rabbitmq 啟動後是磁碟節點,所以可以看到叢集啟動之後,節點型別都是磁碟型別

mq

一般新增1到2個磁碟節點,別的節點節點為記憶體節點,這裡我們將 rabbitmqcluster3 設定成磁碟節點,其他節點設定成記憶體節點

修改 rabbitmqcluster1rabbitmqcluster2 節點型別為記憶體節點

$ rabbitmqctl stop_app
$ rabbitmqctl change_cluster_node_type ram
$ rabbitmqctl start_app
mq

6、檢視叢集狀態

$ rabbitmqctl cluster_status

參考

【RabbitMQ分散式叢集架構和高可用性(HA)】http://chyufly.github.io/blog/2016/04/10/rabbitmq-cluster/
【RabbitMQ分散式部署方案簡介】https://www.jianshu.com/p/c7a1a63b745d
【RabbitMQ實戰指南】https://book.douban.com/subject/27591386/
【RabbitMQ兩種叢集模式配置管理】https://blog.csdn.net/fgf00/article/details/79558498
【RabbitMQ 中的分散式模式,普通 cluster 模式的構建】https://boilingfrog.github.io/2022/01/07/rabbitmq中的分散式/