RabbitMQ是流行的開源消息隊列系統,本身已經具備了較強的並發處理速度及運行穩定性,然而在大規模的實際應用中,往往還需要使用集群配置來保證系統中消息通信部分的高可用性,並發處理性能及異常恢復能力。這里將介紹一種實用的消息集群架構,以及一種能夠快速、高效、可靠地部署並配置消息集群的方式,通過這種方法,我們可以在系統部署時僅需短短幾分鍾就能完成規模化的消息集群架設,極大地提高了工作效率和部署成功率。
集群架構設計
RabbitMQ這款消息隊列中間件產品本身是基於Erlang編寫,Erlang語言天生具備分布式特性(通過同步Erlang集群各節點的magic cookie來實現)。因此,RabbitMQ天然支持Clustering。這使得RabbitMQ本身不需要像ActiveMQ、Kafka那樣通過ZooKeeper分別來實現HA方案和保存集群的元數據。集群是保證可靠性的一種方式,同時可以通過水平擴展以達到增加消息吞吐量能力的目的。
名詞說明:
Broker:它提供一種傳輸服務,它的角色就是維護一條從生產者到消費者的路線,保證數據能按照指定的方式進行傳輸
Exchange:消息交換機,它指定消息按什么規則,路由到哪個隊列
Queue:消息的載體,每個消息都會被投到一個或多個隊列
Binding:綁定,它的作用就是把exchange和queue按照路由規則綁定起來
Routing Key:路由關鍵字,exchange根據這個關鍵字進行消息投遞
vhost:虛擬主機,一個broker里可以有多個vhost,用作不同用戶的權限分離
Producer:消息生產者,就是投遞消息的程序
Consumer:消息消費者,就是接受消息的程序
Channel:消息通道,在客戶端的每個連接里,可建立多個channel
元數據
RabbitMQ 內部有各種基礎構件,包括隊列、交換器、綁定、虛擬主機等,他們組成了 AMQP 協議消息通信的基礎,而這些構件以元數據的形式存在,它始終記錄在 RabbitMQ 內部,它們分別是:
隊列元數據:隊列名稱和它們的屬性
交換器元數據:交換器名稱、類型和屬性
綁定元數據:一張簡單的表格展示了如何將消息路由到隊列
vhost元數據:為 vhost 內的隊列、交換器和綁定提供命名空間和安全屬性
隊列
這里有個問題需要思考,RabbitMQ 默認會將消息冗余到所有節點上嗎?這樣聽起來正符合高可用的特性,只要集群上還有一個節點存活,那么就可以繼續進行消息通信,但這也隨之為 RabbitMQ 帶來了致命的缺點:
每次發布消息,都要把它擴散到所有節點上,而且對於磁盤節點來說,每一條消息都會觸發磁盤活動,這會導致整個集群內性能負載急劇拉升。
如果每個節點都有所有隊列的完整內容,那么添加節點不會給你帶來額外的存儲空間,也會帶來木桶效應,舉個例子,如果集群內有個節點存儲了 3G 隊列內容,那么在另外一個只有 1G 存儲空間的節點上,就會造成內存空間不足的情況,也就是無法通過集群節點的擴容提高消息積壓能力。
解決這個問題就是通過集群中唯一節點來負責任何特定隊列,只有該節點才會受隊列大小的影響,其它節點如果接收到該隊列消息,那么就要根據元數據信息,傳遞給隊列所有者節點(也就是說其它節點上只存儲了特定隊列所有者節點的指針)。這樣一來,就可以通過在集群內增加節點,存儲更多的隊列數據。
exchange
交換器其實本質是一張查詢表,里面包括了交換器名稱和一個隊列的綁定列表,當你將消息發布到交換器中,實際上是你所在的信道將消息上的路由鍵與交換器的綁定列表進行匹配,然后將消息路由出去。有了這個機制,那么在所有節點上傳遞交換器消息將簡單很多,而 RabbitMQ 所做的事情就是把交換器拷貝到所有節點上,因此每個節點上的每條信道都可以訪問完整的交換器了。
http://chyufly.github.io/images/RabbitMQ_cluster_exchanges.PNG
內存節點和磁盤節點
關於上面隊列所說的問題與解決辦法,又有了一個伴隨而來的問題出現:如果特定隊列的所有者節點發生了故障,那么該節點上的隊列和關聯的綁定都會消失嗎?
如果是內存節點,那么附加在該節點上的隊列和其關聯的綁定都會丟失,並且消費者可以重新連接集群並重新創建隊列;
如果是磁盤節點,重新恢復故障后,該隊列又可以進行傳輸數據了,並且在恢復故障磁盤節點之前,不能在其它節點上讓消費者重新連到集群並重新創建隊列,如果消費者繼續在其它節點上聲明該隊列,會得到一個 404 NOT_FOUND 錯誤,這樣確保了當故障節點恢復后加入集群,該節點上的隊列消息不回丟失,也避免了隊列會在一個節點以上出現冗余的問題。
接下來說說內存節點與磁盤節點在集群中的作用,在集群中的每個節點,要么是內存節點,要么是磁盤節點,如果是內存節點,會將所有的元數據信息僅存儲到內存中,而磁盤節點則不僅會將所有元數據存儲到內存上, 還會將其持久化到磁盤。
在單節點 RabbitMQ 上,僅允許該節點是磁盤節點,這樣確保了節點發生故障或重啟節點之后,所有關於系統的配置與元數據信息都會重磁盤上恢復;而在 RabbitMQ 集群上,允許節點上至少有一個磁盤節點,在內存節點上,意味着隊列和交換器聲明之類的操作會更加快速。原因是這些操作會將其元數據同步到所有節點上,對於內存節點,將需要同步的元數據寫進內存就行了,但對於磁盤節點,意味着還需要及其消耗性能的磁盤寫入操作。
RabbitMQ 集群只要求至少有一個磁盤節點,這是有道理的,當其它內存節點發生故障或離開集群,只需要通知至少一個磁盤節點進行元數據的更新,如果是碰巧唯一的磁盤節點也發生故障了,集群可以繼續路由消息,但是不可以做以下操作了:
創建隊列
創建交換器
創建綁定
添加用戶
更改權限
添加或刪除集群節點
這是因為上述操作都需要持久化到磁盤節點上,以便內存節點恢復故障可以從磁盤節點上恢復元數據,解決辦法是在集群添加 2 台以上的磁盤節點,這樣其中一台發生故障了,集群仍然可以保持運行,且能夠在任何時候保存元數據變更。
負載均衡集群部署搭建
負載均衡部署如下圖所示: 4-5節點 如圖所示
經過上面的RabbitMQ10個節點集群搭建和HAProxy軟彈性負載均衡配置后即可組建一個中小規模的RabbitMQ集群了,然而為了能夠在實際的生產環境使用還需要根據實際的業務需求對集群中的各個實例進行一些性能參數指標的監控,從性能、吞吐量和消息堆積能力等角度考慮,可以選擇Kafka來作為RabbitMQ集群的監控隊列使用。因此,一個中小規模RabbitMQ集群架構設計圖如下圖所示
服務器環境: centos7.x 系統 4c 16g 200g
rabbitmq 安裝
每個節點修改host文件
配置各節點的hosts文件( vim /etc/hosts)
xxx.xxx.xxx.xxx mq-1
xxx.xxx.xxx.xxx mq-2
xxx.xxx.xxx.xxx mq-3
...
安裝依賴
-
Jdk 1.8
-
Erlang運行時環境 erlang> = 19.3
-
socat
安裝時候注意版本
### add erlang repo
[rabbitmq-erlang]
name=rabbitmq-erlang
baseurl=https://dl.bintray.com/rabbitmq/rpm/erlang/20/el/7
gpgcheck=1
gpgkey=https://dl.bintray.com/rabbitmq/Keys/rabbitmq-release-signing-key.asc
repo_gpgcheck=0
enabled=1
yum install erlang java-1.8.0-openjdk socat
# 查看erlang包版本
$ rpm -q erlang
erlang-20.3-1.el7.centos.x86_64
修改.erlang.cookie文件
erlang.cookie是erlang實現分布式的必要文件,erlang分布式的每個節點上要保持相同的.erlang.cookie文件,同時保證文件的權限是400
編輯每台RabbitMQ的cookie文件,以確保各個節點的cookie文件使用的是同一個值,可以scp其中一台機器上的cookie至其他各個節點,cookie的默認路徑為/var/lib/rabbitmq/.erlang.cookie或者$HOME/.erlang.cookie,節點之間通過cookie確定相互是否可通信。
添加rabbitmq 3.7.x repo
# import the new PackageCloud key that will be used starting December 1st, 2018 (GMT)
rpm --import https://packagecloud.io/rabbitmq/rabbitmq-server/gpgkey
# import the old PackageCloud key that will be discontinued on December 1st, 2018 (GMT)
rpm --import https://packagecloud.io/gpg.key
## centos7.x add repo
[bintray-rabbitmq-server]
name=bintray-rabbitmq-rpm
baseurl=https://dl.bintray.com/rabbitmq/rpm/rabbitmq-server/v3.7.x/el/7/
gpgcheck=0
repo_gpgcheck=0
enabled=1
## 在每個節點執行install rabbitmq
yum install rabbitmq-server
加入集群
逐個節點啟動RabbitMQ服務
systemctl start rabbitmq-server
查看各個節點和集群的工作運行狀態
rabbitmqctl status, rabbitmqctl cluster_status
以mq-test1為主節點, 在其他節點執行如下命令, 以mq-test2為例
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl join_cluster rabbit@mq-test-2
rabbitmqctl start_app
設置節點類型
在RabbitMQ集群中的節點只有兩種類型:內存節點/磁盤節點,單節點系統只運行磁盤類型的節點。而在集群中,可以選擇配置部分節點為內存節點
內存節點將所有的隊列,交換器,綁定關系,用戶,權限,和vhost的元數據信息保存在內存中。而磁盤節點將這些信息保存在磁盤中,但是內存節點的性能更高,為了保證集群的高可用性,必須保證集群中有兩個以上的磁盤節點,來保證當有一個磁盤節點崩潰了,集群還能對外提供訪問服務。在上面的操作中,可以通過如下的方式,設置新加入的節點為內存節點還是磁盤節點
#加入時候設置節點為內存節點(默認加入的為磁盤節點)
[root@mq-testvm1 ~]# rabbitmqctl join_cluster rabbit@rmq-broker-test-1 --ram
#也通過下面方式修改的節點的類型
[root@mq-testvm1 ~]# rabbitmqctl change_cluster_node_type disc | ram
啟用web管理plugin
rabbitmq-plugins enable rabbitmq_management
設置鏡像隊列
當節點發生故障時,盡管所有元數據信息都可以從磁盤節點上將元數據拷貝到本節點上,但是隊列的消息內容就不行了,這樣就會導致消息的丟失,那是因為在默認情況下,隊列只會保存在其中一個節點上,我們在將集群隊列時也說過。
聰明的 RabbitMQ 早就意識到這個問題了,在 2.6以后的版本中增加了,隊列冗余選項:鏡像隊列。鏡像隊列的主隊列(master)依然是僅存在於一個節點上,其余從主隊列拷貝的隊列叫從隊列(slave)。如果主隊列沒有發生故障,那么其工作流程依然跟普通隊列一樣,生產者和消費者不會感知其變化,當發布消息時,依然是路由到主隊列中,而主隊列通過類似廣播的機制,將消息擴散同步至其余從隊列中,這就有點像 fanout 交換器一樣。而消費者依然是從主隊列中讀取消息。
一旦主隊列發生故障,集群就會從最老的一個從隊列選舉為新的主隊列,這也就實現了隊列的高可用了,但我們切記不要濫用這個機制,在上面也說了,隊列的冗余操作會導致不能通過擴展節點增加存儲空間,而且會造成性能瓶頸。
官網參考文檔: http://www.rabbitmq.com/parameters.html#policies
命令如下
$ rabbitmqctl set_policy [-p Vhost] Name Pattern Definition [Priority]
-p Vhost: 可選參數,針對指定vhost下的queue進行設置
Name: policy的名稱
Pattern: queue的匹配模式(正則表達式)
Definition: 鏡像定義,包括三個部分ha-mode, ha-params, ha-sync-mode
ha-mode: 指明鏡像隊列的模式,有效值為 all/exactly/nodes
all: 表示在集群中所有的節點上進行鏡像
exactly: 表示在指定個數的節點上進行鏡像,節點的個數由ha-params指定
nodes: 表示在指定的節點上進行鏡像,節點名稱通過ha-params指定
ha-params: ha-mode模式需要用到的參數
ha-sync-mode: 進行隊列中消息的同步方式,有效值為automatic和manual
priority: 可選參數,policy的優先級
舉幾個例子
以下示例聲明名為ha-all的策略,它與名稱以”ha”開頭的隊列相匹配,並將鏡像配置到集群中的所有節點:
## 命令會將所有的隊列冗余到所有節點上,一般可以拿來測試。
rabbitmqctl set_policy ha-all "^" '{"ha-mode":"all"}'
##策略的名稱以”two”開始的隊列鏡像到群集中的任意兩個節點,並進行自動同步:
$ rabbitmqctl set_policy ha-two "^two." '{"ha-mode":"exactly","ha-params":2,"ha-sync-mode":"automatic"}'
## 以”node”開頭的隊列鏡像到集群中的特定節點的策略
$ rabbitmqctl set_policy ha-nodes "^nodes." '{"ha-mode":"nodes","ha-params":["rabbit@nodeA", "rabbit@nodeB"]}'
監控
RabbitMQ的Monitoring文檔中介紹了各種監控RabbitMQ(https://www.rabbitmq.com/monitoring.html)的方式。 其中推薦了第三方的Prometheus Plugin(https://github.com/deadtrickster/prometheus_rabbitmq_exporter), 這是一個第三方實現RabbitMQ的管理插件,可以作為Prometheus的RabbitMQ Exporter。
安裝RabbitMQ Prometheus Plugin
這里RabbitMQ的版本為3.7.7,Erlang版本為Erlang 21 [erts-10.0.5],從Release for latest RabbitMQ 3.7.x versions這個鏈接中下載以下插件:
https://github.com/deadtrickster/prometheus_rabbitmq_exporter/releases
## 這是我下載的plugin
[root@fg-rabbitmq-cluster02 mq_plugin]# ls -alh
total 704K
drwxrwxr-x 3 lebosa lebosa 283 Nov 11 09:22 .
drwx------. 4 lebosa lebosa 128 Nov 11 09:22 ..
-rw-rw-r-- 1 lebosa lebosa 14K Nov 11 09:22 accept-0.3.3.ez
-rw-rw-r-- 1 lebosa lebosa 195K Nov 11 09:22 prometheus-3.5.1.ez
-rw-rw-r-- 1 lebosa lebosa 14K Nov 11 09:22 prometheus_cowboy-0.1.4.ez
-rw-rw-r-- 1 lebosa lebosa 22K Nov 11 09:22 prometheus_httpd-2.1.8.ez
-rw-rw-r-- 1 lebosa lebosa 17K Nov 11 09:22 prometheus_process_collector-1.3.1.ez
drwxrwxr-x 5 lebosa lebosa 205 Nov 11 09:22 prometheus_rabbitmq_exporter-3.7.2.2
-rw-rw-r-- 1 lebosa lebosa 210K Nov 11 09:22 prometheus_rabbitmq_exporter-v3.7.2.2.ez
-rw-rw-r-- 1 lebosa lebosa 218K Nov 11 09:22 v3.7.2.2.tar.gz
將下載的插件拷貝到RabbitMQ的插件目錄,rpm形式安裝的RabbitMQ的插件目錄位於-rabbit plugins_dir “/usr/lib/rabbitmq/plugins:/usr/lib/rabbitmq/lib/rabbitmq_server-3.7.7/plugins”下。
查看插件的目錄可以通過
## 通過這個命令可以查看相應的plugin目錄
systemctl status rabbitmq-server -l
## 啟用插件
rabbitmq-plugins enable accept
rabbitmq-plugins enable prometheus
rabbitmq-plugins enable prometheus_httpd
rabbitmq-plugins enable prometheus_rabbitmq_exporter
rabbitmq-plugins enable prometheus_process_collector
## 重啟MQ
systemctl restart rabbitmq-server
## 驗證訪問http://<ip>:15672/api/metrics可以得到RabbitMQ Exporter暴露的metrics。
curl http://127.0.0.1:15672/api/metrics
promethues 配置
Prometheus的配置文件中增加如下內容:
- job_name: 'rabbitmq'
metrics_path: /api/metrics
static_configs:
- targets:
- host1-ip:15672
labels:
instance: s3
- targets:
- host2-ip:15672
labels:
instance: s4
- targets:
- host3-ip:15672
labels:
instance: s5
Grafana配置
導入grafana dashborad https://github.com/deadtrickster/prometheus_rabbitmq_exporter/tree/master/priv/dashboards
參考文檔:https://github.com/deadtrickster/prometheus_rabbitmq_exporter