如果这篇文章对您有些用处,请点赞告诉我O(∩_∩)O
本系列分三章探索实际应用中的RabbitMQ:
【困敦】探索RabbitMQ(一)--搭建高可用集群(HA)
【困敦】探索RabbitMQ(二)--实战rabbitmq.client
【困敦】探索RabbitMQ(三)--实战spring.amqp
本章需要提前在虚拟机中安装docker,具体参考《VirtualBox安装CentOS及Docker》。
这里分三个阶段介绍如何搭建rabbitmq高可用(HA)集群:
第一阶段-搭建rabbitmq镜像集群
第二阶段-nginx + rabbitmq镜像集群
第三阶段-nginx集群 + keepalived + rabbitmq镜像集群
每个阶段有对应的客户端可用性测试,不需要的同学可以跳过。
docker pull rabbitmq:3.8.15-management
docker run -d --name rabbitmq1 --hostname rabbitmq_host1 -p 15672:15672 -p 5672:5672 -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=admin rabbitmq:3.8.15-management
docker run -d --name rabbitmq2 --hostname rabbitmq_host2 -p 15673:15672 -p 5673:5672 --link rabbitmq1:rabbitmq_host1 rabbitmq:3.8.15-management
docker run -d --name rabbitmq3 --hostname rabbitmq_host3 -p 15674:15672 -p 5674:5672 --link rabbitmq1:rabbitmq_host1 --link rabbitmq2:rabbitmq_host2 rabbitmq:3.8.15-management
注意:
a、可以在docker仓库中选择其他版本,https://hub.docker.com/_/rabbitmq?tab=tags&page=1&ordering=last_updated
b、--link 的作用是使两个节点间可以相互通信(网络),此时还没有组成集群。
每个节点/var/lib/rabbitmq目录下都有一个隐藏文件.erlang.cookie,其值需要在rabbitmq集群中保持统一。
有两种方式设置:
(1)在docker启动时可以添加参数:-e RABBITMQ_ERLANG_COOKIE='rabbitmq_cookie'。设置简单但后面操作中会出现警告:
RABBITMQ_ERLANG_COOKIE env variable support is deprecated and will be REMOVED in a future version。
(2)启动容器后,在每个容器目录中手动设置。
a、docker宿主机器中,准备一个 .erlang.cookie文件,内容为rabbitmq_cookie,并修改权限为读写,不改权限报错erlang.cookie must be accessible by owner only
chmod 600 .erlang.cookie
b、拷贝到所有容器
docker cp .erlang.cookie rabbitmq1:/var/lib/rabbitmq
docker cp .erlang.cookie rabbitmq2:/var/lib/rabbitmq
docker cp .erlang.cookie rabbitmq3:/var/lib/rabbitmq
c、重启容器
docker restart rabbitmq1
docker restart rabbitmq2
docker restart rabbitmq3
docker exec -it rabbitmq1 bash
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl start_app
exit
docker exec -it rabbitmq2 bash
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl join_cluster --ram rabbit@rabbitmq_host1
rabbitmqctl start_app
exit
docker exec -it rabbitmq3 bash
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl join_cluster --ram rabbit@rabbitmq_host1
rabbitmqctl start_app
exit
docker exec -it rabbitmq1 bash
rabbitmqctl set_policy ha-all "^" '{"ha-mode":"all"}'
docker exec -it rabbitmq1 bash
rabbitmqctl set_policy ha-all "^" '{"ha-mode":"all"}'
注意:
(1)如果不配置镜像,则为普通集群,每个节点都有元数据,但只有master节点有队列数据,如果访问到slave节点,则从master节点拉数据返回。一旦master节点宕机,选择一个slave节点变为master节点,不阻碍新消息产生和消费,但之前的队列消息会丢失,直至节点恢复。
(2)如果配置镜像集群,与普通集群不同的是,每个节点上都有队列数据,当master节点队列数据发生变化时会主动推送到slave节点,以防止任一节点宕机情况下,已有队列消息丢失,缺点是镜像节点越多,同步消耗越大,性能越低。
public class RabbitMQHelper {
public static final String QUEUE_NAME = "queue_test";
public static final String EXCHANGE_NAME = "exchange_test";
private static final RabbitMQHelper INSTANCE = new RabbitMQHelper();
private ConnectionFactory factory;
private Address[] addresses;
private RabbitMQHelper() {
factory = new ConnectionFactory();
factory.setVirtualHost("/");
factory.setUsername("admin");
factory.setPassword("admin");
addresses = new Address[] {
new Address("192.168.56.102", 5672),
new Address("192.168.56.102", 5673),
new Address("192.168.56.102", 5674)
};
}
public static RabbitMQHelper getInstance() {
return INSTANCE;
}
/**
* 创建连接
* @return
*/
public Connection createConnection() throws IOException, TimeoutException {
return factory.newConnection(addresses);
}
/**
* 发送消息
* @param exchageName
* @param queueName
* @param message
*/
public void sendSimpleMessage(String exchageName, String queueName, String message) {
try(Connection conn = createConnection();
Channel channel = conn.createChannel();) {
//声明交换机:交换机名称、类型
channel.exchangeDeclare(exchageName, BuiltinExchangeType.FANOUT); //最常用发广播
//声明队列:队列名称,是否持久化,是否排他,是否自动删除,其他参数Map
channel.queueDeclare(queueName, true, false, false, null);
//绑定交换机和队列,第三个参数为routingKey,FANOUT类型交换机无需routingKey
channel.queueBind(queueName, exchageName, "");
//发布消息,第二个参数routingKey,FANOUT类型交换机无需路由键
channel.basicPublish(exchageName, "", null, message.getBytes("UTF-8"));
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 消费消息
* @param queueName
*/
public void consumeSimpleMessage(String queueName, SimpleConsumerCallback callback) {
try(Connection conn = createConnection();
Channel channel = conn.createChannel();) {
//消费消息:队列名称,是否自动应答autoAck,消息回调接口
channel.basicConsume(queueName, true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
callback.delivery(new String(body, "UTF-8"));
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 回调接口
*/
public interface SimpleConsumerCallback {
void delivery(String message);
}
}
public class Producer {
public static void main(String[] args) {
RabbitMQHelper rabbitMQHelper = RabbitMQHelper.getInstance();
rabbitMQHelper.sendSimpleMessage(RabbitMQHelper.EXCHANGE_NAME, RabbitMQHelper.QUEUE_NAME, "欢迎来到RabbitMQ!");
System.out.println("消息发送成功");
}
}
public class Consumer {
public static void main(String[] args) {
RabbitMQHelper rabbitMQHelper = RabbitMQHelper.getInstance();
rabbitMQHelper.consumeSimpleMessage(RabbitMQHelper.QUEUE_NAME, (String message) -> {
System.out.println("收到消息:" + message);
});
}
}
rabbitmq集群和客户端已准备好,现在我们测试下当一个节点宕时,其他节点能否正常选举出master,客户端能否正常产生并消费消息。
(1)rabbitmq控制台介绍
访问192.168.56.102:15672
可以看出:
a、集群有三个节点组成,rabbitmq_host1,rabbitmq_host2,rabbitmq_host3。
b、其中rabbitmq_host1为master节点,+2表示有两个slave节点
c、ha-all是我们配置的镜像策略
(2)测试步骤:
a、rabbitmq1,rabbitmq2,rabbitmq3同时存活情况下,客户端发送消息并消费,客户端输出成功。(rabbitmq1为master)
b、停掉master节点,客户端发送消息,访问mq控制台192.168.56.102:15673(15672已经关闭,无法访问)
docker exec -it rabbitmq1 bash
rabbitmqctl stop_app
exit
可以看出:
rabbitmq_host1 节点已停止运行。master节点切换为rabbitmq_host3,+1表示此时只有1个slave节点。并且Ready为1表示消息没有丢失。
c、客户端消费消息,客户端输出成功。(rabbitmq3为master)
d、恢复节点,集群恢复
docker exec -it rabbitmq1 bash
rabbitmqctl start_app
exit
(3)补充
当消息队列中还有消息为消费时,加入新节点或恢复节点,需要手动同步队列数据到新节点。
红色的+1说明有一个节点有问题,点击队列名称查看细节,发现刚加入的节点未同步,点击Synchronize按钮即可恢复。
集群的最终目标是高可用,就先了解下HA的概念(来自百度百科)
“高可用性”(High Availability)通常来描述一个系统经过专门的设计,从而减少停工时间,而保持其服务的高度可用性。
简单理解,在服务器端出错时(非代码原因),客户端依然正常工作,不需要“停工”。对于mq集群,首先要考虑的是如果我们需要替换下有问题的mq节点时,不需要修改客户端代码。因此我们客户端代码就不能“写死”mq地址,如:
addresses = new Address[] {
new Address("192.168.56.102", 5672), //rabbitmq节点地址写死在代码中
new Address("192.168.56.102", 5673),
new Address("192.168.56.102", 5674)
};
我们可以用一台Nginx反向代理已经搭建的RabbitMQ镜像集群
docker pull nginx:latest
docker run -it -d --name rabbitmq-nginx1 --privileged --net=host nginx
docker ps
注意ps看不到容器,表示启动有问题,需查看docker日志
docker logs -f --tail 100 rabbitmq-nginx1
docker exec -it rabbitmq-nginx1 bash
vi /etc/nginx/nginx.conf
修改部分如下:
#include /etc/nginx/conf.d/*.conf;
}
stream{
upstream rabbitmq-cluster {
server 192.168.56.102:5672;
server 192.168.56.102:5673;
server 192.168.56.102:5674;
}
server {
listen 6001;
proxy_pass rabbitmq-cluster;
}
}
注意:
第一行需要注释,当同一台机器启动多个nginx容器时,默认的80端口会冲突。
exit
docker restart rabbitmq-nginx1
......
private RabbitMQHelper() {
factory = new ConnectionFactory();
factory.setVirtualHost("/");
factory.setUsername("admin");
factory.setPassword("admin");
factory.setHost("192.168.56.102"); //使用nginx代理服务器IP
factory.setPort(6001); //使用nginx代理服务器端口
// addresses = new Address[] {
// new Address("192.168.56.102", 5672),
// new Address("192.168.56.102", 5673),
// new Address("192.168.56.102", 5674)
// };
}
......
/**
* 创建连接
* @return
*/
public Connection createConnection() throws IOException, TimeoutException {
// return factory.newConnection(addresses);
return factory.newConnection();
}
......
测试当rabbitmq集群节点宕机时,nginx的可用性。
a、手动停止集群rabbitmq2,rabbitmq3节点,只保留rabbitmq1。
docker exec -it rabbitmq2 bash
rabbitmqctl stop_app
exit
docker exec -it rabbitmq3 bash
rabbitmqctl stop_app
exit
b、调用客户端成功产生并消费消息,控制台成功输出。
c、观察日志
docker logs -f --tail 100 rabbitmq-nginx1
可以看出消费过程中,先请求到5673失败,然后转发到5674依然失败,最后转发到5672成功。
综上,nginx使得rabbitmq集群具备一定的可用性,并使用独立的ip,使得新增或删除rabbitmq节点时,客户端代码不需要修改。但需要修改nginx服务器配置,并且此时nginx本身同样变成单点,如果rabbitmq-nginx1宕机,那么rabbitmq集群依然不可用。
为了实现集群高可用(HA),只使用一台nginx并不满足,需要搭建nginx集群,但如果是多台nginx同样会有多个地址,客户端如果写死多个nginx地址,又回到老的问题,如何切换有问题的nginx节点,而不修改客户端代码呢?并且rabbitmq集群本身支持故障转移,nginx集群又该如何?
引入双机热备方案:(来自百度百科)
双机热备是应用于服务器的一种解决方案,其构造思想是主机和从机通过TCP/IP网络连接,正常情况下主机处于工作状态,从机处于监视状态,一旦从机发现主机异常,从机将会在很短的时间之内代替主机,完全实现主机的功能。
要实现双机热备,我们需要在每个nginx节点上加入keepalived,如图:
客户端访问一个固定的虚拟IP(VIP),这个虚拟IP加到nginx集群的哪个节点,客户端不知道,是由keepalived决定,起初虚拟IP加到nginx集群master节点上,同时master上的keepalived会不断向slave发送心跳消息(广播),表明自己还活着,master一旦宕机,slave接管master资源(包括虚拟IP)继续为客户端提供服务。注意,同一时间虚拟IP在同一个网络下只能指向一个服务器。
yum install -y ipvsadm
ipvsadm
注意:
如果ipvsadm不启动,keepalived映射虚拟端口虽然启动成功,但无法访问,查看日志发现:
Can't initialize ipvs: Protocol not available
(1)rabbitmq-nginx1在第二部分已启用,此时进入rabbitmq-nginx1,安装并配置keepalived。
docker exec -it rabbitmq-nginx1 bash
apt-get -y update
apt-get -y install keepalived
vi /etc/keepalived/keepalived.conf
vrrp_instance VI_1 {
state MASTER
interface enp0s8
virtual_router_id 51
priority 150
advert_int 1
authentication {
auth_type PASS
auth_pass 123456
}
virtual_ipaddress {
192.168.56.103/24
}
}
virtual_server 192.168.56.103 6003 {
delay_loop 3
lb_algo rr
lb_kind NAT
persistence_timeout 50
protocol TCP
real_server 192.168.56.102 6002 {
weight 1
}
}
注意:
virtual_router_id 是路由id,保证集群内统一
authentication 是主从服务器认证方式,保证集群内统一,否则无法完成主从切换
virtual_ipaddress,virtual_server 是虚拟地址
interface 是对应本机网卡,使用ip add查看,本机IP对应的网卡。
(2)安装ipvsadm,重启keepalived并检查虚拟地址是否映射成功。
apt-get -y install ipvsadm
ipvsadm
service keepalived restart
ip add
如果虚拟IP没有显示,说明keepalived启动不成功,需要先安装日志组件rsyslog,再查看日志。
apt-get -y install rsyslog
service rsyslog restart
tail -f /var/log/messages
(1)启用rabbitmq-nginx2容器并修改配置
docker run -it -d --name rabbitmq-nginx2 --privileged --net=host nginx
docker ps
docker exec -it rabbitmq-nginx2 bash
vi /etc/nginx/nginx.conf
修改部分如下:
#include /etc/nginx/conf.d/*.conf;
}
stream{
upstream rabbitmq-cluster {
server 192.168.56.102:5672;
server 192.168.56.102:5673;
server 192.168.56.102:5674;
}
server {
listen 6002;
proxy_pass rabbitmq-cluster;
}
}
注意:
第一行需要注释,当同一台机器启动多个nginx容器时,默认的80端口会冲突。
(2)进入rabbitmq-nginx2,安装并配置keepalived
docker exec -it rabbitmq-nginx2 bash
apt-get -y update
apt-get -y install keepalived
vi /etc/keepalived/keepalived.conf
vrrp_instance VI_1 {
state MASTER
interface enp0s8
interface enp0s8
virtual_router_id 51
priority 150
advert_int 1
authentication {
auth_type PASS
auth_pass 123456
}
virtual_ipaddress {
192.168.56.103/24
}
}
virtual_server 192.168.56.103 6003 {
delay_loop 3
lb_algo rr
lb_kind NAT
persistence_timeout 50
protocol TCP
real_server 192.168.56.102 6002 {
weight 1
}
}
(3)安装ipvsadm,重启keepalived并检查虚拟地址是否映射成功。
apt-get -y install ipvsadm
ipvsadm
service keepalived restart
ip add
至此集群全部配置完毕,以防万一,建议重启节点。
docker restart rabbitmq-nginx1
docker restart rabbitmq-nginx2
......
private RabbitMQHelper() {
factory = new ConnectionFactory();
factory.setVirtualHost("/");
factory.setUsername("admin");
factory.setPassword("admin");
factory.setHost("192.168.56.103"); //使用虚拟IP
factory.setPort(6003); //使用虚拟端口
// addresses = new Address[] {
// new Address("192.168.56.102", 5672),
// new Address("192.168.56.102", 5673),
// new Address("192.168.56.102", 5674)
// };
}
......
a、保证rabbitmq-nginx1,rabbitmq-nginx2同时启动时,调用客户端访问虚拟地址发送并消费消息。(经过rabbitmq-nginx1)
b、停止rabbitmq-nginx1,调用客户端发送并消费消息,控制台输出成功。(经过rabbitmq-nginx2)
docker stop rabbitmq-nginx1
c、停止rabbitmq-nginx2,调用客户端发送并消费消息,控制台输出异常。(此时已没有nginx节点,当然失败)
docker stop rabbitmq-nginx2
d、启动rabbitmq-nginx1,调用客户端发送并消费消息,控制台输出成功。(经过rabbitmq-nginx1)
docker start rabbitmq-nginx1
综上,说明nginx集群已有故障转移的能力,具备高可用性(HA)。
写本篇文章的目的在于,学习搭建RabbitMQ高可用集群思路,实战主要的搭建方式。实际生产环境更复杂,需要考虑的问题会更多,这里省略。如:
第三部分最后测试nginx集群可用性时,我们直接停掉节点,虚拟IP可以切换,但如果节点没有宕机,只是节点上的nginx崩溃,keepalived没有崩溃。此时虚拟IP切换失败,因为slave节点的keepalived觉得master节点的keepalived没有宕机。因此,在keepalived.conf中需要增加配置,不停检查nginx是否崩溃,如果是,则调用脚本关闭keepalived。
(完)