RabbitMQ 集羣高可用原理及實戰部署介紹
在項目中想要 RabbitMQ 變得更加健壯,就要使得其變成高可用,今天我們一起來聊聊關於 RabbitMQ 集羣原理和部署流程
一、介紹
在前幾篇文章中,我們詳細的介紹了 RabbitMQ 的內部結構和使用,以及 SpringBoot 和 RabbitMQ 整合,都是基於單臺 RabbitMQ 進行使用的。
我們知道在微服務流行的當下,一旦單臺服務器掛了,基本上就無法提供高可用的服務了,因此爲了保證服務高可用,在生產環境上我們通常的做法是搭建一個 RabbitMQ 集羣,即使某臺 RabbitMQ 故障了,其他正常的 RabbitMQ 服務器依然可以使用,應用程序的持續運行不會受到影響。
二、集羣架構原理
在前幾篇文章中,我們有介紹到 RabbitMQ 內部有各種基礎構件,包括隊列、交換器、綁定、虛擬主機等,他們組成了 AMQP 協議消息通信的基礎,而這些構件以元數據的形式存在,它始終記錄在 RabbitMQ 內部,它們分別是:
-
隊列元數據:隊列名稱和它們的屬性
-
交換器元數據:交換器名稱、類型和屬性
-
綁定元數據:一張簡單的表格展示瞭如何將消息路由到隊列
-
vhost 元數據:爲 vhost 內的隊列、交換器和綁定提供命名空間和安全屬性
這些元數據,其實本質是一張查詢表,裏面包括了交換器名稱和一個隊列的綁定列表,當你將消息發佈到交換器中,實際上是將你所在的信道將消息上的路由鍵與交換器的綁定列表進行匹配,然後將消息路由出去。
消息路由表
有了這個機制,那麼在所有節點上傳遞交換器消息將簡單很多,而 RabbitMQ 所做的事情就是把交換器元數據拷貝到所有節點上,因此每個節點上的每條信道都可以訪問完整的交換器。
如果消息生產者所連接的是節點 2 或者節點 3,此時隊列 1 的完整數據不在該兩個節點上,那麼在發送消息過程中這兩個節點主要起了一個路由轉發作用,根據這兩個節點上的元數據轉發至節點 1 上,最終發送的消息還是會存儲至節點 1 的隊列 1 上。
同樣,如果消息消費者所連接的節點 2 或者節點 3,那這兩個節點也會作爲路由節點起到轉發作用,將會從節點 1 的隊列 1 中拉取消息進行消費。
與常見的集羣主從架構模式不同的地方在於:RabbitMQ 集羣模式下,僅僅只是同步元數據,每個隊列內容還是在自己的服務器節點上。
這麼設計主要還是基於集羣本身的性能和存儲空間上來考慮:
-
存儲空間:真正存放數據的地方是在隊列裏面,如果每個集羣節點都擁有所有隊列的完全數據拷貝,那麼每個節點的存儲空間會非常大,集羣的消息積壓能力會非常弱。例如你現在存儲了 3G 隊列內容,那麼在另外一個只有 1G 存儲空間的節點上,就會造成內存空間不足的情況,也就是無法通過集羣節點的擴容提高消息積壓能力。
-
性能:消息的發佈者需要將消息複製到每一個集羣節點,每一條消息都會觸發磁盤活動,這會導致整個集羣內性能負載急劇拉昇。
既然每個隊列內容還是在自己的服務器節點上,同樣也會帶來新的問題,那就是如果隊列所在服務器掛了,那存在服務器上的隊列數據是不是全部都丟失了?
在單個節點上,RabbitMQ 存儲數據有兩種方案:
-
內存模式:這種模式會將數據存儲在內存當中,如果服務器突然宕機重啓之後,那麼附加在該節點上的隊列和其關聯的綁定都會丟失,並且消費者可以重新連接集羣並重新創建隊列;
-
磁盤模式:這種模式會將數據存儲磁盤當中,如果服務器突然宕機重啓,數據會自動恢復,該隊列又可以進行傳輸數據了,並且在恢復故障磁盤節點之前,不能在其它節點上讓消費者重新連到集羣並重新創建隊列,如果消費者繼續在其它節點上聲明該隊列,會得到一個 404 NOT_FOUND 錯誤,這樣確保了當故障節點恢復後加入集羣,該節點上的隊列消息不會丟失,也避免了隊列會在一個節點以上出現冗餘的問題。
在集羣中的每個節點,要麼是內存節點,要麼是磁盤節點,如果是內存節點,會將所有的元數據信息僅存儲到內存中,而磁盤節點則不僅會將所有元數據存儲到內存上, 還會將其持久化到磁盤。
在單節點 RabbitMQ 上,僅允許該節點是磁盤節點,這樣確保了節點發生故障或重啓節點之後,所有關於系統的配置與元數據信息都會從磁盤上恢復。
而在 RabbitMQ 集羣上,至少有一個磁盤節點,也就是在集羣環境中需要添加 2 臺及以上的磁盤節點,這樣其中一臺發生故障了,集羣仍然可以保持運行。其它節點均設置爲內存節點,這樣會讓隊列和交換器聲明之類的操作會更加快速,元數據同步也會更加高效。
三、集羣部署
爲了和生產環境保持一致,我們選用CentOS7
操作系統進行環境部署,分別創建 3 臺虛擬機。
# 3臺服務器的IP
197.168.24.206
197.168.24.233
197.168.24.234
放開防火牆限制,保證 3 臺服務器網絡都可以互通!
3.1、重新設置主機名
由於 RabbitMQ 集羣連接是通過主機名來連接服務的,必須保證各個主機名之間可以 ping 通,重新設置 3 臺服務器主機名,所以需要做以下操作:
# 修改節點1的主機名
hostname node1
# 修改節點2的主機名
hostname node2
# 修改節點3的主機名
hostname node3
編輯/etc/hosts
文件,添加到在三臺機器的/etc/hosts
中以下內容:
sudo vim /etc/hosts
添加內容如下:
197.168.24.206 node1
197.168.24.233 node2
197.168.24.234 node3
3.2、rabbitMQ 安裝
RabbitMQ 基於 erlang 進行通信,相比其它的軟件,安裝有些麻煩,不過本例採用rpm
方式安裝,任何新手都可以完成安裝,過程如下!
3.2.1、安裝前命令準備
輸入如下命令,完成安裝前的環境準備。
yum install lsof build-essential openssl openssl-devel unixODBC unixODBC-devel make gcc gcc-c++ kernel-devel m4 ncurses-devel tk tc xz wget vim
3.2.2、下載 RabbitMQ、erlang、socat 的安裝包
本次下載的是RabbitMQ-3.6.5
版本,採用rpm
一鍵安裝,適合新手直接上手。
先創建一個rabbitmq
目錄,本例的目錄路徑爲/usr/app/rabbitmq
,然後在目錄下執行如下命令,下載安裝包!
- 下載 erlang
wget www.rabbitmq.com/releases/erlang/erlang-18.3-1.el7.centos.x86_64.rpm
- 下載 socat
wget http://repo.iotti.biz/CentOS/7/x86_64/socat-1.7.3.2-5.el7.lux.x86_64.rpm
- 下載 rabbitMQ
wget www.rabbitmq.com/releases/rabbitmq-server/v3.6.5/rabbitmq-server-3.6.5-1.noarch.rpm
最終目錄文件如下:
3.2.3、安裝軟件包
下載完之後,按順序依次安裝軟件包,這個很重要哦~
- 安裝 erlang
rpm -ivh erlang-18.3-1.el7.centos.x86_64.rpm
- 安裝 socat
rpm -ivh socat-1.7.3.2-5.el7.lux.x86_64.rpm
- 安裝 rabbitmq
rpm -ivh rabbitmq-server-3.6.5-1.noarch.rpm
安裝完成之後,修改rabbitmq
的配置,默認配置文件在/usr/lib/rabbitmq/lib/rabbitmq_server-3.6.5/ebin
目錄下。
vim /usr/lib/rabbitmq/lib/rabbitmq_server-3.6.5/ebin/rabbit.app
修改loopback_users
節點的值!
分別重新命令rabbit
節點名稱
vim /etc/rabbitmq/rabbitmq-env.conf
在文件裏添加一行,如下配置!
NODENAME=rabbit@node1
其它兩個節點命令也類似,然後,再保存!通過如下命令,啓動服務即可!
# 啓動服務
rabbitmq-server start &
# 停止服務
rabbitmqctl stop
通過如下命令,查詢服務是否啓動成功!
lsof -i:5672
如果出現5672
已經被監聽,說明已經啓動成功!
3.2.4、啓動可視化的管控臺
輸入如下命令,啓動控制檯!
rabbitmq-plugins enable rabbitmq_management
用瀏覽器打開http://ip:15672
,這裏的ip
就是 CentOS 系統的 ip,結果如下:
賬號、密碼,默認爲guest
,如果出現無法訪問,檢測防火牆是否開啓,如果開啓將其關閉即可!
登錄之後的監控平臺,界面如下:
3.3、複製 Erlang cookie
RabbitMQ 集羣環境下,元數據同步基於 cookie 共享方案實現。
在這裏將 node1 的 cookie 文件複製到 node2,由於這個文件權限是 400 爲方便傳輸,先修改權限,非必須操作,所以需要先修改 node1 中的該文件權限爲 777
chmod 777 /var/lib/rabbitmq/.erlang.cookie
用 scp 拷貝到節點 2,節點 3 的操作也類似。
scp /var/lib/rabbitmq/.erlang.cookie node2:/var/lib/rabbitmq/
最後,將權限改回來
chmod 400 /var/lib/rabbitmq/.erlang.cookie
3.4、組成集羣
在節點 2 執行如下命令:
# 停止rabbitmq服務
rabbitmqctl stop_app
# 清空節點狀態
rabbitmqctl reset
# node2和node1構成集羣,node2必須能通過node1的主機名ping通
rabbitmqctl join_cluster rabbit@node1
# 開啓rabbitmq服務
rabbitmqctl start_app
節點 3 的操作也類似!
在任意一臺機上面查看集羣狀態:
rabbitmqctl cluster_status
-
第一行:表示當前節點信息
-
第二行:表示集羣中的節點成員,disc 表示這些都是磁盤節點
-
第三行:表示正在運行的節點成員
登錄可視化管控臺,可以很清晰的看到,三個服務節點已經互相關聯起來了!
如果你想將某個節點移除集羣,以移除節點 3 爲例,可以按照如下方式進行操作!
# 首先停止要移除的節點服務
rabbitmqctl stop
# 移除節點3
rabbitmqctl -n rabbit@node1 forget_cluster_node rabbit@node3
如果移除之後,無法啓動 rabbitMQ,刪除已有 mnesia 信息!
rm -rf /var/lib/rabbitmq/mnesia
然後再次重啓服務即可!
3.5、設置內存節點
#加入時候設置節點爲內存節點(默認加入的爲磁盤節點)
rabbitmqctl join_cluster rabbit@node1 --ram
其中--ram
指的是作爲內存節點,如果不加,那就默認爲磁盤節點。
如果節點在集羣中已經是磁盤節點了,通過以下命令可以將節點改成內存節點:
# 停止rabbitmq服務
rabbitmqctl stop_app
# 更改節點爲內存節點
rabbitmqctl change_cluster_node_type ram
# 開啓rabbitmq服務
rabbitmqctl start_app
3.6、鏡像隊列
上面我們提到,在默認情況下,隊列只會保存在其中一個節點上,當節點發生故障時,儘管所有元數據信息都可以從磁盤節點上將元數據恢復到本節點上,但是內存節點的隊列消息內容就不行了,這樣就會導致消息的丟失。
RabbitMQ 很早就意識到這個問題,在 2.6 以後的版本中增加了隊列冗餘選項:鏡像隊列。
所謂鏡像隊列,其實就是主隊列(master)依然是僅存在於一個節點上,通過關聯的 rabbitMQ 服務器,從主隊列同步消息到各個節點,也就是所謂的主從模式,將主隊列的消息進行備份處理。
如果主隊列沒有發生故障,那麼其工作流程跟普通隊列一樣,生產者和消費者不會感知其變化,當發佈消息時,依然是路由到主隊列中,而主隊列通過類似廣播的機制,將消息擴散同步至其餘從隊列中,這就有點像 fanout 交換器一樣。而消費者依然是從主隊列中讀取消息。
一旦主隊列發生故障,集羣就會從最老的一個從隊列選舉爲新的主隊列,這也就實現了隊列的高可用了,但我們切記不要濫用這個機制,在上面也說了,隊列的冗餘操作會導致不能通過擴展節點增加存儲空間,而且會造成性能瓶頸。
命令格式如下:
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"}'
類似操作很多,具體使用可以參考官方 api。
四、集羣的負載均衡
HAProxy 提供高可用性、負載均衡以及基於 TCP 和 HTTP 應用的代理,支持虛擬主機,它是免費、快速並且可靠的一種解決方案。根據官方數據,其最高極限支持 10G 的併發。HAProxy 支持從 4 層至 7 層的網絡交換,即覆蓋所有的 TCP 協議。就是說,Haproxy 甚至還支持 Mysql 的均衡負載。爲了實現 RabbitMQ 集羣的軟負載均衡,這裏可以選擇 HAProxy。
4.1、HAProxy 安裝
HAProxy 的安裝也很簡單,單獨部署在一臺服務器上,通過如下命令即可安裝完成!
yum install haproxy
編輯 HAProxy 配置文件:
vim /etc/haproxy/haproxy.cfg
我們只需要在文件末尾加上如下配置即可!
#綁定配置
listen rabbitmq_cluster
bind 0.0.0.0:5672
#配置TCP模式
mode tcp
#加權輪詢
balance roundrobin
#RabbitMQ集羣節點配置
server rmq_node1 197.168.24.206:5672 check inter 5000 rise 2 fall 3 weight 1
server rmq_node2 197.168.24.233:5672 check inter 5000 rise 2 fall 3 weight 1
server rmq_node3 197.168.24.234:5672 check inter 5000 rise 2 fall 3 weight 1
#haproxy監控頁面地址
listen monitor
bind 0.0.0.0:8100
mode http
option httplog
stats enable
stats uri /stats
stats refresh 5s
綁定配置參數說明:
-
bind
:這裏定義了客戶端連接連接 IP 地址和端口號,用於客戶端連接 -
balance roundrobin
:表示加權輪詢負載均衡算法
RabbitMQ 集羣節點配置說明:
-
server rmq_node1
:定義 HAProxy 內 RabbitMQ 服務的標識 -
197.168.24.206:5672
:標識了後端 RabbitMQ 的服務地址 -
check inter 5000
:表示每隔多少毫秒檢查 RabbitMQ 服務是否可用,示例參數值爲 5000 -
rise 2
:表示 RabbitMQ 服務在發生故障之後,需要多少次健康檢查才能被再次確認可用,示例參數值爲 2 -
fall 2
:表示需要經歷多少次失敗的健康檢查之後,HAProxy 纔會停止使用此 RabbitMQ 服務,示例參數值爲 2 -
weight 1
:表示權重比例,值越低,會優先進行數據分配,示例參數值爲 1
啓動 HAProxy:
/usr/sbin/haproxy -f /etc/haproxy/haproxy.cfg
登錄http://ip:8100/stats
web 管理界面,即可進行監控查看!
五、Java 客戶端使用
如果是配置了 HAProxy 代理服務器,可以直接使用 HAProxy 代理服務器地址即可!
//ConnectionFactory創建MQ的物理連接
connectionFactory = new ConnectionFactory();
connectionFactory.setHost("197.168.24.207"); //代理服務器地址
connectionFactory.setPort(5672); //代理服務器端口
connectionFactory.setUsername("admin"); //guest只能在本機進行訪問,通過代理服務器發送消息時需要重新建立用戶
connectionFactory.setPassword("admin"); //guest
connectionFactory.setVirtualHost("/"); //虛擬主機
如果沒有代理服務器,使用Spring
的CachingConnectionFactory
類進行配置。
以SpringBoot
項目爲例,配置文件如下:
spring.rabbitmq.addresses=197.168.24.206:5672,197.168.24.233:5672,197.168.24.234:5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.virtual-host=/
RabbitConfig
配置類如下:
@Configuration
public class RabbitConfig {
/**
* 初始化連接工廠
* @param addresses
* @param userName
* @param password
* @param vhost
* @return
*/
@Bean
ConnectionFactory connectionFactory(@Value("${spring.rabbitmq.addresses}") String addresses,
@Value("${spring.rabbitmq.username}") String userName,
@Value("${spring.rabbitmq.password}") String password,
@Value("${spring.rabbitmq.virtual-host}") String vhost) {
CachingConnectionFactory connectionFactory = new CachingConnectionFactory();
connectionFactory.setAddresses(addresses);
connectionFactory.setUsername(userName);
connectionFactory.setPassword(password);
connectionFactory.setVirtualHost(vhost);
return connectionFactory;
}
/**
* 重新實例化 RabbitAdmin 操作類
* @param connectionFactory
* @return
*/
@Bean
public RabbitAdmin rabbitAdmin(ConnectionFactory connectionFactory){
return new RabbitAdmin(connectionFactory);
}
/**
* 重新實例化 RabbitTemplate 操作類
* @param connectionFactory
* @return
*/
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory){
RabbitTemplate rabbitTemplate=new RabbitTemplate(connectionFactory);
//數據轉換爲json存入消息隊列
rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
return rabbitTemplate;
}
}
六、總結
本文主要詳細介紹了 RabbitMQ 集羣的工作原理和如何搭建一個具備負載均衡能力的 RabbitMQ 集羣的方法。
限於筆者的才疏學淺,對本文內容可能還有理解不到位的地方,如有闡述不合理之處還望留言一起探討。
七、參考
1、簡書 - 癲狂俠 - 消息中間件—RabbitMQ
2、後端進階 - zch - RabbitMQ 集羣原理與部署
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/gDIFiiznXYGsLGT7NYdBHg