RabbitMQ 中的分散式,普通 cluster 模式的構建
RabbitMQ 如何做分散式
前言
前面幾篇文章介紹了消息隊列中遇到的問題,這篇來聊聊 RabbitMQ 的集群搭建。
集群配置方案
RabbitMQ 中集群的部署方案有三種 cluster,federation,shovel。
cluster
cluster 有兩種模式,分別是普通模式和鏡像模式
cluster 的特點:
1、不支援跨網段,用於同一個網段內的區域網;
2、可以隨意的動態增加或者減少;
3、節點之間需要運行相同版本的 RabbitMQ 和 Erlang 。
普通模式
cluster 普通模式(默認的集群模式),所有節點中的元數據是一致的,RabbitMQ 中的元數據會被複制到每一個節點上。
隊列裡面的數據只會存在創建它的節點上,其他節點除了存儲元數據,還存儲了指向 Queue 的主節點(owner node)的指針。
集群中節點之間沒有主從節點之分。
舉個栗子來說明下普通模式的消息傳輸:
假設我們 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 中。
同時對於隊列的創建,要平均的落在每個節點上,如果只在一個節點上創建隊列,所有的消費,最終都會落到這個節點上,會產生瓶頸。
存在的問題:
如果 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 如果收到了自己發出的操作命令,這時候就可以確定命令已經同步到了所有的節點。
federation
federation 插件的設計目標是使 RabbitMQ 在不同的 Broker 節點之間進行消息傳遞而無需建立集群。
看了定義還是很迷糊,來舉舉栗子吧
假設我們有一個 RabbitMQ 的集群,分別部署在不同的城市,那麼我們假定分別是在北京,上海,廣州。
如果一個現在有一個業務 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,所以可以減少網路連通性的問題。
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 將阻塞的消息,移交給另一個備份隊列,等到本隊列的消息沒有阻塞了,然後將備份隊列中的消息重新’鏟’過來
節點類型
RAM node
記憶體節點將所有的隊列、交換機、綁定、用戶、許可權和 vhost 的元數據定義存儲在記憶體中,好處是可以使得像交換機和隊列聲明等操作更加的快速。
Disk node
元數據存儲在磁碟中,單節點系統只允許磁碟類型的節點,防止重啟RabbitMQ的時候,丟失系統的配置資訊
RabbitMQ要求在集群中至少有一個磁碟節點,所有其他節點可以是記憶體節點,當節點加入或者離開集群時,必須要將該變更通知到至少一個磁碟節點。
如果集群中唯一的一個磁碟節點崩潰的話,集群仍然可以保持運行,但是無法進行其他操作(增刪改查),直到節點恢復。針對這種情況可以設置兩個磁碟節點、至少保證一個是可用的,就能保證元數據的修改了。
看了很多文章,有的地方會認為所有持久化的消息都會存儲到磁碟節點中,這是不正確的。對於記憶體節點,如果消息進行了持久化的操作,持久化的消息會存儲在該節點中的磁碟中,而不是磁碟節點的磁碟中。
來個栗子:
這裡構建了一個普通的 cluster 集群(見下文),選擇其中的一個記憶體節點,推送消息到該節點中,並且每條消息都選擇持久化,來看下,這個節點的記憶體變化
來看下沒有消息時,節點中的記憶體佔用
這裡向rabbitmqcluster1
推送了 397330 條消息,發現磁碟記憶體從原來的 6.1GiB 變成了 3.9GiB,而磁碟節點的記憶體沒有變化
對於記憶體節點,如果消息進行了持久化的操作,持久化的消息會存儲在該節點中的磁碟中,而不是磁碟節點的磁碟中。
集群的搭建
這是搭建一個普通的 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 啟動後是磁碟節點,所以可以看到集群啟動之後,節點類型都是磁碟類型
一般添加1到2個磁碟節點,別的節點節點為記憶體節點,這裡我們將 rabbitmqcluster3
設置成磁碟節點,其他節點設置成記憶體節點
修改 rabbitmqcluster1
和 rabbitmqcluster2
節點類型為記憶體節點
$ rabbitmqctl stop_app
$ rabbitmqctl change_cluster_node_type ram
$ rabbitmqctl start_app
6、查看集群狀態
$ rabbitmqctl cluster_status
參考
【RabbitMQ分散式集群架構和高可用性(HA)】//chyufly.github.io/blog/2016/04/10/rabbitmq-cluster/
【RabbitMQ分散式部署方案簡介】//www.jianshu.com/p/c7a1a63b745d
【RabbitMQ實戰指南】//book.douban.com/subject/27591386/
【RabbitMQ兩種集群模式配置管理】//blog.csdn.net/fgf00/article/details/79558498
【RabbitMQ 中的分散式模式,普通 cluster 模式的構建】//boilingfrog.github.io/2022/01/07/rabbitmq中的分散式/