RabbitMQ高可用集群搭建

RabbitMQ + HAProxy 高可用镜像模式集群部署

本文介绍了如何搭建 RabbitMQ 高可用集群。首先介绍了部署说明和前提条件,然后详细介绍了集群模式和节点类型。接着介绍了环境准备和集群搭建的步骤,包括普通模式和镜像模式的配置。然后介绍了集群节点管理和故障处理的方法。最后介绍了使用 HAProxy 负载均衡的方法,并提供了安装、配置和验证的步骤。最后列举了一些常见问题。

RabbitMQ 高可用集群搭建

部署说明

下面将以一个示例说明多机部署一个高可用 RabbitMQ 集群的流程。

前提

在部署集群前,必须在将成为集群成员的每个节点上安装 RabbitMQ,并确保每个节点之间能相互访问。

RabbitMQ 集群模式

  • 普通模式(默认的集群模式)

    以两个节点 node1、node2 为例来进行说明。

    对于 queue 来说,消息实体只存在于其中一个节点(node1 或 node2),node1 和 node2 两个节点仅有相同的元数据,即队列的结构。当消息进入 node1 节点的 queue 后,consumer 从 node2 节点消费时,RabbitMQ 会临时在 node1、node2 间进行消息传输,把 node1 中的消息实体取出并经过 node2 发送给 consumer。所以 consumer 应尽量连接每一个节点,从中取消息。即对于同一个逻辑队列,要在多个节点建立物理 queue。否则无论 consumer 连接 node1 或 node2,出口总在 node1,会产生瓶颈。

    当 node1 节点故障后,node2 节点无法取到 node1 节点中还未消费的消息实体。如果做了消息持久化,那么得等 node1 节点恢复,然后才可被消费;如果没有持久化的话,就会产生消息丢失的现象。

  • 镜像模式

    把需要的队列做成镜像队列,存在于多个节点,属于 RabbiMQ 的 HA 方案,在对业务可靠性要求较高的场合中比较适用。

    该模式解决了普通模式中的问题,其实质和普通模式不同之处在于,消息实体会主动在镜像节点间同步,而不是在客户端取数据时临时拉取。该模式带来的副作用也很明显,除了降低系统性能外,如果镜像队列数量过多,加之大量的消息进入,集群内部的网络带宽将会被这种同步通讯大大消耗掉。所以在对可靠性要求较高的场合中适用。

    要实现镜像模式,需要先搭建一个普通集群模式,在这个模式的基础上再配置策略以实现高可用。

集群节点类型

  • 内存(ram)节点
  • 磁盘(disk)节点

RAM 节点仅将内部数据库表存储在 RAM 中。这不包括消息,消息存储索引,队列索引和其他节点状态。

在大多数情况下,您希望所有节点都是磁盘节点。 RAM 节点是一种特殊情况,可用于提高 queue、exchange 或 binding 流失率较高的群集的性能。 RAM 节点不提供更高的消息速率。如有疑问,请仅使用磁盘节点。

由于 RAM 节点仅将内部数据库表存储在RAM中,因此它们必须在启动时从对等节点同步它们。这意味着一个群集必须至少包含一个磁盘节点。因此,不可能手动删除集群中最后剩余的磁盘节点。

在 RabbitMQ 集群中,当磁盘节点宕掉且集群中无其他可用的磁盘节点时,集群将无法写入新的队列元数据信息。

环境准备

  • 系统系统:CentOS7 64位

  • 三台服务器:192.168.0.231/232/233

  • 服务器规划

    服务器 用途 主机名 节点类型
    192.168.0.231 RabbitMQ 集群节点 1 node231 磁盘节点
    192.168.0.232 RabbitMQ 集群节点 2 node232 磁盘节点
    192.168.0.233 RabbitMQ 集群节点 3 node233 磁盘节点

集群搭建

普通模式

配置 hosts

vim /etc/hosts 编辑三个节点的 hosts 文件,在文件末尾添加如下内容:

1
2
3
192.168.0.231 node231
192.168.0.232 node232
192.168.0.233 node233

配置 hostname

  • node231

    vim /etc/hostname 编辑主机名如下:

    1
    node231

    vim /etc/sysconfig/network 编辑网络配置文件,添加如下内容:

    1
    2
    NETWORKING=yes
    HOSTNAME=node231

    重启 network

    1
    systemctl restart network
  • node232

    vim /etc/hostname 编辑主机名如下:

    1
    node232

    vim /etc/sysconfig/network 编辑网络配置文件,添加如下内容:

    1
    2
    NETWORKING=yes
    HOSTNAME=node232

    重启 network

    1
    systemctl restart network
  • node233

    vim /etc/hostname 编辑主机名如下:

    1
    node233

    vim /etc/sysconfig/network 编辑网络配置文件,添加如下内容:

    1
    2
    NETWORKING=yes
    HOSTNAME=node233

    重启 network

    1
    systemctl restart network

RabbitMQ 集群是基于 erlang 进行同步的,在 erlang 的集群中各节点同步需要一个相同的 cookie,所以必须保证各节点 cookie 一致,不然节点之间就无法通信。这个 cookie 默认存放在 /var/lib/rabbitmq/.erlang.cookie 中。

在任意一个节点中 copy .erlang.cookie 文件到其它所有节点,如在 node1 上进行 copy :

1
2
[root@node231 ~]# scp /var/lib/rabbitmq/.erlang.cookie root@192.168.0.232:/var/lib/rabbitmq/
[root@node231 ~]# scp /var/lib/rabbitmq/.erlang.cookie root@192.168.0.233:/var/lib/rabbitmq/

重启节点

如果后面执行 rabbitmqctl stop_app 失败,需要重启 node231、node232、node233 使配置生效。

启动 rabbitmq-server

分别启动 node231、node232、node233 的 rabbitmq-server:

1
2
3
[root@node231 ~]# systemctl start rabbitmq-server
[root@node232 ~]# systemctl start rabbitmq-server
[root@node233 ~]# systemctl start rabbitmq-server

将节点加入集群

将 node232、node233 节点加入 node231 节点集群中,在 node232、node233 中分别执行以下命令:

1
2
3
# rabbitmqctl stop_app
# rabbitmqctl join_cluster rabbit@node231
# rabbitmqctl start_app
  • 默认 RabbitMQ 启动后是磁盘节点,在这个 cluster 下,node231、node232 和 node233 都是是磁盘节点。

  • 如果要使 node232、node233 都是内存节点,加上 --ram 参数即可,如 rabbitmqctl join_cluster --ram rabbit@node232

  • 如果想要更改节点类型,可以使用命令 rabbitmqctl change_cluster_node_type disc(ram),修改节点类型前需要先 rabbitmqctl stop_app

    (Note: disk and disc are used interchangeably)

查看集群状态

任意节点执行:

1
# rabbitmqctl cluster_status

创建管理用户

如果在主机名变更前就已经创建过用户的,仍需要重新重新创建,因为主机名的变更,之前创建的用户无法登录 web 管理系统。

以下操作在 node231 下执行:

  • 创建 vhost(可选,默认使用 "/" vhost)

    这里创建一个 vhost 用于测试:

    1
    [root@node231 ~]# rabbitmqctl add_vhost testvhost
  • 创建用户

    1
    [root@node231 ~]# add_user admin password
  • 设置用户角色

    1
    [root@node231 ~]# set_user_tags admin administrator
  • 设置用户权限

    1
    [root@node231 ~]# set_permissions -p testvhost admin ".*" ".*" ".*"

启用 rabbitmq management

在 node231 上启用 rabbitmq management

1
[root@node231 ~]# rabbitmq-plugins enable rabbitmq_management

在浏览器中访问 http://192.168.0.231:15672,使用 "admin/password" 即可登录。

web-management

镜像模式

上面已经完成 RabbitMQ 默认集群模式,但并不保证队列的高可用性,尽管 Exchanges、Bindings 这些可以复制到集群里的任何一个节点,但是队列内容不会复制。所以集群中的节点宕机后将直接导致队列无法应用或消息丢失,要想队列在节点宕机或故障时也能正常应用,需要复制队列内容到集群中的每个节点,这就要使用镜像队列了。

为了确认队列内容默认不会复制,我们需要做些实验。

  1. 首先我们在 node231 节点上创建一个名为 demo_task 的持久化队列(相关的 exchange 也是持久化的),这事可以看到队列的元信息会立即同步到 node232 和 node233 上。

  2. 然后我们连接到 node231 节点上发布一个消息,发布成功后,你可以在所有节点上获取到该消息。

  3. 然后我们使用 systemctl stop rabbitmq-server 命令将 node231 节点关闭,此时发现 node232 和 node233 虽然还保留了 demo_task 的元信息,但却无法从中获取消息了。

    node-down

  4. 当我们使用 systemctl start rabbitmq-server 命令再次将 node231 节点启动后,node232 和 node233 依然能看到 node231 关闭前已经持久化的消息。

    node-run

镜像队列

默认情况下,RabbitMQ 集群中 queue 的内容位于单个节点(声明该 queue 的节点)上。这与 exchanges 和 bindings 相反,exchanges 和 bindings 始终可以被视为在所有节点上。可以选择使 queue 跨多个节点进行镜像。

每个镜像队列由一个 master 和一个或多个镜像(mirrors)组成。master 托管在一个通常称为主节点的节点上。每个队列都有其自己的主节点。给定队列的所有操作都首先应用于队列的主节点,然后传播到镜像节点。这涉及排队发布,向消费者传递消息,跟踪来自消费者的确认等。

队列镜像意味着节点的集群。发布到队列的消息将复制到所有镜像。无论消费者连接到哪个节点,最终都会被连接到主节点,镜像节点都会丢弃已在主节点上确认的消息。因此,队列镜像可提高可用性,但不会在节点之间分配负载(所有参与的节点均完成所有工作)。

如果承载队列主节点发生故障,则最早的镜像将在同步后提升为新的主节点。根据队列镜像参数,也可以升级不同步的镜像。

配置镜像策略

使用策略(policiy)配置镜像参数。 一个策略按名称(使用正则表达式模式)匹配一个或多个队列,并且包含一个定义(可选参数的映射),该定义被添加到匹配队列的全部属性中。

通过控制台添加策略

add-policy

如果其他节点也启用了 rabbitmq_management,此时其他节点的控制台,可以看到上面添加的这个策略,如图所示:

show-policies

参数说明:

  • Virtual host:策略应用的 vhost。

  • Name:为策略名称,可以是任何东西,但建议使用不带空格的基于ASCII的名称。

  • Pattern:与一个或多个 queue(exchange) 名称匹配的正则表达式,可以使用任何正则表达式。只有一个 ^ 代表匹配所有,^test 为匹配名称为 "test" 的 exchanges 或者 queue。

  • Apply to:Pattern 应用对象。

  • Priority:配置了多个策略时候的优先级,值越大,优先级越高。

    (没有指定优先级的消息会以0优先级对待。对于超过队列所定最大优先级的消息,优先级以最大优先级对待)

  • Definition:一组键/值对(例如 JSON 文档),将被插入匹配 queues and exchanges 的可选参数映射中

    ha-mode:策略键,分为3种模式

    • all - 所有(所有的 queue)
    • exctly - 部分(需配置 ha-params 参数,此参数为 int 类型。比如 3,众多集群中的随机 3 台机器)
    • nodes - 指定(需配置 ha-params 参数,此参数为数组类型。比如 ["rabbit@node2", "rabbit@node3"] 这样指定为 node2 与 node3 这两台机器)

    ha-sync-mode:队列同步

    • manual:手动(默认模式)。新的队列镜像将不会收到现有的消息,它只会接收新的消息
    • automatic:自动同步。当一个新镜像加入时,队列会自动同步。队列同步是一个阻塞操作。
通过命令行添加策略
  • 设置

    1
    rabbitmqctl set_policy [-p <vhost>] [--priority <priority>] [--apply-to <apply-to>] <name> <pattern>  <definition>
  • 清除

    1
    rabbitmqctl clear_policy [-p <vhost>] <name>
  • 查看

    1
    rabbitmqctl list_policies [-p <vhost>]

例如:

1
2
# rabbitmqctl set_policy -p testvhost testha "^" '{"ha-mode":"all","ha-sync-mode":"automatic"}'
Setting policy "testha" for pattern "^" to "{"ha-mode":"all","ha-sync-mode":"automatic"}" with priority "0" for vhost "testvhost" ...
1
2
3
4
# rabbitmqctl list_policies -p testvhost
Listing policies for vhost "testvhost" ...
vhost name pattern apply-to definition priority
testvhost testha ^ all {"ha-mode":"all","ha-sync-mode":"automatic"} 0
1
2
# rabbitmqctl clear_policy -p testvhost testha
Clearing policy "testha" on vhost "testvhost" ...

测试策略是否生效

在控制台的队列页面上,镜像队列将展示策略名称和其他副本(镜像)数量。以下是一个名为 three_replicas 的队列的示例,该队列具有一个 master(主节点)和两个镜像节点。

添加队列:

add-queue

查看队列:

show-queues

queue-detail

集群节点管理

故障节点重新加入集群

删除 /var/lib/rabbitmq/mnesia/ 下的数据,重新启动 rabbitmq-server,正常执行节点增加操作。

1
2
rm -rf /var/lib/rabbitmq/mnesia/
systemctl restart rabbitmq-server.service

节点增加

注:需要同步 .erlang.cooike 内容(.erlang.cooike 的权限为 400,所属者为 rabbitmq)

在需要加入的节点机器上执行:

1
2
3
4
rabbitmqctl stop_app
rabbitmqctl join_cluster --ram rabbit@node231
rabbitmqctl start_app
rabbitmqctl cluster_status

节点删除

正常删除

在需要删除的节点(rabbitmq-server 正常运行)机器上执行:

1
2
3
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl start_app

硬删除

在集群正常节点将故障节点踢出,在其它正常的节点上执行(故障节点 rabbitmq-server 服务不可用):

1
rabbitmqctl forget_cluster_node rabbit@node232

改变节点类型

disc -> ram

1
2
3
4
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl join_cluster --ram rabbit@node231
rabbitmqctl start_app

ram -> disc

1
2
3
4
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl join_cluster rabbit@node231
rabbitmqctl start_app

HAProxy 负载均衡

HAProxy 是由 C 语言编写的免费的开源软件,它快速而高效,可为基于TCP和HTTP的应用程序提供高可用、高性能的负载平衡器和代理服务器。

环境准备

服务器 用途 系统 版本
192.168.0.235 负载均衡服务器 Ubuntu 20.04 haproxy 2.0

安装

Ubuntu

1
$ sudo apt install haproxy

CentOS 7

1
2
3
wget http://www.haproxy.org/download/1.7/src/haproxy-1.7.12.tar.gz
tar -zxvf haproxy-1.7.12.tar.gz
cd haproxy-1.7.12

根据内核版本,选择编译参数

查看内核版本:

1
2
[root@dev235 haproxy-1.7.12]# uname -r
3.10.0-1127.19.1.el7.x86_64

如:3.10.0-1127.19.1.el7.x86_64,此时该参数就为 linux310;kernel 大于 2.6.28 的可以用: TARGET=linux2628

查看编译参数:

1
[root@dev235 haproxy-1.7.12]# less README

编译安装:

1
2
make TARGET=linux310 ARCH=x86_64 PREFIX=/usr/local/haproxy
make install PREFIX=/usr/local/haproxy
  • TARGET=linux310,内核版本,如:3.10.0-1127.19.1.el7.x86_64,此时该参数就为 linux310;kernel 大于 2.6.28 的可以用: TARGET=linux2628
  • ARCH=x86_64,系统位数;
  • PREFIX=/usr/local/haprpxy ,haproxy 安装路径;
使用 systemclt 管理 haproxy

haproxy-1.7.12 中有 systemd 脚本可以使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[root@dev235 haproxy-1.7.12]# cat contrib/systemd/haproxy.service.in
[Unit]
Description=HAProxy Load Balancer
After=network.target

[Service]
Environment="CONFIG=/etc/haproxy/haproxy.cfg" "PIDFILE=/run/haproxy.pid"
ExecStartPre=@SBINDIR@/haproxy -f $CONFIG -c -q
ExecStart=@SBINDIR@/haproxy-systemd-wrapper -f $CONFIG -p $PIDFILE
ExecReload=@SBINDIR@/haproxy -f $CONFIG -c -q
ExecReload=/bin/kill -USR2 $MAINPID
KillMode=mixed
Restart=always

[Install]
WantedBy=multi-user.target

copy 到 /lib/systemd/system 下,并修改 @SBINDIR@ 为 /usr/local/haproxy/sbin,将 haproxy-systemd-wrapper 替换为 haproxy

1
2
3
cp contrib/systemd/haproxy.service.in /lib/systemd/system/haproxy.service
systemctl daemon-reload
systemctl restart haproxy.service

配置

vim /etc/haproxy/haproxy.cfg 编辑 haproxy 配置文件,修改如下:

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
global
maxconn 512
stats socket /tmp/haproxy

defaults
log global
mode http
option abortonclose
compression algo gzip
compression type text/html text/plain application/json
timeout connect 5000ms
# timeout connect 3600s

listen stats
bind *:8888
mode http
log 127.0.0.1 local3 err
stats refresh 60s
stats uri /stats
stats realm Haproxy\ Manager
stats auth admin:password
stats hide-version
stats admin if TRUE

listen rabbitmq_cluster
bind *:5672
mode tcp
option tcpka
balance roundrobin
server rabbit1 192.168.0.231:5672 check inter 1000 rise 2 fall 3 weight 1
server rabbit2 192.168.0.232:5672 check inter 1000 rise 2 fall 3 weight 1
server rabbit3 192.168.0.233:5672 check inter 1000 rise 2 fall 3 weight 1

验证 HAProxy 配置

修改配置后,在启动 HAProxy 前,应先运行以下命令验证配置文件语法:

1
2
$ haproxy -f /etc/haproxy/haproxy.cfg -c -V
Configuration file is valid

如果收到错误消息,请务必先修复,然后再继续。

运行 HAProxy

1
$ systemctl restart haproxy

查看状态:

1
$ systemctl status haproxy

HAProxy Statistics

浏览器访问 http://192.168.0.235:8888/stats,输入配置中的用户名和密码登录:

haproxy-statistics

常见问题

1. 新节点 join_cluster 失败

问题描述:

新创建的 RabbitMQ 节点,在 join_cluster 时失败,提示如下信息:

1
2
3
4
[root@node232 ~]# rabbitmqctl join_cluster rabbit@node231.com
Clustering node rabbit@node232 with rabbit@node231.com
Error:
{:badrpc_multi, {:EXIT, {{:function_clause, [{:gen, :do_for_proc, [{:rex, {:error, {:node_name, :short}}}, #Function<0.9801092/1 in :gen.call/4>], [file: 'gen.erl', line: 220]}, {:gen_server, :call, 3, [file: 'gen_server.erl', line: 219]}, {:rpc, :do_call, 3, [file: 'rpc.erl', line: 327]}, {:lists, :foldl, 3, [file: 'lists.erl', line: 1263]}, {:rabbit_mnesia, :discover_cluster, 1, [file: 'src/rabbit_mnesia.erl', line: 779]}, {:rabbit_mnesia, :join_cluster, 2, [file: 'src/rabbit_mnesia.erl', line: 212]}, {:rpc, :"-handle_call_call/6-fun-0-", 5, [file: 'rpc.erl', line: 197]}]}, {:gen_server, :call, [{:rex, {:error, {:node_name, :short}}}, {:call, :rabbit_mnesia, :cluster_status_from_mnesia, [], #PID<0.62.0>}, :infinity]}}}, [error: {:node_name, :short}]}

问题原因:

rabbit@hostname ,hostname 不允许包含 . 字符,如果主机名是 node231.com,则应该在 /etc/hosts 同时指定 node231 映射的 IP。

解决方法:

  1. 编辑 /etc/hosts 文件,添加 192.168.0.231 node231

  2. 使用 rabbit@node231 而不是 rabbit@node231.com

    1
    2
    3
    4
    5
    [root@node232 ~]# rabbitmqctl join_cluster rabbit@node231
    Clustering node rabbit@node232 with rabbit@node231
    [root@node232 ~]# rabbitmqctl start_app
    Starting node rabbit@node232 ...
    completed with 3 plugins.
  3. 查看集群状态。

    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
    [root@node232 ~]# rabbitmqctl cluster_status
    Cluster status of node rabbit@node232 ...
    Basics

    Cluster name: rabbit@node231

    Disk Nodes

    rabbit@node231
    rabbit@node232
    rabbit@node233

    Running Nodes

    rabbit@node231
    rabbit@node232
    rabbit@node233

    Versions

    rabbit@node231: RabbitMQ 3.8.1 on Erlang 22.2.1
    rabbit@node232: RabbitMQ 3.8.1 on Erlang 22.2.1
    rabbit@node233: RabbitMQ 3.8.1 on Erlang 23.2.3

    Alarms

    (none)

    Network Partitions

    (none)

    Listeners

    Node: rabbit@node231, interface: [::], port: 25672, protocol: clustering, purpose: inter-node and CLI tool communication
    Node: rabbit@node231, interface: [::], port: 5672, protocol: amqp, purpose: AMQP 0-9-1 and AMQP 1.0
    Node: rabbit@node231, interface: [::], port: 15672, protocol: http, purpose: HTTP API
    Node: rabbit@node232, interface: [::], port: 25672, protocol: clustering, purpose: inter-node and CLI tool communication
    Node: rabbit@node232, interface: [::], port: 5672, protocol: amqp, purpose: AMQP 0-9-1 and AMQP 1.0
    Node: rabbit@node232, interface: [::], port: 15672, protocol: http, purpose: HTTP API
    Node: rabbit@node233, interface: [::], port: 25672, protocol: clustering, purpose: inter-node and CLI tool communication
    Node: rabbit@node233, interface: [::], port: 5672, protocol: amqp, purpose: AMQP 0-9-1 and AMQP 1.0
    Node: rabbit@node233, interface: [::], port: 15672, protocol: http, purpose: HTTP API

    Feature flags

    Flag: drop_unroutable_metric, state: enabled
    Flag: empty_basic_get_metric, state: enabled
    Flag: implicit_default_bindings, state: enabled
    Flag: quorum_queue, state: enabled
    Flag: virtual_host_metadata, state: enabled

2. 'gzip' is not a supported algorithm

问题描述:

1
2
3
4
5
[root@dev235 ~]# haproxy -f /etc/haproxy/haproxy.cfg
[ALERT] 103/185121 (15714) : parsing [/etc/haproxy/haproxy.cfg:9] : 'compression' : 'gzip' is not a supported algorithm.

[ALERT] 103/185121 (15714) : Error(s) found in configuration file : /etc/haproxy/haproxy.cfg
[ALERT] 103/185121 (15714) : Fatal errors found in configuration.

问题原因:

Add zlib support in haproxy for compression offloading

解决方法:

This lies in script build-haproxy.sh, where adding USE_ZLIB=1 in the make stanza would do it.

重新编译安装

1
2
make TARGET=linux310 ARCH=x86_64 PREFIX=/usr/local/haproxy USE_ZLIB=1
make install PREFIX=/usr/local/haproxy

References

https://www.rabbitmq.com/clustering.html

https://www.rabbitmq.com/ha.html

https://www.rabbitmq.com/parameters.html#policies

http://www.haproxy.org/

rabbitmq无法重新加入集群,启动失败的问题