为了保证线上环境RabbitMQ的高可用性,需要部署一套集群环境。RabbitMQ集群主要有两方面的优势:高可用、高性能;高可用就是保证其中部分RabbitMQ节点崩溃或停机的情况下应用程序不受影响,高性能就是对RabbitMQ的横向扩展,支持更大量的消息通信。
了解 Erlang
RabbitMQ采用Erlang编写,Erlang和Java类似,它也有虚拟机,每个Erlang实例称为Erlang节点(node),但不同的是多个Erlang应用程序可以运行在同一个节点之上,更特别的是Erlang节点之间可以直接进行本地通信(不管这些节点是否在同一台服务器之上)。
Erlang cookie
Erlang 节点通过交换作为令牌的 Erlang cookie来获得认证,由于你一连接到远程节点后,就能执行命令,因此有必要确保该节点是可信的。Erlang将令牌存储在名为 .erlang.cookie 的文件中,该文件一般位于用户的 home 目录下。erlang强制要求
.erlang.cookie文件只能够被所有者读,也就是文件权限为 400。
Erlang 节点
erlang 节点就是 erlang虚拟机实例,启动Erlang节点时可以指定节点名,指定节点名有两个选项,name和sname,name表示长节点名启动,节点完全限定名想 rabbit@hostname.network.tld 这样,如果用短名,就会像这样 rabbit@hostname。RabbitMQ默认启动方式为短节点名。
Erlang端口映射守护进程epmd(Erlang Port Mapper Daemon)。当你启动一个分布式Erlang节点示,它都会检查本地机器上是否运行着epmd (开机是不会启动的),如果没有,节点就会自动启动epmd 。并自动用epmd 进程进行注册, 提供OS 内核分配的地址和端口。当一台机器上的Erlang节点要和远程节点通信时,本地的epmd 就会通过远程机器上的epmd (默认使用TCP/IP,端口为4369,地址使用的是节点名hotname部分)查询远程节点地址(端口),然后就可以通信啦。
RabbitMQ 集群
RabbitMQ 元数据
- 队列元数据:队列名称和它们的属性(是否可持久化,是否自动删除)
- 交换机元数据:交换机名称‘类型和属性(可持久化等)
- 绑定元数据:一张简单展示了如何将消息路由到队列的表格
- vhost 元数据:为vhost 内的队列、交换机和绑定提供命名空间和安全属性
集群中的队列
单节点中,所有关于队列的消息(元数据、状态、内容)都完全存储在该节点上。但是集群中创建的队列,集群只会在单个节点上而不是所有节点上创建完整的队列信息(元数据、状态、内容),只有队列的所有者节点会记录有关队列的所有信息。其他节点只知道队列的元数据和该队列存在的那个节点的指针。所以当节点崩溃时,节点上队列和关联的绑定都消失了,附加在那些队列上的消费者丢失了订阅消息,并且新的消息也会丢失(镜像队列可以解决这个问题)。
内存节点 VS 磁盘节点
内存节点将所有的队列、交换机、绑定、用户、权限和 vhost 的元数据仅存储在内存中(速度快、性能高)。而磁盘节点则将元数据存储在磁盘中(相对内存节点性能较弱)。单节点系统只能时磁盘节点,不然RabbitMQ重启后数据就没了。集群中RabbitMQ要求至少一个磁盘节点,当集群中的所有磁盘节点都处于非正常情况时,集群可以继续路由消息,但你无法更改任何东西(创建队列、交换机、绑定;添加用户;更改权限;添加删除节点)。
搭建集群
我用两台服务器来完成集群搭建,node1:172.18.219.20;node2:172.18.219.21;这两台服务器已经组成了Docker Swarm,我们直接通过docker stack启动两台 RabbitMQ 实例:
rabbitmq-stack.ym内容如下:
version: "3.7"
services:
rabbit1:
image: rabbitmq:3.10.7-management
hostname: rabbit1
networks:
- rabbitmq-network
ports:
- target: 15672
published: 15672
protocol: tcp
mode: host
- target: 5672
published: 5672
protocol: tcp
mode: host
deploy:
mode: replicated
replicas: 1
update_config:
parallelism: 1
delay: 5s
order: stop-first
resources:
limits:
cpus: '2'
memory: 2g
reservations:
cpus: '0.25'
memory: 50M
placement:
constraints:
- "node.hostname==node1"
environment:
TZ : 'Asia/Shanghai'
RABBITMQ_NODENAME: rabbit
volumes:
- /mnt/rabbitmq/rabbit1:/var/lib/rabbitmq
rabbit2:
image: rabbitmq:3.10.7-management
hostname: rabbit2
networks:
- rabbitmq-network
ports:
- target: 15672
published: 15672
protocol: tcp
mode: host
- target: 5672
published: 5672
protocol: tcp
mode: host
deploy:
mode: replicated
replicas: 1
update_config:
parallelism: 1
delay: 5s
order: stop-first
resources:
limits:
cpus: '2'
memory: 2g
reservations:
cpus: '0.25'
memory: 50M
placement:
constraints:
- "node.hostname==node2"
environment:
TZ : 'Asia/Shanghai'
RABBITMQ_NODENAME: rabbit
volumes:
- /mnt/rabbitmq/rabbit2:/var/lib/rabbitmq
networks:
rabbitmq-network:
- 首先通过 deploy.placement.constaints 分别将rabbit1、rabbit2部署到 node1、node2
- 分别指定两个容器的 hostname 为 rabbit1、rabbit2
- 映射端口15672(RabbitMQ Management)、5672(AMQP)
- 通过环境变量 RABBITMQ_NODENAME 设置 RabbitMQ 节点名称
- /var/lib/rabbitmq 为 RabbitMQ home 目录和 data 目录,需要映射到宿主机
- 新版本已经不建议通过环境变量设置 erlang cookie 了,建议在 home 目录下新建 .erlang.cookie 文件,在 每个节点的 .erlang.cookie 写入一致的字符串,注意 .erlang.cookie 文件的权限应该为 400
使用默认用户名 guest, 默认密码:guest 访问 http://xxxx:15672 登录两个 实例 可以看到目前这样部署后实际上只是两个独立的 RabbitMQ 单节点:
后面要做的就是将rabbit2 节点加入到 rabbit1, 这样就完成了集群的搭建。
进入rabbit2 容器,使用 rabbitmqctl 工具进行操作:
# 先停止 RabbitMQ 应用程序
root@rabbit2:/# rabbitmqctl stop_app
Stopping rabbit application on node rabbit@rabbit2 ...
# 重置节点
root@rabbit2:/# rabbitmqctl reset
Resetting node rabbit@rabbit2 ...
# 加入 rabbit1, rabbit@rabbit1 就是erlang 的节点名,在这里会通过docker dns 找到 rabbit1(service name)
# 这一步如果失败,很可能时 erlang cookie 有问题,不一致或者权限只不为 400
root@rabbit2:/# rabbitmqctl join_cluster rabbit@rabbit1
Clustering node rabbit@rabbit2 with rabbit@rabbit1
00:51:49.386 [warning] Feature flags: the previous instance of this node must have failed to write the `feature_flags` file at `/var/lib/rabbitmq/mnesia/rabbit@rabbit2-feature_flags`:
00:51:49.386 [warning] Feature flags: - list of previously disabled feature flags now marked as such: [:maintenance_mode_status]
00:51:49.563 [warning] Feature flags: the previous instance of this node must have failed to write the `feature_flags` file at `/var/lib/rabbitmq/mnesia/rabbit@rabbit2-feature_flags`:
00:51:49.563 [warning] Feature flags: - list of previously enabled feature flags now marked as such: [:maintenance_mode_status]
00:51:49.581 [error] Failed to create a tracked connection table for node :rabbit@rabbit2: {:node_not_running, :rabbit@rabbit2}
00:51:49.581 [error] Failed to create a per-vhost tracked connection table for node :rabbit@rabbit2: {:node_not_running, :rabbit@rabbit2}
00:51:49.581 [error] Failed to create a per-user tracked connection table for node :rabbit@rabbit2: {:node_not_running, :rabbit@rabbit2}
# 启动 rabbit2 节点的 RabbitMQ 应用
root@rabbit2:/# rabbitmqctl start_app
Starting node rabbit@rabbit2 ...
再次查看 RabbitMQ Management:
到此就完成了集群搭建
镜像队列
虽然集群搭建完成了,但是 RabbitMQ 默认情况下队列只会存在单节点是,当某个节点故障时,会导致这个节点上的所有队列都不再可用, 镜像队列就可以保证 RabbitMQ 的高可用,镜像队列可以参考该Blog