【困敦】探索RabbitMQ(一)-搭建高可用集群(HA)

如果这篇文章对您有些用处,请点赞告诉我O(∩_∩)O

本系列分三章探索实际应用中的RabbitMQ:

【困敦】探索RabbitMQ(一)--搭建高可用集群(HA)

【困敦】探索RabbitMQ(二)--实战rabbitmq.client

【困敦】探索RabbitMQ(三)--实战spring.amqp

 

本章需要提前在虚拟机中安装docker,具体参考《VirtualBox安装CentOS及Docker》。

这里分三个阶段介绍如何搭建rabbitmq高可用(HA)集群:

第一阶段-搭建rabbitmq镜像集群

第二阶段-nginx + rabbitmq镜像集群

第三阶段-nginx集群 + keepalived + rabbitmq镜像集群

每个阶段有对应的客户端可用性测试,不需要的同学可以跳过。

一、搭建rabbitmq镜像集群

1、拉取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 的作用是使两个节点间可以相互通信(网络),此时还没有组成集群。

2、配置.erlang.cookie

每个节点/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

3、将节点加入集群

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"}'

4、配置镜像集群

docker exec -it rabbitmq1 bash
rabbitmqctl set_policy ha-all "^" '{"ha-mode":"all"}'

注意:

(1)如果不配置镜像,则为普通集群,每个节点都有元数据,但只有master节点有队列数据,如果访问到slave节点,则从master节点拉数据返回。一旦master节点宕机,选择一个slave节点变为master节点,不阻碍新消息产生和消费,但之前的队列消息会丢失,直至节点恢复。

(2)如果配置镜像集群,与普通集群不同的是,每个节点上都有队列数据,当master节点队列数据发生变化时会主动推送到slave节点,以防止任一节点宕机情况下,已有队列消息丢失,缺点是镜像节点越多,同步消耗越大,性能越低。

5、编写客户端代码

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);
        });
    }
}

6、可用性测试

rabbitmq集群和客户端已准备好,现在我们测试下当一个节点宕时,其他节点能否正常选举出master,客户端能否正常产生并消费消息。

(1)rabbitmq控制台介绍

访问192.168.56.102:15672

【困敦】探索RabbitMQ(一)-搭建高可用集群(HA)_第1张图片

【困敦】探索RabbitMQ(一)-搭建高可用集群(HA)_第2张图片

可以看出:

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(一)-搭建高可用集群(HA)_第3张图片

可以看出:

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按钮即可恢复。

【困敦】探索RabbitMQ(一)-搭建高可用集群(HA)_第4张图片

二、nginx + rabbitmq镜像集群

1、改进思路

集群的最终目标是高可用,就先了解下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镜像集群

【困敦】探索RabbitMQ(一)-搭建高可用集群(HA)_第5张图片

2、拉取nginx镜像并启动容器

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

3、修改nginx配置,代理rabbitmq集群节点

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

4、客户端代码修改

......
    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();
    }
......

5、可用性测试

测试当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

【困敦】探索RabbitMQ(一)-搭建高可用集群(HA)_第6张图片

可以看出消费过程中,先请求到5673失败,然后转发到5674依然失败,最后转发到5672成功。

综上,nginx使得rabbitmq集群具备一定的可用性,并使用独立的ip,使得新增或删除rabbitmq节点时,客户端代码不需要修改。但需要修改nginx服务器配置,并且此时nginx本身同样变成单点,如果rabbitmq-nginx1宕机,那么rabbitmq集群依然不可用。

三、nginx集群 + keepalived + rabbitmq镜像集群

1、改进思路

为了实现集群高可用(HA),只使用一台nginx并不满足,需要搭建nginx集群,但如果是多台nginx同样会有多个地址,客户端如果写死多个nginx地址,又回到老的问题,如何切换有问题的nginx节点,而不修改客户端代码呢?并且rabbitmq集群本身支持故障转移,nginx集群又该如何?

引入双机热备方案:(来自百度百科)

双机热备是应用于服务器的一种解决方案,其构造思想是主机和从机通过TCP/IP网络连接,正常情况下主机处于工作状态,从机处于监视状态,一旦从机发现主机异常,从机将会在很短的时间之内代替主机,完全实现主机的功能。

要实现双机热备,我们需要在每个nginx节点上加入keepalived,如图:

【困敦】探索RabbitMQ(一)-搭建高可用集群(HA)_第7张图片

客户端访问一个固定的虚拟IP(VIP),这个虚拟IP加到nginx集群的哪个节点,客户端不知道,是由keepalived决定,起初虚拟IP加到nginx集群master节点上,同时master上的keepalived会不断向slave发送心跳消息(广播),表明自己还活着,master一旦宕机,slave接管master资源(包括虚拟IP)继续为客户端提供服务。注意,同一时间虚拟IP在同一个网络下只能指向一个服务器。

2、docker宿主机安装ipvsadm

yum install -y ipvsadm
ipvsadm

注意:

如果ipvsadm不启动,keepalived映射虚拟端口虽然启动成功,但无法访问,查看日志发现:

Can't initialize ipvs: Protocol not available

3、rabbitmq-nginx1节点加入keepalived

(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对应的网卡。

【困敦】探索RabbitMQ(一)-搭建高可用集群(HA)_第8张图片

(2)安装ipvsadm,重启keepalived并检查虚拟地址是否映射成功。

apt-get -y install ipvsadm
ipvsadm
service keepalived restart
ip add

【困敦】探索RabbitMQ(一)-搭建高可用集群(HA)_第9张图片

如果虚拟IP没有显示,说明keepalived启动不成功,需要先安装日志组件rsyslog,再查看日志。

apt-get  -y install rsyslog
service rsyslog restart 
tail -f /var/log/messages

4、rabbitmq-nginx2节点加入keepalived

(1)启用rabbitmq-nginx2容器并修改配置

docker run -it -d --name rabbitmq-nginx2  --privileged --net=host nginx
docker ps

【困敦】探索RabbitMQ(一)-搭建高可用集群(HA)_第10张图片

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

【困敦】探索RabbitMQ(一)-搭建高可用集群(HA)_第11张图片

至此集群全部配置完毕,以防万一,建议重启节点。

docker restart rabbitmq-nginx1
docker restart rabbitmq-nginx2

4、修改客户端代码

......
 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)
//        };
    }
......

5、可用性测试

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。

(完)

你可能感兴趣的:(中间件,docker,rabbitmq,java,分布式,macos)