4、基于HAProxy和Keepalived的RabbitMQ集群高可用部署
1、版本号与网络拓扑规划
1.1、软件版本
RabbitMQ是版本3.13.6
Erlang采用的匹配版本26.2.5.21
erlang版本号和RabbitMQ版本号的对应关系
https://www.rabbitmq.com/docs/which-erlang
这里采用3.13.6,所以对应erlang版本号可以采用26.2.x,这里用的是26.2.5.21,下载地址:
https://github.com/erlang/otp/releases/download/OTP-26.2.5.21/otp_src_26.2.5.21.tar.gz
https://github.com/rabbitmq/rabbitmq-server/releases/download/v3.13.6/rabbitmq-server-generic-unix-3.13.6.tar.xz
1.2、机器规划
准备三台虚拟机,可以用克隆功能。 192.168.0.61 RabbitMQ内存节点 HAPRoxy Keepalived(master) 192.168.0.62 RabbitMQ内存节点 HAPRoxy Keepalived(backup) 192.168.0.63 RabbitMQ磁盘节点 VIP 192.168.0.60
修改对应主机的/etc/hosts文件,配置IP和主机名称的映射关系(根据自己的具体情况配置)
192.168.0.61 study-server-1
192.168.0.62 study-server-2
192.168.0.63 study-server-3
1.3、网络结构图

2、单机安装
2.1、编译安装erlang
- 更新
sudo apt-get update
sudo apt-get upgrade
- 安装依赖
sudo apt-get install build-essential autoconf libncurses5-dev libssl-dev m4 unixodbc-dev
- 指定安装目录
./configure --prefix=/usr/local/erlang
- 编译
sudo make
- 安装
sudo make install
- 配置配置环境变量
修改文件/etc/profile,在文件末尾添加一行
export PATH=$PATH:/usr/local/erlang/bin
执行命令使文件生效
source /etc/profile
- 验证
执行命令验证erlang是否安装成功,如果正常能够看到目标版本号输出,则说明配置成功

2.2、安装rabbitmq
- 解压
rabbitmq下载下来的是一个后缀为xz的文件,并不是一个标准的tar或者tar.gz
xz -d rabbitmq-server-generic-unix-3.13.6.tar.xz
tar -xf rabbitmq-server-generic-unix-3.13.6.tar
解压完成后,进入到解压后的rabbitmq_server-3.13.6目录下,rabbitmq相关的,操作命令都在sbin目录下


2.3、启动rabbitmq
正常情况下,到这一步就可以启动rabbitmq了,可以使用rabbitmq-server脚本启动

使用
start参数是直接启动,如果采用后台方式启动,可以改用参数-detached./rabbitmq-server -detached
2.4、开启UI管理界面
通过上面的操作,rabbitmq就正常运行起来了,但是如果这个时候我们要对rabbitmq进行操作的话是很困难的,因为默认情况下rabbitmq的UI管理后台是未启用状态,它是属于Rabbitmq的一个插件,需要手动启用才行
- 查看插件状态

可以看到rabbitmq_management插件是未启用状态
- 启用插件
可以执行以下命令进行启用
./rabbitmq-plugins enable rabbitmq_management

- 重新启动

启动成功后就可以访问15672端口了

但是这个时候用默认的guest账号密码进行登录的话可能会报guest这个用户只能通过localhost访问,这是因为rabbitmq从3.3.0开始禁止使用guest/guest权限通过除localhost外的访问

- 账号管理
可以通过命令查看以下当前有哪些账号:

这里不妨添加一个我们自己的账号:
./rabbitmqctl add_user admin 123abc32110
./rabbitmqctl set_user_tags admin administrator
这里账号名我用的admin,密码是随便设置的123abc32110,并且给admin设置了超级管理员角色
再次查看用户列表,就可以看到刚刚创建的admin账号了

- 管理员界面
管理界面插件启用了,也有了自己的管理员账号,就可以用新创建的这个账号登录到UI管理后台了



2.5、配置rabbitmq到环境变量
修改/etc/profile文件,在文件末尾添加一行
export PATH=$PATH:/home/ubuntu/tools/rabbitmq_server-3.13.6/sbin
执行source命令使配置生效
source /etc/profile
3、集群配置
经过上面的操作,我们得到了三个独立的rabbitmq节点,但是为了高可用,我们的目标是集群模式,所以还需要做一些操作
3.1、启动节点3
rabbitmq-server -detached > nohup.out &
rabbitmqctl start_app

3.2、启动节点1
rabbitmq-server -detached > nohup.out &
rabbitmqctl start_app
rabbitmqctl stop_app
rabbitmqctl join_cluster --ram rabbit@study-server-3
执行join_cluster的时候可能会报下面这个错:

这主要是因为两个节点的~/.erlang.cookie文件内容不一致,导致rabbit无法进行节点通信,这里可以将集群所有节点上这个文件都统一一下,再执行的时候就可以正常加入节点了。
节点加入成功之后可以上192.168.0.63上面看一下节点的状态:

页面上提示节点1没有运行,这是因为前面我们在将节点加入集群之前停掉了服务
- 启动服务
rabbitmqctl start_app
上192.168.0.63上查看节点状态:

3.3、启动节点2
参照3.2操作步骤,启动节点2,并将其加入到集群中
修改
~/.erlang.cookie文件运行服务 & 加入集群
rabbitmq-server -detached > nohup.out &
rabbitmqctl start_app
rabbitmqctl stop_app
rabbitmqctl join_cluster --ram rabbit@study-server-3
rabbitmqctl start_app
至此,我们的基础版的三节点集群就搭建完成了

3.4、设置镜像队列
在所有的节点执行下面命令设置集群镜像队列
rabbitmqctl set_policy ha-all "^" '{"ha-mode":"all"}'

这个策略意思是对默认vhost下的交换机和队列做HA镜像,ha-mode参数的值有:
all:集群的所有节点
exactly:镜像到集群内指定数量的节点,如果节点数少于这个值,会自动镜像到所有节点,如果大于该值,如果镜像的节点停止了,新的镜像也不会在其它节点上创建
nodes:指定镜像的节点名称,如果指定的节点在集群中不存在也不会报错,当队列申明的时候,如果指定的节点不在线,队列会被创建在当前客户端连接的节点上
管理员菜单下的策略管理那里也可以看到新添加的这个策略:

查看管理控制台可以看到交换机的属性,多了一个ha-all:

同样的,查看队列信息可以看到Features多了ha-all,而且Node后面多了一个+2,也就是有两个镜像

3.5、补充
从前面的操作可以看大,我们在将节点加入集群的时候指定了一个参数-ram,这个参数的意思是指定将该节点设置为内存节点(默认是磁盘节点,对应值-disc),主要的目的是为了在利用内存的读取效率提升集群消息的吞吐量,但是在高版本的UI管理控制台上貌似现在看不出来节点类型了,不过可以通过命令查看
ubuntu@study-server-2:~/tools/rabbitmq_server-3.13.6$ rabbitmqctl cluster_status
Cluster status of node rabbit@study-server-2 ...
Basics
Cluster name: rabbit@study-server-2
Total CPU cores available cluster-wide: 6
Disk Nodes
rabbit@study-server-3
RAM Nodes
rabbit@study-server-1
rabbit@study-server-2
Running Nodes
rabbit@study-server-1
rabbit@study-server-2
rabbit@study-server-3
Versions
rabbit@study-server-2: RabbitMQ 3.13.6 on Erlang 26.2.5.21
rabbit@study-server-3: RabbitMQ 3.13.6 on Erlang 26.2.5.21
rabbit@study-server-1: RabbitMQ 3.13.6 on Erlang 26.2.5.21
CPU Cores
Node: rabbit@study-server-2, available CPU cores: 2
Node: rabbit@study-server-3, available CPU cores: 2
Node: rabbit@study-server-1, available CPU cores: 2
Maintenance status
Node: rabbit@study-server-2, status: not under maintenance
Node: rabbit@study-server-3, status: not under maintenance
Node: rabbit@study-server-1, status: not under maintenance
Alarms
(none)
Network Partitions
(none)
Listeners
Node: rabbit@study-server-2, interface: [::], port: 15672, protocol: http, purpose: HTTP API
Node: rabbit@study-server-2, interface: [::], port: 25672, protocol: clustering, purpose: inter-node and CLI tool communication
Node: rabbit@study-server-2, interface: [::], port: 5672, protocol: amqp, purpose: AMQP 0-9-1 and AMQP 1.0
Node: rabbit@study-server-3, interface: [::], port: 15672, protocol: http, purpose: HTTP API
Node: rabbit@study-server-3, interface: [::], port: 25672, protocol: clustering, purpose: inter-node and CLI tool communication
Node: rabbit@study-server-3, interface: [::], port: 5672, protocol: amqp, purpose: AMQP 0-9-1 and AMQP 1.0
Node: rabbit@study-server-1, interface: [::], port: 15672, protocol: http, purpose: HTTP API
Node: rabbit@study-server-1, interface: [::], port: 25672, protocol: clustering, purpose: inter-node and CLI tool communication
Node: rabbit@study-server-1, interface: [::], port: 5672, protocol: amqp, purpose: AMQP 0-9-1 and AMQP 1.0
Feature flags
Flag: classic_mirrored_queue_version, state: enabled
Flag: classic_queue_type_delivery_support, state: enabled
Flag: detailed_queues_endpoint, state: enabled
Flag: direct_exchange_routing_v2, state: enabled
Flag: drop_unroutable_metric, state: enabled
Flag: empty_basic_get_metric, state: enabled
Flag: feature_flags_v2, state: enabled
Flag: implicit_default_bindings, state: enabled
Flag: khepri_db, state: disabled
Flag: listener_records_in_ets, state: enabled
Flag: maintenance_mode_status, state: enabled
Flag: message_containers, state: enabled
Flag: message_containers_deaths_v2, state: enabled
Flag: quorum_queue, state: enabled
Flag: quorum_queue_non_voters, state: enabled
Flag: restart_streams, state: enabled
Flag: stream_filtering, state: enabled
Flag: stream_queue, state: enabled
Flag: stream_sac_coordinator_unblock_group, state: enabled
Flag: stream_single_active_consumer, state: enabled
Flag: stream_update_config_command, state: enabled
Flag: tracking_records_in_ets, state: enabled
Flag: user_limits, state: enabled
Flag: virtual_host_metadata, state: enabled
从这里还是可以看出节点1和节点2是内存节点,节点3是磁盘节点,而在早期的版本是可以直接在UI管理控制台界面里看出来的,如图:

4、安装HAProxy
4.1、安装HAProxy
在三个节点上分别安装HAProxy
sudo apt-get install haproxy
4.2、修改配置
haproxy的配置文件在/etc/haproxy

修改haproxy.cfg文件,添加haproxy的9188监测端口、rabbitmq的UI界面15672端口和客户端5672端口,具体文件内容参考如下:
global
log /dev/log local0
log /dev/log local1 notice
chroot /var/lib/haproxy
stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners
stats timeout 30s
user haproxy
group haproxy
daemon
# Default SSL material locations
ca-base /etc/ssl/certs
crt-base /etc/ssl/private
# See: https://ssl-config.mozilla.org/#server=haproxy&server-version=2.0.3&config=intermediate
ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets
defaults
log global
mode http
option httplog
option dontlognull
timeout connect 5000
timeout client 50000
timeout server 50000
errorfile 400 /etc/haproxy/errors/400.http
errorfile 403 /etc/haproxy/errors/403.http
errorfile 408 /etc/haproxy/errors/408.http
errorfile 500 /etc/haproxy/errors/500.http
errorfile 502 /etc/haproxy/errors/502.http
errorfile 503 /etc/haproxy/errors/503.http
errorfile 504 /etc/haproxy/errors/504.http
###################### 打开haproxy的监测界面###############################
listen rabbitmq-status
bind 0.0.0.0:9188
mode http
stats enable
stats refresh 30s
stats uri /stats #设置haproxy监控地址为http://localhost:9188/stats
stats auth admin:123abc32110 #添加用户名密码认证
stats realm (Haproxy\ statistic)
stats admin if TRUE
######################监听rabbitmq的web操作页面############################
listen rabbitmq-server-ui
bind 0.0.0.0:15670
mode tcp
balance roundrobin
option tcplog
server study-server-1 192.168.0.61:15672 check
server study-server-2 192.168.0.62:15672 check
server study-server-3 192.168.0.63:15672 check
#######################监听rabbimq_cluster #################################
listen rabbitmq-server
bind 0.0.0.0:5670
mode tcp
balance roundrobin
option tcplog
server study-server-1 192.168.0.61:5672 check
server study-server-2 192.168.0.62:5672 check
server study-server-3 192.168.0.63:5672 check
为了避免端口冲突,由于上面我们将15672端口映射到15670的端口监听上、5672的端口映射到了5670的端口监听上,所以我们可以用IP+对应的监听端口进行访问:
- UI管理控制台

- haproxy监测界面

5、安装KeepAlived
5.1、安装keepalived
这里采用1主2从,所以在三个节点上都安装
sudo apt-get install keepalived
5.2、修改配置
keepalived的默认配置文件在/etc/keepalived目录下,如果该目录下没有keepalived.conf文件,则手动创建一个文件,并对应修改文件内容
192.168.0.61:master
global_defs {
router_id study-server-1
}
vrrp_instance VI_1 {
state MASTER
interface eth0 # 替换成你自己的网卡名
virtual_router_id 51
priority 120 # 主节点优先级最高,其他节点递减
advert_int 1
authentication {
auth_type PASS
auth_pass 1111
}
virtual_ipaddress {
192.168.0.60/24 dev eth0 # 替换成你的VIP和网卡名
}
}
192.168.0.62:backup
global_defs {
router_id study-server-2
}
vrrp_instance VI_1 {
state BACKUP
interface eth0 # 替换成你自己的网卡名
virtual_router_id 51
priority 100 # 主节点优先级最高,其他节点递减
advert_int 1
authentication {
auth_type PASS
auth_pass 1111
}
virtual_ipaddress {
192.168.0.60/24 dev eth0 # 替换成你的VIP和网卡名
}
}
192.168.0.63:backup
global_defs {
router_id study-server-3
}
vrrp_instance VI_1 {
state BACKUP
interface eth0 # 替换成你自己的网卡名
virtual_router_id 51
priority 80 # 主节点优先级最高,其他节点递减
advert_int 1
authentication {
auth_type PASS
auth_pass 1111
}
virtual_ipaddress {
192.168.0.60/24 dev eth0 # 替换成你的VIP和网卡名
}
}
5.3、启动服务
service keepalived start
查看服务状态:
service keepalived status

可以看到节点1进入MASTER状态

节点2进入了BACKUP状态

节点3也进入了BACKUP状态
前面配置了虚拟IP为192.168.0.60,我们可以尝试用这个IP+15670端口号访问一下控制台,可以看到也是能够正常访问的,而且刷新页面可以看到右上角的Cluster 名称也是会变化的,这就是前面HAProxy配置的轮询负载均衡机制

至此,我们的三节点镜像模式的rabbitmq集群就搭建完成了,后续应用就可以通过VIP 192.168.0.60 + 端口号5670进行访问了。
5.4、补充
这里遗留了一个问题是并没有充分发挥keepalived自动检查的可靠机制。比如:如果我们要监测HAProxy,如果HAProxy服务挂了,先重启, 如果重启不成功,关闭当前Keepalived服务,切换倒backup。
这个时候我们可以创建一个脚本,比如脚本文件名称为check_haproxy.sh,放到了keepalived的目录下/etc/keepalived/check_haproxy.sh,修改文件内容
#!/bin/bash
if [ $(ps -C haproxy --no-header | wc -l) -eq 0 ];then
haproxy -f /etc/haproxy/haproxy.cfg
fi
sleep 2
if [ $(ps -C haproxy --no-header | wc -l) -eq 0 ];then
service keepalived stop
fi
想要脚本生效,还需要修改一下keepalived的配置文件,添加一段vrrp_script内容,并修改vrrp_instance指定track_script 用哪个脚本,如下是节点1的keepalived配置文件修改后的内容,其它两个节点参照修改即可
global_defs {
router_id study-server-1
}
vrrp_script chk_haproxy {
script "/etc/keepalived/check_haproxy.sh"
interval 5
weight 2
}
vrrp_instance VI_1 {
state MASTER
interface eth0 # 替换成你自己的网卡名
virtual_router_id 51
priority 120 # 主节点优先级最高,其他节点递减
advert_int 1
authentication {
auth_type PASS
auth_pass 1111
}
virtual_ipaddress {
192.168.0.60/24 dev eth0 # 替换成你的VIP和网卡名
}
track_script {
chk_haproxy
}
}
vrrp_script脚本功能可以用来做很多事情,比如应用的活性探测,如果检测到应用挂了可以用脚本自动重启等,所以这也是keepalived的核心功能,可以结合业务场景自由发挥
6、高可用验证
简单创建一个springboot项目,并引入rabbitmq的maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
<version>2.7.18</version>
</dependency>
创建一个生产者
package com.study.rabbitmq.ha;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
public class Producer {
public static void main(String[] args) {
// 1、创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
// 2、设置连接属性
factory.setUsername("admin");
factory.setPassword("123abc32110");
factory.setVirtualHost("/");
Connection connection = null;
Channel channel = null;
// 3、可以设置每个节点的链接地址和端口,这里选择直接指定VIP+端口号,最终看到的效果是相同的
Address[] addresses = new Address[]{
new Address("192.168.0.60", 5670)
};
try {
// 开启/关闭连接自动恢复,默认是开启状态
factory.setAutomaticRecoveryEnabled(true);
// 设置每100毫秒尝试恢复一次,默认是5秒:com.rabbitmq.client.ConnectionFactory.DEFAULT_NETWORK_RECOVERY_INTERVAL
factory.setNetworkRecoveryInterval(100);
factory.setTopologyRecoveryEnabled(false);
// 4、使用连接集合里面的地址获取连接
connection = factory.newConnection(addresses, "生产者");
// 添加重连监听器
((Recoverable) connection).addRecoveryListener(new RecoveryListener() {
/**
* 重连成功后的回调
* @param recoverable
*/
public void handleRecovery(Recoverable recoverable) {
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SS").format(new Date()) + " 已重新建立连接!");
}
/**
* 开始重连时的回调
* @param recoverable
*/
public void handleRecoveryStarted(Recoverable recoverable) {
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SS").format(new Date()) + " 开始尝试重连!");
}
});
// 5、从链接中创建通道
channel = connection.createChannel();
/**
* 6、声明(创建)队列
* 如果队列不存在,才会创建
* RabbitMQ 不允许声明两个队列名相同,属性不同的队列,否则会报错
*
* queueDeclare参数说明:
* @param queue 队列名称
* @param durable 队列是否持久化
* @param exclusive 是否排他,即是否为私有的,如果为true,会对当前队列加锁,其它通道不能访问,并且在连接关闭时会自动删除,不受持久化和自动删除的属性控制
* @param autoDelete 是否自动删除,当最后一个消费者断开连接之后是否自动删除
* @param arguments 队列参数,设置队列的有效期、消息最大长度、队列中所有消息的生命周期等等
*/
channel.queueDeclare("queue1", true, false, false, null);
for (int i = 0; i < 100; i++) {
// 消息内容
String message = "Hello World " + i;
try {
// 7、发送消息
channel.basicPublish("", "queue1", null, message.getBytes());
} catch (AlreadyClosedException e) {
// 可能连接已关闭,等待重连
System.out.println("消息 " + message + " 发送失败!");
i--;
TimeUnit.SECONDS.sleep(2);
continue;
}
System.out.println("消息 " + i + " 已发送!");
TimeUnit.SECONDS.sleep(2);
}
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 8、关闭通道
if (channel != null && channel.isOpen()) {
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
// 9、关闭连接
if (connection != null && connection.isOpen()) {
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
运行程序,可以看到生产者成功连接上了rabbitmq,并且开始发送消息了

如果这个时候模拟故障,停掉rabbitmq集群中的某些节点,如果这个时候当前客户端正好是连接到这个节点,那么程序会自动重新发起连接,这里我们不妨把节点2和节点3都停掉
rabbitmqctl stop_app
再观察代码控制台的输出,可以看到程序重连后依旧能够正常的发送消息

7、常用命令
7.1、应用管理
- 查看节点状态
rabbitmqctl status
- 停止运行erlang虚拟机和Rabbit MQ的服务应用
rabbitmqctl stop [pid_file]
如果指定了pid_file,还需要等待指定进程结束。这里的pid_file是通过调用rabbitmq-server命令启动Rabbit MQ的时候创建的,默认情况下放在Mnesia目录中。
注意:如果rabbitmq-server -detach带有这个参数,则不会生成这个pid_file,可以用rabbitmqctl stop_app来停止服务
- 启动RabbitMQ应用
rabbitmqctl start_app
这个命令用于在执行了其它命令操作之后重新启动之前停止的RabbitMQ应用。比如前面我们在加入节点之前会先将这个节点停止
- 重置节点到原始状态
rabbitmqctl reset
该操作包括将当前节点从原来所在集群中删除,从管理数据库中删除所有的配置数据((如已经配置了vhost、用户等),以及删除所有持久化消息
注意:重置节点之前必须先停止rabbitmq服务(rabbitmqctl stop_app)
- 强制重置节点状态
rabbitmqctl force_reset
强制将rabbitmq节点重置还原到最初最原始的状态。此命令不论当前管理的数据库的状态和集群配置是什么,都会无条件重置节点,所以只能用于在数据库或集群配置已经确认损坏的情况下使用,其它情况下慎用
7.2、集群管理
- 节点加入集群
rabbitmqctl [-n nodename] join_cluster {cluster_node} [-ram]
该命令执行之前需要先停止rabbitmq应用并重置节点
-n nodename :要操作的目标节点,
cluster_node:需要加入的集群节点
-ram:集群节点类型,ram/disc,默认是disc
内存节点:所有的元数据都存储在内存中
磁盘节点l:所有的元数据都存储在磁盘中
- 查看集群状态
rabbitmqctl cluster_status
- 修改集群节点的类型
rabbitmqctl change_cluster_node_type {ram|disc}
注意:执行该命令之前需要先停止该节点的rabbitmq应用
- 将节点从集群中删除,允许离线执行
rabbitmqctl forget_cluster_node [--offline]
- 更新集群信息
rabbitmqctl update_cluster_nodes {clustermode}
在集群中的节点应用启动之前先咨询集群节点信息,然后更新集群信息。(这个命令和join_cluster不同,它不加入集群)
- 确保节点可以启动
rabbitmqctl forece_boot
- 设置集群名称
rabbitmqctl set_cluster_name {name}
集群名称在客户端连接的时候会通报给客户端,集群名称默认是集群的第一个节点名称,通过这个命令可以自定义设置

