Docker 搭建 RabbitMQ 集群

大纲

前言

本文将介绍如何使用 Docker 在单机上快速搭建 RabbitMQ 集群(三节点),其中集群模式使用的是 “普通模式”。

学习资源

集群概念

集群模式

RabbitMQ 集群模式分为两种,包括普通模式和镜像模式;可以说镜像模式是普通模式的升级版,其中 RabbitMQ 默认使用的是普通模式。

  • 普通模式:

    • 以两个节点(rabbit01、rabbit02)为例来进行说明,rabbit01 和 rabbit02 两个节点仅有相同的元数据,即队列的结构,但消息实体只存在于其中一个节点 rabbit01(或者 rabbit02)中。当消息进入 rabbit01 节点的 Queue 后,Consumer 从 rabbit02 节点消费时,RabbitMQ 会临时在 rabbit01、rabbit02 间进行消息传输,把 A 中的消息实体取出并经过 B 发送给 Consumer。所以,对于同一个逻辑队列,要在多个节点上建立物理队列,比如使用镜像队列或者 Quorum。换言之,如果使用的是 RabbitMQ 默认队列,Consumer 一定要尽量直连到队列所在的节点,才能避免跨节点传输带来的性能问题。否则,无论 Consumer 连接 rabbit01 还是 rabbit02,出口总在 rabbit01,会产生性能瓶颈。另外,当 rabbit01 节点故障后,rabbit02 节点无法取到 rabbit01 节点中还未消费的消息实体。如果做了消息持久化,那么得等 rabbit01 节点恢复,然后才可被消费;如果没有持久化的话,就会产生消息丢失的现象。
  • 镜像模式:

    • 在普通模式的基础上,通过镜像队列策略(HA Policy)将需要的队列配置成镜像队列,让队列存储在多个节点上,消息实体会主动在镜像节点之间同步,而不是在客户端取数据时临时拉取,也就是说有多少个镜像节点消息就会备份多少份。该模式带来的副作用也很明显,除了降低系统性能外,如果镜像队列数量过多,加之大量的消息进入,集群内部的网络带宽将会被这种同步通讯大大消耗掉,所以在对业务可靠性要求较高的场合中适用。由于镜像队列之间消息自动同步,且内部有选举 Master 机制,即使 Master 节点宕机也不会影响整个集群的使用,达到去中心化的目的,从而有效的防止消息丢失及服务不可用等问题

集群节点

RabbitMQ 的集群节点分为磁盘节点、内存节点。RabbitMQ 支持消息的持久化,也就是数据写在磁盘上。在 RabbitMQ 集群中,必须至少有一个磁盘节点,否则队列元数据无法写入到集群中。当磁盘节点宕掉时,集群将无法写入新的队列元数据信息。如果 RabbitMQ 集群全部宕机,必须先启动磁盘节点,然后再启动内存节点。最合适的方案就是既有磁盘节点,又有内存节点;建议至少 2 个磁盘节点,其他节点可以作为内存节点来提高性能,比如采用 2 个 磁盘节点 + 1 个内存节点的集群搭建方式。

节点类型节点特点
disc(磁盘节点)元数据持久化到磁盘,集群元数据(如队列、交换机、绑定、用户、权限的定义等)会复制到这些节点上。至少需要一个 disc 节点才能组成完整的 RabbitMQ 集群。
ram(内存节点)大部分集群元数据仅保存在内存中,性能更高。启动时会从磁盘节点同步元数据,但自身不会持久化元数据。一旦内存节点宕机或重启,它的元数据就会丢失(除非从磁盘节点重新同步)。掉电就丢失元数据,不适合长期存储,只适合当辅助节点使用

磁盘节点补充说明

  • RabbitMQ 默认的集群节点类型是 disc(磁盘节点),至少需要一个 disc 节点才能组成完整的 RabbitMQ 集群。

内存节点补充说明

  • ram(内存节点)决定的是 RabbitMQ 的集群元数据保存在内存中,元数据包括队列、交换机、绑定、用户、权限的定义等,而不是决定队列和消息都存储在内存里面,也就是消息的存储还是由队列持久化、消息持久化决定的。
  • 假设在 RabbitMQ 的一个内存节点上创建了队列,如果这个节点宕机,它的元数据就没了,重启后队列的定义也不存在。但是,如果将队列设置为持久化(durable=true),消息也设置为持久化(delivery_mode=2),即使内存节点宕机了,只要还有磁盘节点存在,就可以恢复内存节点的队列和消息数据。

集群规划

节点的名称宿主机的 IP 节点暴露的端口节点的主机名称节点的控制台访问地址节点的类型节点的用途
节点一 192.168.2.148567315673rabbitmq-node1http://192.168.2.148:15673磁盘节点(disc核心节点,保存所有元数据
节点二 192.168.2.148567415674rabbitmq-node2http://192.168.2.148:15674磁盘节点(disc冗余备份,提高可用性
节点三 192.168.2.148567515675rabbitmq-node3http://192.168.2.148:15675内存节点(ram提高集群性能,但掉电会导致集群元数据丢失

集群搭建

准备工作

  • 在宿主机中,创建用于存储数据的目录,用于挂载卷(数据持久化)
1
2
3
sudo mkdir -p /share/rabbitmq-node1/data
sudo mkdir -p /share/rabbitmq-node2/data
sudo mkdir -p /share/rabbitmq-node3/data
  • 在宿主机中,创建 .erlang.cookie 文件(注意:当前使用的是哪个用户权限进行操作),用于 RabbitMQ 集群节点之间建立通信
1
2
3
4
5
# 创建文件
sudo echo "secret_cookie" > ~/.erlang.cookie

# 文件授权
sudo chmod 400 ~/.erlang.cookie

搭建集群

  • 创建 Docker-Compose 的配置文件(docker-compose.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
version: '3.5'

services:
rabbitmq-node1:
image: rabbitmq:3.8.26-management
container_name: rabbitmq-node1
hostname: rabbitmq-node1
privileged: false
restart: always
environment:
- RABBITMQ_DEFAULT_USER=admin
- RABBITMQ_DEFAULT_PASS=admin
- RABBITMQ_DEFAULT_VHOST=/
ports:
- "5673:5672"
- "15673:15672"
volumes:
- '/share/rabbitmq-node1/data:/var/lib/rabbitmq'
- '~/.erlang.cookie:/var/lib/rabbitmq/.erlang.cookie'
networks:
rabbitmq_net:
ipv4_address: 172.24.0.5

rabbitmq-node2:
image: rabbitmq:3.8.26-management
container_name: rabbitmq-node2
hostname: rabbitmq-node2
privileged: false
restart: always
environment:
- RABBITMQ_DEFAULT_USER=admin
- RABBITMQ_DEFAULT_PASS=admin
- RABBITMQ_DEFAULT_VHOST=/
ports:
- "5674:5672"
- "15674:15672"
volumes:
- '/share/rabbitmq-node2/data:/var/lib/rabbitmq'
- '~/.erlang.cookie:/var/lib/rabbitmq/.erlang.cookie'
networks:
rabbitmq_net:
ipv4_address: 172.24.0.6

rabbitmq-node3:
image: rabbitmq:3.8.26-management
container_name: rabbitmq-node3
hostname: rabbitmq-node3
privileged: false
restart: always
environment:
- RABBITMQ_DEFAULT_USER=admin
- RABBITMQ_DEFAULT_PASS=admin
- RABBITMQ_DEFAULT_VHOST=/
ports:
- "5675:5672"
- "15675:15672"
volumes:
- '/share/rabbitmq-node3/data:/var/lib/rabbitmq'
- '~/.erlang.cookie:/var/lib/rabbitmq/.erlang.cookie'
networks:
rabbitmq_net:
ipv4_address: 172.24.0.7

networks:
rabbitmq_net:
driver: bridge
ipam:
config:
- subnet: 172.24.0.0/16

特别注意

  • 在新版本中,RabbitMQ 官方已经不推荐使用环境变量 RABBITMQ_ERLANG_COOKIE 来设置 Erlang Cookie,而是建议在宿主机上创建 .erlang.cookie 文件,然后挂载到容器中。
  • RabbitMQ 是基于 Erlang 语言开发的,而 Erlang 使用一个叫 Erlang Cookie 的机制来允许节点互相通信。Cookie 是 RabbitMQ 集群节点之间建立信任、组成集群的关键密码,所有节点必须保持一致

特别注意

  • RabbitMQ 的数据库名称规则是 NODENAME@hostname,由于 Docker 每次从 Docker Image 启动容器的时候会自动生成 hostname,这样一来之前保存在主机上的数据库就会没用了,包括之前创建的用户也会没有了。
  • 所以,在创建容器的时候必须指定 hostname,比如 --hostname=my-rabbit,这样 Docker 容器启动后 RabbitMQ 就会一直读取固定目录中的数据
  • 创建并启动容器,在 docker-compose.yml 配置文件所在目录下执行以下命令
1
sudo docker compose up -d
  • 配置集群,分别进入 rabbitmq-node2rabbitmq-node3 容器中,执行以下命令将它们加入到 rabbitmq-node1 集群中
1
2
3
4
5
6
7
8
# 进入节点二的容器中
sudo docker exec -it rabbitmq-node2 bash

# 将节点二加入到集群
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl join_cluster rabbit@rabbitmq-node1
rabbitmqctl start_app
1
2
3
4
5
6
7
8
# 进入节点三的容器中
sudo docker exec -it rabbitmq-node3 bash

# 将节点三加入到集群
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl join_cluster --ram rabbit@rabbitmq-node1
rabbitmqctl start_app

提示

  • rabbit@rabbitmq-node1 是 RabbitMQ 节点的名称,默认格式是 rabbit@主机名
  • RabbitMQ 默认的节点类型是 disc(磁盘节点),在将节点加入集群时,可以手动指定 --ram 参数让某些节点作为 ram(内存节点),比如 rabbitmqctl join_cluster --ram rabbit@rabbitmq-node1

验证集群

  • 验证集群状态,进入在任意节点的容器中,执行以下命令
1
2
3
4
5
# 进入节点二的容器中
sudo docker exec -it rabbitmq-node2 bash

# 查看集群运行状态(输出结果如下所示)
rabbitmqctl cluster_status
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Cluster status of node rabbit@rabbitmq-node3 ...
Basics

Cluster name: rabbit@rabbitmq-node3

Disk Nodes

rabbit@rabbitmq-node1
rabbit@rabbitmq-node2

RAM Nodes

rabbit@rabbitmq-node3

Running Nodes

rabbit@rabbitmq-node1
rabbit@rabbitmq-node2
rabbit@rabbitmq-node3

......
  • 浏览器访问任意一个节点的 Web 控制台页面(比如 http://192.168.2.148:15673),如果页面显示所有节点都是绿色的,则说明 RabbitMQ 集群搭建成功(如下图所示)。

集群维护

移除集群节点

从 RabbitMQ 集群中安全地移除某个节点,分为以下几种情况和步骤。

优雅地移除节点(推荐)

  • 在任意一个还在集群中的节点上,执行以下移除命令:
1
rabbitmqctl forget_cluster_node rabbit@主机名称

这个命令的作用:

  • 通知集群将该节点从元数据中移除。
  • 不会删除正在运行的容器或主机上的节点,只是让它 “脱离组织”。
  • 如果该节点之后恢复过来,还会以 “孤立节点” 的身份启动。
  • 如果是 ram(内存节点),这个方法最常用。

强制移除宕机节点(节点已关机或无法访问)

  • 如果某个节点已经挂掉,RabbitMQ 默认不会自动移除它。在任意一个还在集群中的节点上,执行以下移除命令:
1
rabbitmqctl forget_cluster_node --offline rabbit@主机名称

这个命令的作用:

  • 从元数据中强制剔除已经掉线的节点,相当于告诉集群:” 我确认 rabbit@主机名称 永远不会再回来,请将它删除掉”。

从被移除节点本身退出集群(可选)

如果希望当前节点不再是集群成员,而是变成一个 “独立 RabbitMQ 节点”,可在当前节点上,执行以下命令:

1
2
3
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl start_app
  • 这个命令的作用:
    • 清除当前节点之前的集群配置,它会像新安装的节点一样重新运行。

验证节点移除的结果

  • 在任意一个还在集群中的节点上,执行以下命令:
1
rabbitmqctl cluster_status

特别注意

  • 千万不要直接删除节点容器或进程,而不通知 RabbitMQ 集群。比如,直接执行 docker rm -f rabbitmq-node1 是不安全的做法,可能导致以下错误情况:
  • (1) 集群中残留 "僵尸节点";
  • (2) 镜像队列同步失败;
  • (3) 后续启动同名节点时产生冲突。

参考教程