手把手教你搭建 RabbitMQ 集羣

@[toc] 單個的 RabbitMQ 肯定無法實現高可用,要想高可用,還得上集羣。

今天松哥就來和大家聊一聊 RabbitMQ 集羣的搭建。

1. 兩種模式

說到集羣,小夥伴們可能第一個問題是,如果我有一個 RabbitMQ 集羣,那麼是不是我的消息集羣中的每一個實例都保存一份呢?

這其實就涉及到 RabbitMQ 集羣的兩種模式:

  • 普通集羣
  • 鏡像集羣

1.1 普通集羣

普通集羣模式,就是將 RabbitMQ 部署到多臺服務器上,每個服務器啓動一個 RabbitMQ 實例,多個實例之間進行消息通信。

此時我們創建的隊列 Queue,它的元數據(主要就是 Queue 的一些配置信息)會在所有的 RabbitMQ 實例中進行同步,但是隊列中的消息只會存在於一個 RabbitMQ 實例上,而不會同步到其他隊列。

當我們消費消息的時候,如果連接到了另外一個實例,那麼那個實例會通過元數據定位到 Queue 所在的位置,然後訪問 Queue 所在的實例,拉取數據過來發送給消費者。

這種集羣可以提高 RabbitMQ 的消息吞吐能力,但是無法保證高可用,因爲一旦一個 RabbitMQ 實例掛了,消息就沒法訪問了,如果消息隊列做了持久化,那麼等 RabbitMQ 實例恢復後,就可以繼續訪問了;如果消息隊列沒做持久化,那麼消息就丟了。

大致的流程圖如下圖:

1.2 鏡像集羣

它和普通集羣最大的區別在於 Queue 數據和原數據不再是單獨存儲在一臺機器上,而是同時存儲在多臺機器上。也就是說每個 RabbitMQ 實例都有一份鏡像數據(副本數據)。每次寫入消息的時候都會自動把數據同步到多臺實例上去,這樣一旦其中一臺機器發生故障,其他機器還有一份副本數據可以繼續提供服務,也就實現了高可用。

大致流程圖如下圖:

1.3 節點類型

RabbitMQ 中的節點類型有兩種:

  • RAM node:內存節點將所有的隊列、交換機、綁定、用戶、權限和 vhost 的元數據定義存儲在內存中,好處是可以使得交換機和隊列聲明等操作速度更快。
  • Disk node:將元數據存儲在磁盤中,單節點系統只允許磁盤類型的節點,防止重啓 RabbitMQ 的時候,丟失系統的配置信息

RabbitMQ 要求在集羣中至少有一個磁盤節點,所有其他節點可以是內存節點,當節點加入或者離開集羣時,必須要將該變更通知到至少一個磁盤節點。如果集羣中唯一的一個磁盤節點崩潰的話,集羣仍然可以保持運行,但是無法進行其他操作(增刪改查),直到節點恢復。爲了確保集羣信息的可靠性,或者在不確定使用磁盤節點還是內存節點的時候,建議直接用磁盤節點。

2. 搭建普通集羣

2.1 預備知識

大致的結構瞭解了,接下來我們就把集羣給搭建起來。先從普通集羣開始,我們就使用 docker 來搭建。

搭建之前,有兩個預備知識需要大家瞭解:

  1. 搭建集羣時,節點中的 Erlang Cookie 值要一致,默認情況下,文件在 /var/lib/rabbitmq/.erlang.cookie,我們在用 docker 創建 RabbitMQ 容器時,可以爲之設置相應的 Cookie 值。
  2. RabbitMQ 是通過主機名來連接服務,必須保證各個主機名之間可以 ping 通。可以通過編輯 /etc/hosts 來手工添加主機名和 IP 對應關係。如果主機名 ping 不通,RabbitMQ 服務啓動會失敗(如果我們是在不同的服務器上搭建 RabbitMQ 集羣,大家需要注意這一點,接下來的 2.2 小結,我們將通過 Docker 的容器連接 link 來實現容器之間的訪問,略有不同)。

2.2 開始搭建

執行如下命令創建三個 RabbitMQ 容器:

docker run -d --hostname rabbit01 --name mq01 -p 5671:5672 -p 15671:15672 -e RABBITMQ_ERLANG_COOKIE="javaboy_rabbitmq_cookie" rabbitmq:3-management
docker run -d --hostname rabbit02 --name mq02 --link mq01:mylink01 -p 5672:5672 -p 15672:15672 -e RABBITMQ_ERLANG_COOKIE="javaboy_rabbitmq_cookie" rabbitmq:3-management
docker run -d --hostname rabbit03 --name mq03 --link mq01:mylink02 --link mq02:mylink03 -p 5673:5672 -p 15673:15672 -e RABBITMQ_ERLANG_COOKIE="javaboy_rabbitmq_cookie" rabbitmq:3-management

運行結果如下:

三個節點現在就啓動好了,注意在 mq02 和 mq03 中,分別使用了 --link 參數來實現容器連接,關於這個參數,如果大家不懂,可以在公衆號江南一點雨後臺回覆 docker,由松哥寫的 docker 入門教程,裏邊有講這個。這裏我就不囉嗦了。另外還需要注意,mq03 容器中要既能夠連接 mq01 也能夠連接 mq02。

接下來進入到 mq02 容器中,首先查看一下 hosts 文件,可以看到我們配置的容器連接已經生效了:

將來在 mq02 容器中,就可以通過 mylink01 或者 rabbit01 訪問到 mq01 容器了。

接下來我們開始集羣的配置。

分別執行如下命令將 mq02 容器加入集羣中:

rabbitmqctl stop_app
rabbitmqctl join_cluster rabbit@rabbit01
rabbitmqctl start_app

接下來輸入如下命令我們可以查看集羣的狀態:

rabbitmqctl cluster_status

可以看到,集羣中已經有兩個節點了。

接下來通過相同的方式將 mq03 也加入到集羣中:

rabbitmqctl stop_app
rabbitmqctl join_cluster rabbit@rabbit01
rabbitmqctl start_app

接下來,我們可以查看集羣信息:

可以看到,此時集羣中已經有三個節點了。

其實,這個時候,我們也可以通過網頁來查看集羣信息,在三個 RabbitMQ 實例的 Web 端首頁,都可以看到如下內容:

2.3 代碼測試

接下來我們來簡單測試一下這個集羣。

我們創建一個名爲 mq_cluster_demo 的父工程,然後在其中創建兩個子工程。

第一個子工程名爲 provider,是一個消息生產者,創建時引入 Web 和 RabbitMQ 依賴,如下:

然後配置 applicaiton.properties,內容如下(注意集羣配置):

spring.rabbitmq.addresses=localhost:5671,localhost:5672,localhost:5673
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest

接下來提供一個簡單的隊列,如下:

@Configuration
public class RabbitConfig {
    public static final String MY_QUEUE_NAME = "my_queue_name";
    public static final String MY_EXCHANGE_NAME = "my_exchange_name";
    public static final String MY_ROUTING_KEY = "my_queue_name";

    @Bean
    Queue queue() {
        return new Queue(MY_QUEUE_NAME, true, false, false);
    }

    @Bean
    DirectExchange directExchange() {
        return new DirectExchange(MY_EXCHANGE_NAME, true, false);
    }

    @Bean
    Binding binding() {
        return BindingBuilder.bind(queue())
                .to(directExchange())
                .with(MY_ROUTING_KEY);
    }
}

這個沒啥好說的,都是基本內容,接下來我們在單元測試中進行消息發送測試:

@SpringBootTest
class ProviderApplicationTests {

    @Autowired
    RabbitTemplate rabbitTemplate;

    @Test
    void contextLoads() {
        rabbitTemplate.convertAndSend(null, RabbitConfig.MY_QUEUE_NAME, "hello 江南一點雨");
    }

}

這條消息發送成功之後,在 RabbitMQ 的 Web 管理端,我們會看到三個 RabbitMQ 實例上都會顯示有一條消息,但是實際上消息本身只存在於一個 RabbitMQ 實例。

接下來我們再創建一個消息消費者,消息消費者的依賴以及配置和消息生產者都是一模一樣,我就不重複了,消息消費者中增加一個消息接收器:

@Component
public class MsgReceiver {

    @RabbitListener(queues = RabbitConfig.MY_QUEUE_NAME)
    public void handleMsg(String msg) {
        System.out.println("msg = " + msg);
    }
}

當消息消費者啓動成功後,這個方法中只收到一條消息,進一步驗證了我們搭建的 RabbitMQ 集羣是沒問題的。

2.4 反向測試

接下來松哥再舉兩個反例,以證明消息並沒有同步到其他 RabbitMQ 實例。

確保三個 RabbitMQ 實例都是啓動狀態,關閉掉 Consumer,然後通過 provider 發送一條消息,發送成功之後,關閉 mq01 實例,然後啓動 Consumer 實例,此時 Consumer 實例並不會消費消息,反而會報錯說 mq01 實例連接不上,這個例子就可以說明消息在 mq01 上,並沒有同步到另外兩個 MQ 上。相反,如果 provider 發送消息成功之後,我們沒有關閉 mq01 實例而是關閉了 mq02 實例,那麼你就會發現消息的消費不受影響。

3. 搭建鏡像集羣

所謂的鏡像集羣模式並不需要額外搭建,只需要我們將隊列配置爲鏡像隊列即可。

這個配置可以通過網頁配置,也可以通過命令行配置,我們分別來看。

3.1 網頁配置鏡像隊列

先來看看網頁上如何配置鏡像隊列。

點擊 Admin 選項卡,然後點擊右邊的 Policies,再點擊 Add/update a policy,如下圖:

接下來添加一個策略,如下圖:

各參數含義如下:

  • 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 的優先級。

配置完成後,點擊下面的 add/update policy 按鈕,完成策略的添加,如下:

添加完成後,我們可以進行一個簡單的測試。

首先確認三個 RabbitMQ 都啓動了,然後用上面的 provider 向消息隊列發送一條消息。

發完之後關閉 mq01 實例。

接下來啓動 consumer,此時發現 consumer 可以完成消息的消費(注意和前面的反向測試區分),這就說明鏡像隊列已經搭建成功了。

3.2 命令行配置鏡像隊列

命令行的配置格式如下:

rabbitmqctl set_policy [-p vhost] [--priority priority] [--apply-to apply-to] {name} {pattern} {definition}

舉一個簡單的配置案例:

rabbitmqctl set_policy -p / --apply-to queues my_queue_mirror "^" '{"ha-mode":"all","ha-sync-mode":"automatic"}'

4. 小結

好啦,這就是松哥和大家分享的 RabbitMQ 中的集羣搭建,感興趣的小夥伴趕緊去試試吧~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章