相关文章推荐
腹黑的槟榔  ·  fofa搜索致远oa - OSCHINA ...·  11 月前    · 
机灵的盒饭  ·  SQL ...·  1 年前    · 

在前几篇文章中,我们详细的介绍了 RabbitMQ 的内部结构和使用,以及 SpringBoot 和 RabbitMQ 整合,都是基于单台 RabbitMQ 进行使用的。

我们知道在微服务流行的当下,一旦单台服务器挂了,基本上就无法提供高可用的服务了,因此为了保证服务高可用,在生产环境上我们通常的做法是搭建一个 RabbitMQ 集群,即使某台 RabbitMQ 故障了,其他正常的 RabbitMQ 服务器依然可以使用,应用程序的持续运行不会受到影响。

二、集群架构原理

在前几篇文章中,我们有介绍到 RabbitMQ 内部有各种基础构件,包括队列、交换器、绑定、虚拟主机等,他们组成了 AMQP 协议消息通信的基础,而这些构件以 元数据 的形式存在,它始终记录在 RabbitMQ 内部,它们分别是:

队列元数据 :队列名称和它们的属性 交换器元数据 :交换器名称、类型和属性 绑定元数据 :一张简单的表格展示了如何将消息路由到队列 vhost 元数据 :为 vhost 内的队列、交换器和绑定提供命名空间和安全属性

这些元数据,其实本质是一张查询表,里面包括了交换器名称和一个队列的绑定列表,当你将消息发布到交换器中,实际上是将你所在的信道将消息上的路由键与交换器的绑定列表进行匹配,然后将消息路由出去。

消息路由表

有了这个机制,那么在所有节点上传递交换器消息将简单很多,而 RabbitMQ 所做的事情就是把交换器元数据拷贝到所有节点上,因此每个节点上的每条信道都可以访问完整的交换器。

如果消息生产者所连接的是节点 2 或者节点 3,此时队列1的完整数据不在该两个节点上,那么在发送消息过程中这两个节点主要起了一个路由转发作用,根据这两个节点上的元数据转发至节点1上,最终发送的消息还是会存储至节点1的队列1上。

同样,如果消息消费者所连接的节点2或者节点3,那这两个节点也会作为路由节点起到转发作用,将会从节点1的队列1中拉取消息进行消费。

与常见的集群主从架构模式不同的地方在于 RabbitMQ 集群模式下,仅仅只是同步元数据,每个队列内容还是在自己的服务器节点上

这么设计主要还是基于集群本身的 性能和存储空间 上来考虑:

存储空间 :真正存放数据的地方是在队列里面,如果每个集群节点都拥有所有队列的完全数据拷贝,那么每个节点的存储空间会非常大,集群的消息积压能力会非常弱。例如你现在存储了 3G 队列内容,那么在另外一个只有 1G 存储空间的节点上,就会造成内存空间不足的情况,也就是无法通过集群节点的扩容提高消息积压能力。 性能 :消息的发布者需要将消息复制到每一个集群节点,每一条消息都会触发磁盘活动,这会导致整个集群内性能负载急剧拉升。

既然每个队列内容还是在自己的服务器节点上,同样也会带来新的问题, 那就是如果队列所在服务器挂了,那存在服务器上的队列数据是不是全部都丢失了

在单个节点上,RabbitMQ 存储数据有两种方案:

内存模式 :这种模式会将数据存储在内存当中,如果服务器突然宕机重启之后,那么附加在该节点上的队列和其关联的绑定都会丢失,并且消费者可以重新连接集群并重新创建队列; 磁盘模式 :这种模式会将数据存储磁盘当中,如果服务器突然宕机重启,数据会自动恢复,该队列又可以进行传输数据了,并且在恢复故障磁盘节点之前,不能在其它节点上让消费者重新连到集群并重新创建队列,如果消费者继续在其它节点上声明该队列,会得到一个 404 NOT_FOUND 错误,这样确保了当故障节点恢复后加入集群,该节点上的队列消息不会丢失,也避免了队列会在一个节点以上出现冗余的问题。

在集群中的每个节点,要么是内存节点,要么是磁盘节点,如果是内存节点,会将所有的元数据信息仅存储到内存中, 而磁盘节点则不仅会将所有元数据存储到内存上, 还会将其持久化到磁盘

在单节点 RabbitMQ 上,仅允许该节点是磁盘节点,这样确保了节点发生故障或重启节点之后,所有关于系统的配置与元数据信息都会从磁盘上恢复。

而在 RabbitMQ 集群上,至少有一个磁盘节点,也就是在集群环境中需要添加 2 台及以上的磁盘节点,这样其中一台发生故障了,集群仍然可以保持运行。其它节点均设置为内存节点,这样会让队列和交换器声明之类的操作会更加快速,元数据同步也会更加高效。

三、集群部署

为了和生产环境保持一致,我们选用 CentOS7 操作系统进行环境部署,分别创建 3 台虚拟机。

# 3台服务器的IP
197.168.24.206
197.168.24.233
197.168.24.234

放开防火墙限制,保证 3 台服务器网络都可以互通!

3.1、重新设置主机名

由于 RabbitMQ 集群连接是通过主机名来连接服务的,必须保证各个主机名之间可以 ping 通,重新设置 3 台服务器主机名,所以需要做以下操作:

# 修改节点1的主机名
hostname node1

# 修改节点2的主机名
hostname node2

# 修改节点3的主机名
hostname node3

编辑 /etc/hosts 文件,添加到在三台机器的 /etc/hosts 中以下内容:

sudo vim /etc/hosts

添加内容如下:

197.168.24.206 node1
197.168.24.233 node2
197.168.24.234 node3

3.2、rabbitMQ安装

RabbitMQ 基于 erlang 进行通信,相比其它的软件,安装有些麻烦,不过本例采用 rpm 方式安装,任何新手都可以完成安装,过程如下!

3.2.1、安装前命令准备

输入如下命令,完成安装前的环境准备。

yum install lsof  build-essential openssl openssl-devel unixODBC unixODBC-devel make gcc gcc-c++ kernel-devel m4 ncurses-devel tk tc xz wget vim
3.2.2、下载 RabbitMQ、erlang、socat 的安装包

本次下载的是 RabbitMQ-3.6.5 版本,采用 rpm 一键安装,适合新手直接上手。

先创建一个 rabbitmq 目录,本例的目录路径为 /usr/app/rabbitmq ,然后在目录下执行如下命令,下载安装包!

下载erlang
wget www.rabbitmq.com/releases/erlang/erlang-18.3-1.el7.centos.x86_64.rpm
下载socat
wget http://repo.iotti.biz/CentOS/7/x86_64/socat-1.7.3.2-5.el7.lux.x86_64.rpm
下载rabbitMQ
wget www.rabbitmq.com/releases/rabbitmq-server/v3.6.5/rabbitmq-server-3.6.5-1.noarch.rpm

最终目录文件如下:

rpm -ivh rabbitmq-server-3.6.5-1.noarch.rpm

安装完成之后,修改 rabbitmq 的配置,默认配置文件在 /usr/lib/rabbitmq/lib/rabbitmq_server-3.6.5/ebin 目录下。

vim /usr/lib/rabbitmq/lib/rabbitmq_server-3.6.5/ebin/rabbit.app

修改 loopback_users 节点的值!

分别重新命令 rabbit 节点名称

vim /etc/rabbitmq/rabbitmq-env.conf

在文件里添加一行,如下配置!

NODENAME=rabbit@node1

其它两个节点命令也类似,然后,再保存!通过如下命令,启动服务即可!

# 启动服务
rabbitmq-server start &

# 停止服务
rabbitmqctl stop

通过如下命令,查询服务是否启动成功!

lsof -i:5672

如果出现 5672 已经被监听,说明已经启动成功!

3.3、复制 Erlang cookie

RabbitMQ 集群环境下,元数据同步基于 cookie 共享方案实现。

在这里将 node1 的 cookie 文件复制到 node2,由于这个文件权限是 400 为方便传输,先修改权限,非必须操作,所以需要先修改 node1 中的该文件权限为 777

chmod 777 /var/lib/rabbitmq/.erlang.cookie

用 scp 拷贝到节点 2,节点 3 的操作也类似。

scp /var/lib/rabbitmq/.erlang.cookie node2:/var/lib/rabbitmq/

最后,将权限改回来

chmod 400 /var/lib/rabbitmq/.erlang.cookie

3.4、组成集群

在节点 2 执行如下命令:

# 停止rabbitmq服务
rabbitmqctl stop_app

# 清空节点状态
rabbitmqctl reset

# node2和node1构成集群,node2必须能通过node1的主机名ping通
rabbitmqctl join_cluster rabbit@node1

# 开启rabbitmq服务
rabbitmqctl start_app

节点 3 的操作也类似!

在任意一台机上面查看集群状态:

rabbitmqctl cluster_status

如果你想将某个节点移除集群,以移除节点3为例,可以按照如下方式进行操作!

# 首先停止要移除的节点服务
rabbitmqctl stop

# 移除节点3
rabbitmqctl -n rabbit@node1 forget_cluster_node rabbit@node3

如果移除之后,无法启动 rabbitMQ,删除已有 mnesia 信息!

rm -rf /var/lib/rabbitmq/mnesia

然后再次重启服务即可!

3.5、设置内存节点

#加入时候设置节点为内存节点(默认加入的为磁盘节点)
rabbitmqctl join_cluster rabbit@node1 --ram

其中 --ram 指的是作为内存节点,如果不加,那就默认为磁盘节点。

如果节点在集群中已经是磁盘节点了,通过以下命令可以将节点改成内存节点:

# 停止rabbitmq服务
rabbitmqctl stop_app

# 更改节点为内存节点
rabbitmqctl change_cluster_node_type ram

# 开启rabbitmq服务
rabbitmqctl start_app

3.6、镜像队列

上面我们提到,在默认情况下,队列只会保存在其中一个节点上,当节点发生故障时,尽管所有元数据信息都可以从磁盘节点上将元数据恢复到本节点上, 但是内存节点的队列消息内容就不行了 ,这样就会导致消息的丢失。

RabbitMQ 很早就意识到这个问题,在 2.6 以后的版本中增加了队列冗余选项: 镜像队列

所谓镜像队列,其实就是主队列(master)依然是仅存在于一个节点上,通过关联的 rabbitMQ 服务器,从主队列同步消息到各个节点,也就是所谓的主从模式,将主队列的消息进行备份处理。

如果主队列没有发生故障,那么其工作流程跟普通队列一样,生产者和消费者不会感知其变化,当发布消息时,依然是路由到主队列中,而主队列通过类似广播的机制,将消息扩散同步至其余从队列中,这就有点像 fanout 交换器一样。而消费者依然是从主队列中读取消息。

一旦主队列发生故障,集群就会从最老的一个从队列选举为新的主队列,这也就实现了队列的高可用了,但我们切记不要滥用这个机制,在上面也说了,队列的冗余操作会导致不能通过扩展节点增加存储空间,而且会造成性能瓶颈。

命令格式如下:

rabbitmqctl set_policy [-p Vhost] Name Pattern Definition [Priority]

参数介绍:

-p Vhost: 可选参数,针对指定vhost下的queue进行设置
Name: policy的名称
Pattern: queue的匹配模式(正则表达式)
Definition: 镜像定义,包括三个部分ha-mode, ha-params, ha-sync-mode
    ha-mode: 指明镜像队列的模式,有效值为 all/exactly/nodes
        all: 表示在集群中所有的节点上进行镜像
        exactly: 表示在指定个数的节点上进行镜像,节点的个数由ha-params指定
        nodes: 表示在指定的节点上进行镜像,节点名称通过ha-params指定
    ha-params: ha-mode模式需要用到的参数
    ha-sync-mode: 进行队列中消息的同步方式,有效值为automatic和manual
priority: 可选参数,policy的优先级

举个例子,声明名为 ha-all 的策略,它与名称以 ha 开头的队列相匹配,并将镜像配置到集群中的所有节点:

rabbitmqctl set_policy ha-all "^" '{"ha-mode":"all"}'

类似操作很多,具体使用可以参考官方 api。

四、集群的负载均衡

HAProxy 提供高可用性、负载均衡以及基于TCP和HTTP应用的代理,支持虚拟主机,它是免费、快速并且可靠的一种解决方案。根据官方数据,其最高极限支持10G的并发。HAProxy支持从4层至7层的网络交换,即覆盖所有的 TCP 协议。就是说,Haproxy 甚至还支持 Mysql 的均衡负载。为了实现 RabbitMQ 集群的软负载均衡,这里可以选择HAProxy。

vim /etc/haproxy/haproxy.cfg

我们只需要在文件末尾加上如下配置即可!

#绑定配置
listen rabbitmq_cluster
        bind 0.0.0.0:5672
        #配置TCP模式
        mode tcp
        #加权轮询
        balance roundrobin
        #RabbitMQ集群节点配置
        server rmq_node1 197.168.24.206:5672 check inter 5000 rise 2 fall 3 weight 1
        server rmq_node2 197.168.24.233:5672 check inter 5000 rise 2 fall 3 weight 1
        server rmq_node3 197.168.24.234:5672 check inter 5000 rise 2 fall 3 weight 1

#haproxy监控页面地址
listen monitor
        bind 0.0.0.0:8100
        mode http
        option httplog
        stats enable
        stats uri /stats
        stats refresh 5s

绑定配置参数说明:

bind :这里定义了客户端连接连接 IP 地址和端口号,用于客户端连接 balance roundrobin :表示加权轮询负载均衡算法

RabbitMQ 集群节点配置说明:

server rmq_node1 :定义HAProxy内RabbitMQ服务的标识 197.168.24.206:5672 :标识了后端RabbitMQ的服务地址 check inter 5000 :表示每隔多少毫秒检查RabbitMQ服务是否可用,示例参数值为 5000 rise 2 :表示 RabbitMQ 服务在发生故障之后,需要多少次健康检查才能被再次确认可用,示例参数值为 2 fall 2 :表示需要经历多少次失败的健康检查之后,HAProxy 才会停止使用此RabbitMQ服务,示例参数值为 2 weight 1 :表示权重比例,值越低,会优先进行数据分配,示例参数值为 1

启动 HAProxy:

/usr/sbin/haproxy -f /etc/haproxy/haproxy.cfg

登录 http://ip:8100/stats web 管理界面,即可进行监控查看!

五、Java 客户端使用

如果是配置了 HAProxy 代理服务器,可以直接使用 HAProxy 代理服务器地址即可!

 //ConnectionFactory创建MQ的物理连接
connectionFactory = new ConnectionFactory();
connectionFactory.setHost("197.168.24.207");  //代理服务器地址
connectionFactory.setPort(5672);          //代理服务器端口
connectionFactory.setUsername("admin");  //guest只能在本机进行访问,通过代理服务器发送消息时需要重新建立用户
connectionFactory.setPassword("admin");  //guest
connectionFactory.setVirtualHost("/");    //虚拟主机

如果没有代理服务器,使用 Spring CachingConnectionFactory 类进行配置。

SpringBoot 项目为例,配置文件如下:

spring.rabbitmq.addresses=197.168.24.206:5672,197.168.24.233:5672,197.168.24.234:5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.virtual-host=/

RabbitConfig 配置类如下:

@Configuration
public class RabbitConfig {

    /**
     * 初始化连接工厂
     * @param addresses
     * @param userName
     * @param password
     * @param vhost
     * @return
     */

    @Bean
    ConnectionFactory connectionFactory(@Value("${spring.rabbitmq.addresses}") String addresses,
                                        @Value("${spring.rabbitmq.username}") String userName,
                                        @Value("${spring.rabbitmq.password}") String password,
                                        @Value("${spring.rabbitmq.virtual-host}") String vhost) 
{
        CachingConnectionFactory connectionFactory = new CachingConnectionFactory();
        connectionFactory.setAddresses(addresses);
        connectionFactory.setUsername(userName);
        connectionFactory.setPassword(password);
        connectionFactory.setVirtualHost(vhost);
        return connectionFactory;
    }

    /**
     * 重新实例化 RabbitAdmin 操作类
     * @param connectionFactory
     * @return
     */

    @Bean
    public RabbitAdmin rabbitAdmin(ConnectionFactory connectionFactory){
        return new RabbitAdmin(connectionFactory);
    }

    /**
     * 重新实例化 RabbitTemplate 操作类
     * @param connectionFactory
     * @return
     */

    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory){
        RabbitTemplate rabbitTemplate=new RabbitTemplate(connectionFactory);
        //数据转换为json存入消息队列
        rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
        return rabbitTemplate;
    }

}

本文主要详细介绍了 RabbitMQ 集群的工作原理和如何搭建一个具备负载均衡能力的 RabbitMQ 集群的方法。

限于笔者的才疏学浅,对本文内容可能还有理解不到位的地方,如有阐述不合理之处还望留言一起探讨。

1、简书 - 癫狂侠 - 消息中间件—RabbitMQ

2、后端进阶 - zch - RabbitMQ集群原理与部署

< E N D >

【推荐阅读】