RabbitMQ 如何做分散式
前言
前面幾篇文章介紹了訊息佇列中遇到的問題,這篇來聊聊 RabbitMQ 的叢集搭建。
叢集配置方案
RabbitMQ 中叢集的部署方案有三種 cluster,federation,shovel。
cluster
cluster 有兩種模式,分別是普通模式和映象模式
cluster 的特點:
1、不支援跨網段,用於同一個網段內的區域網;
2、可以隨意的動態增加或者減少;
3、節點之間需要執行相同版本的 RabbitMQ 和 Erlang 。
普通模式
cluster 普通模式(預設的叢集模式),所有節點中的後設資料是一致的,RabbitMQ 中的後設資料會被複制到每一個節點上。
佇列裡面的資料只會存在建立它的節點上,其他節點除了儲存後設資料,還儲存了指向 Queue 的主節點(owner node)的指標。
叢集中節點之間沒有主從節點之分。
![mq](https://i.iter01.com/images/a9e5cba298d0fa65f824fef5088cc5931118a50314e87692b4696b7417befde7.png)
舉個例子來說明下普通模式的訊息傳輸:
假設我們 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](https://i.iter01.com/images/941b79193a9f75db232e83b711b777f1ce88477edb78fa6dd7fd4bd6a06cb1ed.png)
同時對於佇列的建立,要平均的落在每個節點上,如果只在一個節點上建立佇列,所有的消費,最終都會落到這個節點上,會產生瓶頸。
存在的問題:
如果 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](https://i.iter01.com/images/123d77a4d0201fd599af22dad316e517309b8843c3a8dca8f672f7d7ef5f0bc7.png)
federation
federation 外掛的設計目標是使 RabbitMQ 在不同的 Broker 節點之間進行訊息傳遞而無需建立叢集。
看了定義還是很迷糊,來舉舉栗子吧
假設我們有一個 RabbitMQ 的叢集,分別部署在不同的城市,那麼我們假定分別是在北京,上海,廣州。
![mq](https://i.iter01.com/images/1040034e13b023cd3bdb8d1e92ec85f7f6e703103fb5d7b80cc60ae630c80caf.png)
如果一個現在有一個業務 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](https://i.iter01.com/images/6d53a0952a4bd786ce95cb923f272df42feec9037437a209d7c12cb758670834.png)
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](https://i.iter01.com/images/42d94ab1431832f83e1cda245669d306f3f8cc1e36a27aa81d2b5c002d122481.png)
節點型別
RAM node
記憶體節點將所有的佇列、交換機、繫結、使用者、許可權和 vhost 的後設資料定義儲存在記憶體中,好處是可以使得像交換機和佇列宣告等操作更加的快速。
Disk node
後設資料儲存在磁碟中,單節點系統只允許磁碟型別的節點,防止重啟RabbitMQ的時候,丟失系統的配置資訊
RabbitMQ要求在叢集中至少有一個磁碟節點,所有其他節點可以是記憶體節點,當節點加入或者離開叢集時,必須要將該變更通知到至少一個磁碟節點。
如果叢集中唯一的一個磁碟節點崩潰的話,叢集仍然可以保持執行,但是無法進行其他操作(增刪改查),直到節點恢復。針對這種情況可以設定兩個磁碟節點、至少保證一個是可用的,就能保證後設資料的修改了。
看了很多文章,有的地方會認為所有持久化的訊息都會儲存到磁碟節點中,這是不正確的。對於記憶體節點,如果訊息進行了持久化的操作,持久化的訊息會儲存在該節點中的磁碟中,而不是磁碟節點的磁碟中。
來個栗子:
這裡構建了一個普通的 cluster 叢集(見下文),選擇其中的一個記憶體節點,推送訊息到該節點中,並且每條訊息都選擇持久化,來看下,這個節點的記憶體變化
來看下沒有訊息時,節點中的記憶體佔用
![mq](https://i.iter01.com/images/fc998541b1a28cf16013c40c6b48c8be071792d9c3b0cd19821936c41f58bd84.jpg)
這裡向rabbitmqcluster1
推送了 397330 條訊息,發現磁碟記憶體從原來的 6.1GiB 變成了 3.9GiB,而磁碟節點的記憶體沒有變化
![mq](https://i.iter01.com/images/260e264e4c4b7229b9fc3fc5c6fb39cd68e8c6215cd4807c0ec311cc917c2e03.jpg)
對於記憶體節點,如果訊息進行了持久化的操作,持久化的訊息會儲存在該節點中的磁碟中,而不是磁碟節點的磁碟中。
叢集的搭建
這是搭建一個普通的 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,史上最簡單實用安裝步驟
3、設定不同節點間同一認證的Erlang Cookie
每臺機器中安裝 RabbitMQ ,都會生成單獨的Erlang Cookie
。Erlang 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、將節點加入到叢集中
在 rabbitmqcluster2
和 rabbitmqcluster3
中執行
$ rabbitmqctl stop_app
$ rabbitmqctl join_cluster rabbit@rabbitmqcluster1
$ rabbitmqctl start_app
預設 rabbitmq 啟動後是磁碟節點,所以可以看到叢集啟動之後,節點型別都是磁碟型別
![mq](https://i.iter01.com/images/549bbdbf80d08695cab9f55f584b00a269df1a86f20c4f4b8bb954ef851d18ff.jpg)
一般新增1到2個磁碟節點,別的節點節點為記憶體節點,這裡我們將 rabbitmqcluster3
設定成磁碟節點,其他節點設定成記憶體節點
修改 rabbitmqcluster1
和 rabbitmqcluster2
節點型別為記憶體節點
$ rabbitmqctl stop_app
$ rabbitmqctl change_cluster_node_type ram
$ rabbitmqctl start_app
![mq](https://i.iter01.com/images/013785e93a5fe2039a4299d84996a927fcd9f469d23cbb5d120b97462472eb0f.jpg)
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中的分散式/