RabbitMQ 普通集群模式

RabbitMQ 集群是一个或 多个节点,每个节点共享用户、虚拟主机、 队列、交换、绑定、运行时参数和其他分布式状态。

RabbitMQ 集群是在多台机器上启动多个 rabbitmq 实例,每个机器启动一个。

但是你创建的 queue,只会放在一个 rabbtimq 实例上,但是每个实例都同步 queue 的元数据(存放含queue数据的真正实例位置)。消费的时候,实际上如果连接到了另外一个实例,那么那个实例会从 queue 所在实例上拉取数据过来。

RabbitMQ 集群可以通过多种方式形成:

  • 通过在配置文件中列出群集节点以声明方式
  • 以声明方式使用基于 DNS 的发现
  • 以声明方式使用 AWS (EC2) 实例发现(通过插件)
  • 声明式使用 Kubernetes 发现(通过插件)
  • 以声明方式使用基于 Consul 的发现(通过插件)
  • 声明式地使用基于 etcd 的发现(通过插件)
  • 手动使用 rabbitmqctl

集群部署的核心概念

1、 仲裁队列

仲裁队列是 RabbitMQ 实现持久、 基于Raft共识算法的复制FIFO队列。 它从 RabbitMQ 3.8.0 开始可用。

仲裁队列和流取代了持久镜像队列,即原始队列 复制的队列类型,现已弃用并计划删除。

仲裁队列针对数据安全性为 重中之重。这在动机中有所介绍。 仲裁队列应被视为复制队列类型的默认选项。

与传统镜像队列相比,仲裁队列在行为上也存在重要差异和一些限制, 包括特定于工作负载的,例如,当使用者重复对同一消息重新排队时。另外仲裁队列支持死信交换 (DLX)。

仲裁队列和镜像队列差异

2、节点名称

RabbitMQ 节点由节点名称标识。节点名称由两部分组成,前缀(通常是 rabbit)和主机名。例如,rabbit@node1.messaging.svc.local 是一个节点名称,前缀为 rabbit,主机名为 node1.messaging.svc.local

群集中的节点名称必须是唯一的。 如果给定主机上正在运行多个节点(这通常在开发和 QA 环境中),他们必须使用不同的前缀,例如:rabbit1@hostnamerabbit2@hostname

在群集中,节点使用节点名称相互标识和联系。这意味着必须解析每个节点名称的主机名部分。CLI 工具还使用节点名称识别和寻址节点。

当节点启动时,它会检查是否已为其分配节点名称。这是完成的通过 RABBITMQ_NODENAME 环境变量。如果未显式配置任何值,节点解析其主机名,并将 rabbit 附加到它以计算其节点名称。

如果系统使用完全限定域名 (FQDN) 作为主机名,RabbitMQ 节点 并且必须将 CLI 工具配置为使用所谓的长节点名称。 对于服务器节点,这是通过将 RABBITMQ_USE_LONGNAME 环境变量设置为 true 来完成的。

对于 CLI 工具,必须设置 RABBITMQ_USE_LONGNAME--longnames 选项 必须指定。

3、集群形成要求

主机名解析

RabbitMQ 节点使用节点名称(组合)相互寻址 前缀和域名,可以是短的还是完全限定的 (FQDN)。因此,每个集群成员都必须能够解析主机名。对于所有其他集群成员,其自己的主机名也是如此。作为可能使用 RabbitMQCTL 等命令行工具的计算机。节点将在节点引导时尽早执行主机名解析。在基于容器的环境中,主机名很重要 在容器启动之前,解决方案已准备就绪。 对于 Kubernetes 用户来说,这意味着 CoreDNS 的 DNS 缓存间隔值在 5-10 秒范围内。

主机名解析可以使用任何标准操作系统提供 方法:

  • 域名系统记录
  • 本地主机文件 (例如 /etc/hosts)

在限制性更强的环境中,DNS 记录或 主机文件修改受到限制、无法或 不想要的,erlang可以将 VM 配置为使用备用主机名 解析方法,例如备用 DNS 服务器, 本地文件、非标准主机文件位置或混合 的方法。这些方法可以与 标准操作系统主机名解析方法。要使用 FQDN,请参阅配置指南中的 RABBITMQ_USE_LONGNAME

端口访问

RabbitMQ 节点绑定到端口(开放服务器 TCP 套接字)以接受客户端和 CLI 工具连接。 其他进程和工具(如 SELinux)可能会阻止 RabbitMQ 绑定到端口。当这种情况发生时, 节点将无法启动。CLI 工具、客户端库和 RabbitMQ 节点也打开连接(客户端 TCP 套接字)。 防火墙可以阻止节点和 CLI 工具相互通信。 以下端口与群集中的节点间通信最相关:

  • 4369:epmd,RabbitMQ 节点和 CLI 工具使用的帮助程序发现守护进程

  • 6000 到 6500:由 RabbitMQ 流复制使用

  • 25672:用于节点间和 CLI 工具通信(Erlang 分发服务器端口) 并且从动态范围分配(默认情况下仅限于单个端口, 计算为 AMQP 端口 + 20000)。除非这些端口上的外部连接确实是必要的(例如 群集使用联合身份验证或在子网外部的计算机上使用 CLI 工具), 这些端口不应公开。有关详细信息,请参阅网络指南。

  • 35672-35682:CLI 工具(Erlang 分发客户端端口)用于与节点通信 并从动态范围(计算为服务器分发端口 + 10000 到 服务器分发端口 + 10010)。

可以将 RabbitMQ 配置为使用不同的端口和特定的网络接口

对等节点

一些分布式系统 具有领导节点和从节点。对于 RabbitMQ 来说,通常并非如此。 RabbitMQ 集群中的所有节点都是平等的对等节点:RabbitMQ 核心中没有特殊的节点。 当仲裁队列和插件时,本主题变得更加微妙 被考虑在内,但出于大多数意图和目的, 应将所有群集节点视为相等

RabbitMQ 集群部署

配置主机名

准备3台服务器,配置本地 /etc/hosts

1
2
3
4
5
$ cat >> /etc/hosts << EOF
192.168.50.41 rabbit1
192.168.50.42 rabbit2
192.168.50.43 rabbit3
EOF

分别配置主机名:

1
2
3
4
5
6
7
8
# on 192.168.50.41
hostnamectl set-hostname rabbit1

# on 192.168.50.42
hostnamectl set-hostname rabbit2

# on 192.168.50.43
hostnamectl set-hostname rabbit3

RabbitMQ 集群 节点需要有相同的 Erlang Cookie,否则不能进行通信。如果该文件不存在,Erlang VM 将尝试创建 一个在 RabbitMQ 服务器时随机生成的值启动。

官方在介绍集群的文档中提到过 .erlang.cookie一般会存在这两个地址:

  • 第一个是 $home/.erlang.cookie 也就是 /root 目录下
  • 第二个地方就是 /var/lib/rabbitmq/.erlang.cookie,如果我们使用rpm等安装包方式进行安装的,这个文件会在该目录下。

注意这个文件权限是 400!为保证cookie的完全一致,采用从一个节点copy的方式:

1
2
3
# on 192.168.50.41 拷贝给另外两个节点
scp -P57321 /var/lib/rabbitmq/.erlang.cookie root@192.168.50.42:/var/lib/rabbitmq/
scp -P57321 /var/lib/rabbitmq/.erlang.cookie root@192.168.50.43:/var/lib/rabbitmq/

启动独立节点

以正常方式在所有节点上启动 RabbitMQ ,这将分别在三台服务器创建三个独立的 RabbitMQ 实例:

1
2
3
systemctl daemon-reload
systemctl enable rabbitmq-server
systemctl start rabbitmq-server

使用 rabbitmqctl cluster_status 命令所确认主机名:

1
2
3
4
5
6
7
8
9
10
11
# on rabbit1
rabbitmqctl cluster_status
Cluster status of node rabbit@rabbit1 ...

# on rabbit2
rabbitmqctl cluster_status
Cluster status of node rabbit@rabbit2 ...

# on rabbit3
rabbitmqctl cluster_status
Cluster status of node rabbit@rabbit3 ...

创建集群

将三个独立的 rabbit 节点组成集群,需要分别告诉另外两个节点,通知其 “加入”。在操作 “加入” 动作之前,这两个新节点必须执行 “重置”。

操作 rabbit2 加入 rabbit1 的集群:

1
2
3
4
5
6
7
8
9
10
11
12
# on rabbit2
$ rabbitmqctl stop_app
Stopping rabbit application on node rabbit@rabbit2 ...

$ rabbitmqctl reset
Resetting node rabbit@rabbit2 ...

$ rabbitmqctl join_cluster rabbit@rabbit1
Clustering node rabbit@rabbit2 with rabbit@rabbit1

$ rabbitmqctl start_app
Starting node rabbit@rabbit2 ...

现在可以在 rabbit1 或 rabbit2 任一节点上查看集群状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# on rabbit1
$ rabbitmqctl cluster_status
Cluster status of node rabbit@rabbit1 ...
....
Maintenance status

Node: rabbit@rabbit1, status: not under maintenance
Node: rabbit@rabbit2, status: not under maintenance
....


# on rabbit2
$ rabbitmqctl cluster_status
Cluster status of node rabbit@rabbit2 ...
....
Maintenance status

Node: rabbit@rabbit1, status: not under maintenance
Node: rabbit@rabbit2, status: not under maintenance
....

接下来我们操作 rabbit3 去加入到 rabbit2 所在的集群,步骤与上面的步骤相同。以证明选择集群到的节点无关紧要——只需提供一个在线节点就足够了。

1
2
3
4
5
6
7
8
9
10
11
12
# on rabbit3
$ rabbitmqctl stop_app
Stopping rabbit application on node rabbit@rabbit3 ...

$ rabbitmqctl reset
Resetting node rabbit@rabbit3 ...

$ rabbitmqctl join_cluster rabbit@rabbit2
Clustering node rabbit@rabbit3 with rabbit@rabbit2

$ rabbitmqctl start_app
Starting node rabbit@rabbit3 ...

在这三个节点的任何节点上运行 cluster_status 命令查看集群状态:

1
2
3
4
5
6
7
8
9
10
$ rabbitmqctl cluster_status
Cluster status of node rabbit@rabbit3 ...
....
Maintenance status

Node: rabbit@rabbit1, status: not under maintenance
Node: rabbit@rabbit2, status: not under maintenance
Node: rabbit@rabbit3, status: not under maintenance
....

通过执行上述步骤,我们可以在集群运行时随时向集群添加新节点。

RabbitMQ 集群维护

重新启动群集节点

已加入群集的节点可以随时停止。它们也可能出现故障或被操作系统终止。

通常,如果节点停止后大多数节点仍处于联机状态,这不会影响集群的其余部分,尽管集群的客户端连接分布、队列副本放置和负载分布会发生变化。

重启前注意从负载均衡器摘除该节点的上游配置。

1
2
3
4
5
systemctl restart rabbitmq-server.service

# or
systemctl stop rabbitmq-server.service
systemctl start rabbitmq-server.service

来自联机对等方的架构同步

重新启动的节点将在启动时同步来自其对等节点的架构和其他信息。在这个过程完成之前,节点将不会完全启动和运行。

因此,了解流程节点在停止和重新启动时所经历的过程非常重要。

停止节点在重新启动后选择要同步的联机群集成员(只考虑磁盘节点)。在重新启动时,节点将尝试默认联系该对等方10次,并有30秒的响应超时。

如果对等方在该时间间隔内可用,则节点成功启动,从对等方同步所需内容并继续运行。

如果对等节点不可用,则重新启动的节点将放弃并自动停止。这种情况可以通过日志中的超时 timeout_waiting_for_tables 警告消息来识别,这些消息最终会导致节点启动失败:

1
2
3
4
2020-07-27 21:10:51.361 [warning] <0.269.0> Error while waiting for Mnesia tables: {timeout_waiting_for_tables,[rabbit@node2,rabbit@node1],[rabbit_durable_queue]}
2020-07-27 21:10:51.361 [info] <0.269.0> Waiting for Mnesia tables for 30000 ms, 1 retries left
2020-07-27 21:11:21.362 [warning] <0.269.0> Error while waiting for Mnesia tables: {timeout_waiting_for_tables,[rabbit@node2,rabbit@node1],[rabbit_durable_queue]}
2020-07-27 21:11:21.362 [info] <0.269.0> Waiting for Mnesia tables for 30000 ms, 0 retries left
1
2
3
4
2020-07-27 21:15:51.380 [info] <0.269.0> Waiting for Mnesia tables for 30000 ms, 1 retries left
2020-07-27 21:16:21.381 [warning] <0.269.0> Error while waiting for Mnesia tables: {timeout_waiting_for_tables,[rabbit@node2,rabbit@node1],[rabbit_user,rabbit_user_permission, …]}
2020-07-27 21:16:21.381 [info] <0.269.0> Waiting for Mnesia tables for 30000 ms, 0 retries left
2020-07-27 21:16:51.393 [info] <0.44.0> Application mnesia exited with reason: stopped
1
2
3
2020-07-27 21:16:51.397 [error] <0.269.0> BOOT FAILED
2020-07-27 21:16:51.397 [error] <0.269.0> ===========
2020-07-27 21:16:51.397 [error] <0.269.0> Timeout contacting cluster nodes: [rabbit@node1].

当节点在关闭期间没有联机对等端时,它将在不尝试与任何已知对等端同步的情况下启动。然而,它并不是作为一个独立的节点启动的,对等节点将能够重新加入它。

因此,当整个集群关闭时,最后一个关闭的节点是唯一一个在关闭时没有任何正在运行的对等节点的节点。该节点可以在不首先联系任何对等方的情况下启动。由于节点将尝试与已知对等方联系长达5分钟(默认情况下),因此可以在这段时间内以任何顺序重新启动节点。在这种情况下,他们将成功地一个接一个地重新加入对方。此时间窗口可以使用两种配置设置进行调整:

1
2
3
4
5
# wait for 60 seconds instead of 30
mnesia_table_loading_retry_timeout = 60000

# retry 15 times instead of 10
mnesia_table_loading_retry_limit = 15

通过调整这些设置和调整已知对等方必须返回的时间窗口,可以考虑到可能需要5分钟以上才能完成的集群范围内的重新部署场景。

在升级过程中,有时最后一个停止的节点必须是升级后第一个启动的节点。该节点将被指定执行集群范围的模式迁移,其他节点可以从该迁移同步并在重新加入时应用。

重新启动和运行状况检查(就绪探测器)

在某些环境中,通过指定的运行状况检查来控制节点的重新启动。检查验证一个节点是否已启动,并且部署过程可以继续到下一个节点。如果检查不通过,则认为节点的部署不完整,部署过程通常会等待一段时间并重试。这种环境的一个流行例子是Kubernetes,在这里,当使用OrderedReady pod管理策略时,操作员定义的就绪探测可以阻止部署继续进行。使用并行吊舱管理策略的部署不会受到影响,但必须担心初始集群形成期间的自然竞争条件。

考虑到上述对等同步行为,这样的健康检查可能会阻止集群范围内的重新启动及时完成。显式或隐式地假设重新加入其群集对等体的完全启动节点将失败并阻止进一步的节点部署的检查。

大多数健康检查,甚至是相对基本的健康检查,都隐含地假设节点已经完成引导。它们不适用于正在等待来自对等方的架构表同步的节点。

这种检查的一个非常常见的例子:

1
2
3
4
5
6
7
# will exit with an error for the nodes that are currently waiting for
# a peer to sync schema tables from


$ rabbitmq-diagnostics check_running
Checking if RabbitMQ is running on node rabbit@rabbit1 ...
RabbitMQ on node rabbit@rabbit1 is fully booted and running
1
2
3
4
5
6
7
# a very basic check that will succeed for the nodes that are currently waiting for
# a peer to sync schema from


$ rabbitmq-diagnostics ping
Will ping rabbit@rabbit1. This only checks if the OS process is running and registered with epmd. Timeout: 60000 ms.
Ping succeeded

在重启时修改主机名

如果节点名称或主机名更改后重新加入的节点的数据目录路径因此而更改,则该节点可以作为空白节点启动。这样的节点将无法重新加入集群。当节点处于脱机状态时,其对等节点可以重置或使用空白数据目录启动。在这种情况下,恢复节点也将无法重新加入其对等节点,因为内部数据存储集群标识将不再匹配。

考虑以下情况:

  1. 形成由A、B和C三个节点组成的集群

  2. 节点A已关闭

  3. 节点B已重置

  4. 节点A已启动

  5. 节点A试图重新加入B,但B的群集标识已更改

  6. 节点B无法将A识别为已知群集成员,因为它已重置

  7. 在这种情况下,节点B将在日志中用适当的错误消息拒绝来自A的集群尝试:

1
Node 'rabbit@node1.local' thinks it's clustered with node 'rabbit@node2.local', but 'rabbit@node2.local' disagrees

在这种情况下,B可以再次重置,然后能够加入A,或者A可以重置,然后成功加入B。

参考文档: