1. 程式人生 > 其它 >HM-SpringCloud微服務系列12.4【MQ叢集】

HM-SpringCloud微服務系列12.4【MQ叢集】

1. 叢集分類

RabbitMQ的是基於Erlang語言編寫,而Erlang又是一個面向併發的語言,天然支援叢集模式。RabbitMQ的叢集有兩種模式:

  • 普通叢集:是一種分散式叢集,將佇列分散到叢集的各個節點,從而提高整個叢集的併發能力。

    • 普通模式叢集不進行資料同步,每個MQ都有自己的佇列、資料資訊(其它元資料資訊如交換機等會同步)。例如我們有2個MQ:mq1,和mq2,如果你的訊息在mq1,而你連線到了mq2,那麼mq2會去mq1拉取訊息,然後返回給你。如果mq1宕機,訊息就會丟失。
  • 映象叢集:是一種主從叢集,普通叢集的基礎上,添加了主從備份功能,提高叢集的資料可用性。

    • 與普通模式不同,佇列會在各個mq的映象節點之間同步,因此你連線到任何一個映象節點,均可獲取到訊息。而且如果一個節點宕機,並不會導致資料丟失。不過,這種方式增加了資料同步的頻寬消耗。

映象叢集雖然支援主從,但主從同步並不是強一致的,某些情況下可能有資料丟失的風險。因此在RabbitMQ的3.8版本以後,推出了新的功能:仲裁佇列來代替映象叢集,底層採用Raft協議確保主從的資料一致性。

2. 普通叢集

2.1 叢集結構和特徵

普通叢集,或者叫標準叢集(classic cluster)。

具備下列特徵:

  • 會在叢集的各個節點間共享部分資料,包括:交換機、佇列元資訊。不包含佇列中的訊息。
  • 當訪問叢集某節點時,如果佇列不在該節點,會從資料所在節點傳遞到當前節點並返回
  • 佇列所在節點宕機,佇列中的訊息就會丟失

結構如圖:

2.2 部署

計劃部署3節點的mq叢集:

主機名 控制檯埠 amqp通訊埠
mq1 8081 ---> 15672 8071 ---> 5672
mq2 8082 ---> 15672 8072 ---> 5672
mq3 8083 ---> 15672 8073 ---> 5672

叢集中的節點標示預設都是:rabbit@[hostname],因此以上三個節點的名稱分別為:

  • rabbit@mq1
  • rabbit@mq2
  • rabbit@mq3

2.2.1 獲取cookie

RabbitMQ底層依賴於Erlang,而Erlang虛擬機器就是一個面向分散式的語言,預設就支援叢集模式。

叢集模式中的每個RabbitMQ 節點使用 cookie 來確定它們是否被允許相互通訊。

要使兩個節點能夠通訊,它們必須具有相同的共享祕密,稱為Erlang cookie

cookie 只是一串最多 255 個字元的字母數字字元。

每個叢集節點必須具有相同的 cookie。例項之間也需要它來相互通訊。

我們先在之前https://www.cnblogs.com/yppah/p/16244811.html啟動的mq2容器中獲取一個cookie值,作為叢集的cookie。

執行下面的命令:

docker exec -it mq2 cat /var/lib/rabbitmq/.erlang.cookie

可以看到cookie值如下:

GTLNTUUMPWVBGLSFOJVT

我們需要重新搭建叢集,接下來,停止並刪除當前的mq2容器,防止干擾:

docker rm -f mq2

然後,通過以下命令清理一下mq2的資料卷:

docker volume prune

此外,將https://www.cnblogs.com/yppah/p/15832492.html的mq也刪除掉

2.2.2 準備叢集配置

在/tmp目錄新建一個配置檔案 rabbitmq.conf:

cd /tmp
# 建立檔案
touch rabbitmq.conf

檔案內容如下:

loopback_users.guest = false
listeners.tcp.default = 5672
cluster_formation.peer_discovery_backend = rabbit_peer_discovery_classic_config
cluster_formation.classic_config.nodes.1 = rabbit@mq1
cluster_formation.classic_config.nodes.2 = rabbit@mq2
cluster_formation.classic_config.nodes.3 = rabbit@mq3

在/tmp目錄再建立一個檔案,記錄cookie:

cd /tmp
# 建立cookie檔案
touch .erlang.cookie
# 寫入cookie(上一步中獲取到的)
echo "GTLNTUUMPWVBGLSFOJVT" > .erlang.cookie
# 修改cookie檔案的許可權(600:root使用者具有讀寫許可權,其他使用者無任何許可權)
chmod 600 .erlang.cookie

在/tmp下準備三個目錄mq1、mq2、mq3:

cd /tmp
# 建立目錄
mkdir mq1 mq2 mq3

然後拷貝rabbitmq.conf、cookie檔案到mq1、mq2、mq3:

# 進入/tmp
cd /tmp
# 拷貝
cp rabbitmq.conf mq1
cp rabbitmq.conf mq2
cp rabbitmq.conf mq3
cp .erlang.cookie mq1
cp .erlang.cookie mq2
cp .erlang.cookie mq3

2.2.3 啟動叢集

為了使三個docker容器可以互聯,首先建立一個網路

docker network create mq-net

docker volume create,依次執行命令:

docker run -d --net mq-net \
-v ${PWD}/mq1/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf \
-v ${PWD}/.erlang.cookie:/var/lib/rabbitmq/.erlang.cookie \
-e RABBITMQ_DEFAULT_USER=haifei \
-e RABBITMQ_DEFAULT_PASS=123321 \
--name mq1 \
--hostname mq1 \
-p 8071:5672 \
-p 8081:15672 \
rabbitmq:3.8-management
docker run -d --net mq-net \
-v ${PWD}/mq1/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf \
-v ${PWD}/.erlang.cookie:/var/lib/rabbitmq/.erlang.cookie \
-e RABBITMQ_DEFAULT_USER=haifei \
-e RABBITMQ_DEFAULT_PASS=123321 \
--name mq2 \
--hostname mq2 \
-p 8072:5672 \
-p 8082:15672 \
rabbitmq:3.8-management
docker run -d --net mq-net \
-v ${PWD}/mq1/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf \
-v ${PWD}/.erlang.cookie:/var/lib/rabbitmq/.erlang.cookie \
-e RABBITMQ_DEFAULT_USER=haifei \
-e RABBITMQ_DEFAULT_PASS=123321 \
--name mq3 \
--hostname mq3 \
-p 8073:5672 \
-p 8083:15672 \
rabbitmq:3.8-management

埠專用報錯解決方案:https://blog.csdn.net/qq_47768542/article/details/114147974

PS:需要參照2.2.1 獲取cookie中方法刪掉剛剛圖1建立的mq1,再重新建立

再開啟三個視窗,分別通過命令docker logs -f mq1/2/3看一下三個mq的啟動情況

本地瀏覽器分別訪問<10.193.193.141:8081/8082/8083>通過mq管理平臺檢視叢集

2.3 測試

2.3.1 資料共享測試

在mq1這個節點上新增一個佇列:

如圖,在mq2和mq3兩個控制檯也都能看到:

在mq1中點選這個佇列,進入管理頁面:

然後利用控制檯傳送一條訊息到這個佇列:

結果在mq2、mq3上都能看到這條訊息:

2.3.2 可用性測試

我們讓其中一臺節點mq1宕機:

docker stop mq1

然後登入mq2或mq3的控制檯,發現simple.queue也不可用了:

說明資料並沒有拷貝到mq2和mq3。


重新啟動mq1節點

3. 映象叢集

在剛剛的案例中,一旦建立佇列的主機宕機,佇列就會不可用。不具備高可用能力。

如果要解決這個問題,必須使用官方提供的映象叢集方案。

官方文件地址:https://www.rabbitmq.com/ha.html

PS:映象叢集實際上就是在傳統普通叢集基礎上做了一些配置實現的

3.1 叢集結構和特徵

映象叢集:本質是主從模式。

具備下面的特徵:

  • 交換機、佇列、佇列中的訊息會在各個mq的映象節點之間同步備份。
  • 建立佇列的節點被稱為該佇列的主節點,備份到的其它節點叫做該佇列的映象節點。
  • 一個佇列的主節點可能是另一個佇列的映象節點
  • 所有操作都是主節點完成,然後同步給映象節點
  • 主宕機後,映象節點會替代成新的主

結構如圖:

3.2 映象模式的特徵

預設情況下,佇列只儲存在建立該佇列的節點上。而映象模式下,建立佇列的節點被稱為該佇列的主節點,佇列還會拷貝到叢集中的其它節點,也叫做該佇列的映象節點。

但是,不同佇列可以在叢集中的任意節點上建立,因此不同佇列的主節點可以不同。甚至,一個佇列的主節點可能是另一個佇列的映象節點

使用者傳送給佇列的一切請求,例如傳送訊息、訊息回執預設都會在主節點完成,如果是從節點接收到請求,也會路由到主節點去完成。映象節點僅僅起到備份資料作用

當主節點接收到消費者的ACK時,所有映象都會刪除節點中的資料。

總結如下:

  • 映象佇列結構是一主多從(從就是映象)
  • 所有操作都是主節點完成,然後同步給映象節點
  • 主宕機後,映象節點會替代成新的主(如果在主從同步完成前,主就已經宕機,可能出現數據丟失)
  • 不具備負載均衡功能,因為所有操作都會有主節點完成(但是不同佇列,其主節點可以不同,可以利用這個提高吞吐量)

3.3 映象模式的配置

映象模式的配置有3種模式:

ha-mode ha-params 效果
準確模式exactly 佇列的副本量count 叢集中佇列副本(主伺服器和映象伺服器之和)的數量。count如果為1意味著單個副本:即佇列主節點。count值為2表示2個副本:1個佇列主和1個佇列映象。換句話說:count = 映象數量 + 1。如果群集中的節點數少於count,則該佇列將映象到所有節點。如果有叢集總數大於count+1,並且包含映象的節點出現故障,則將在另一個節點上建立一個新的映象。
all (none) 佇列在群集中的所有節點之間進行映象。佇列將映象到任何新加入的節點。映象到所有節點將對所有群集節點施加額外的壓力,包括網路I / O,磁碟I / O和磁碟空間使用情況。推薦使用exactly,設定副本數為(N / 2 +1)。
nodes node names 指定佇列建立到哪些節點,如果指定的節點全部不存在,則會出現異常。如果指定的節點在叢集中存在,但是暫時不可用,會建立節點到當前客戶端連線到的節點。

這裡我們以rabbitmqctl命令作為案例來講解配置語法。

語法示例如下

3.3.1 exactly模式

rabbitmqctl set_policy ha-two "^two\." '{"ha-mode":"exactly","ha-params":2,"ha-sync-mode":"automatic"}'
  • rabbitmqctl set_policy:固定寫法
  • ha-two:策略名稱,自定義
  • "^two\.":匹配佇列的正則表示式,符合命名規則的佇列才生效,這裡是任何以two.開頭的佇列名稱
  • '{"ha-mode":"exactly","ha-params":2,"ha-sync-mode":"automatic"}': 策略內容
    • "ha-mode":"exactly":策略模式,此處是exactly模式,指定副本數量
    • "ha-params":2:策略引數,這裡是2,就是副本數量為2,1主1映象
    • "ha-sync-mode":"automatic":同步策略,預設是manual,即新加入的映象節點不會同步舊的訊息。如果設定為automatic,則新加入的映象節點會把主節點中所有訊息都同步,會帶來額外的網路開銷

3.3.2 all模式

rabbitmqctl set_policy ha-all "^all\." '{"ha-mode":"all"}'
  • ha-all:策略名稱,自定義
  • "^all\.":匹配所有以all.開頭的佇列名
  • '{"ha-mode":"all"}':策略內容
    • "ha-mode":"all":策略模式,此處是all模式,即所有節點都會稱為映象節點

3.3.3 nodes模式

rabbitmqctl set_policy ha-nodes "^nodes\." '{"ha-mode":"nodes","ha-params":["rabbit@nodeA", "rabbit@nodeB"]}'
  • rabbitmqctl set_policy:固定寫法
  • ha-nodes:策略名稱,自定義
  • "^nodes\.":匹配佇列的正則表示式,符合命名規則的佇列才生效,這裡是任何以nodes.開頭的佇列名稱
  • '{"ha-mode":"nodes","ha-params":["rabbit@nodeA", "rabbit@nodeB"]}': 策略內容
    • "ha-mode":"nodes":策略模式,此處是nodes模式
    • "ha-params":["rabbit@mq1", "rabbit@mq2"]:策略引數,這裡指定副本所在節點名稱

3.4 測試

我們使用exactly模式的映象,因為叢集節點數量為3,因此映象數量就設定為2.

執行下面的命令進入mq1容器內部:

docker exec -it mq1 bash

然後配置映象模式:

rabbitmqctl set_policy ha-two "^two\." '{"ha-mode":"exactly","ha-params":2,"ha-sync-mode":"automatic"}'

下面,在mq1上建立一個新的佇列:

在任意一個mq控制檯檢視佇列:

3.4.1 資料共享測試

在mq1中給two.queue傳送一條訊息:

然後在mq1、mq2、mq3的任意控制檯檢視訊息:

3.4.2 高可用測試

現在,我們讓two.queue的主節點mq1宕機:

docker stop mq1

檢視叢集狀態:

檢視佇列狀態:

發現依然是健康的!並且其主節點切換到了rabbit@mq2上


現在,再來重啟mq1

可以發現,two.queue的主機仍是mq2,已與mq1無關

4. 仲裁佇列

4.1 叢集結構和特徵

仲裁佇列:仲裁佇列是3.8版本以後才有的新功能,用來替代映象佇列。

具備下列特徵:

  • 與映象佇列一樣,都是主從模式,支援主從資料同步
  • 使用非常簡單,沒有複雜的配置
  • 主從同步基於Raft協議,強一致

4.2 控制檯手動新增仲裁佇列

在任意控制檯新增一個佇列,一定要選擇佇列型別為Quorum型別。

在任意控制檯檢視佇列:

可以看到,仲裁佇列的 + 2字樣。代表這個佇列有2個映象節點。

因為仲裁佇列預設的映象數為5。如果你的叢集有7個節點,那麼映象數肯定是5;而我們叢集只有3個節點,因此映象數量就是3。


4.3 Java處理仲裁佇列

4.3.1 SpringAMQP連線MQ叢集

注意,yaml這裡用address來代替host、port方式

spring:
  rabbitmq:
    addresses: 10.193.193.141:8071, 10.193.193.141:8072, 10.193.193.141:8073
    username: haifei
    password: 123321
    virtual-host: /

4.3.2 SpringAMQP建立仲裁佇列

@Bean
public Queue quorumQueue() {
    return QueueBuilder
        .durable("quorum.queue") // 持久化
        .quorum() // 仲裁佇列
        .build();
}

4.3.3 測試

考慮到目前為mq叢集模式,consumer服務config包下其餘配置類都是針對此前單機課程的配置,

倘若當前直接啟動consumer服務,難免會報一堆錯,

因此,現將除QuorumConfig類之外的其餘配置類中的@Configuration註解都先註釋掉

並且,令SpringRabbitListener類中只保留istenSimpleQueue(),其餘方法先註釋掉

package cn.itcast.mq.listener;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class SpringRabbitListener {

    @RabbitListener(queues = "simple.queue")
    public void listenSimpleQueue(String msg) {
        System.out.println("消費者接收到simple.queue的訊息:【" + msg + "】");
    }

    /*@RabbitListener(queues = "simple.queue")
    public void listenSimpleQueue(String msg) {
        log.debug("消費者接收到simple.queue的訊息:【" + msg + "】");
        System.out.println(1/0); //模擬異常:手動錯誤
        log.info("消費者處理訊息成功!");
    }*/

    /*@RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "dl.queue", durable = "true"),
            exchange = @Exchange(name = "dl.direct"),
            key = "dl"
    ))
    public void listenDlQueue(String msg) {
        log.debug("消費者接收到dl.queue的延遲訊息:{}", msg);
    }*/

    /*@RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "delay.queue", durable = "true"),
            exchange = @Exchange(name = "delay.direct", delayed = "true"),
            key = "delay"
    ))
    public void listenDelayQueue(String msg) {
        log.debug("消費者接收到delay.queue的延遲訊息:{}", msg);
    }*/

}

啟動consumer服務

PS:建立佇列是如上操作;傳送訊息跟此前學的一樣,再次不再演示

4.4 叢集擴容(擴充套件|未講)

4.4.1 加入叢集

1)啟動一個新的MQ容器:

docker run -d --net mq-net \
-v ${PWD}/.erlang.cookie:/var/lib/rabbitmq/.erlang.cookie \
-e RABBITMQ_DEFAULT_USER=itcast \
-e RABBITMQ_DEFAULT_PASS=123321 \
--name mq4 \
--hostname mq5 \
-p 8074:15672 \
-p 8084:15672 \
rabbitmq:3.8-management

2)進入容器控制檯:

docker exec -it mq4 bash

3)停止mq程序

rabbitmqctl stop_app

4)重置RabbitMQ中的資料:

rabbitmqctl reset

5)加入mq1:

rabbitmqctl join_cluster rabbit@mq1

6)再次啟動mq程序

rabbitmqctl start_app

4.4.2 增加仲裁佇列副本

我們先檢視下quorum.queue這個佇列目前的副本情況,進入mq1容器:

docker exec -it mq1 bash

執行命令:

rabbitmq-queues quorum_status "quorum.queue"

結果:

現在,我們讓mq4也加入進來:

rabbitmq-queues add_member "quorum.queue" "rabbit@mq4"

結果:

再次檢視:

rabbitmq-queues quorum_status "quorum.queue"

檢視控制檯,發現quorum.queue的映象數量也從原來的 +2 變成了 +3: