主备模式:单活,容量对等,可以实现故障转移。使用独立存储时需要借助复制、镜像同步等技术,数据会有延迟、不一致等问题(CAP定律),使用共享存储时就不会有状态同步这个问题。
主从模式:一定程度的双活,容量对等,最常见的是读写分离。通常也需要借助复制技术,或者要求上游实现双写来保证节点数据一致。
主主模式:两边都可以读写,互为主备。如果两边同时写入很容易冲突,所以通常实现的都是“伪主主模式”,或者说就是主从模式的升级版,只是新增了主从节点的选举和切换。
分片集群:不同节点保存不同的数据,上游应用或者代理节点做路由,突破存储容量限制,分摊读写负载;典型的如MongoDB的分片、MySQL的分库分表、Redis集群。
异地多活:“两地三中心”是金融行业经典的容灾模式(有资源闲置的问题),“异地多活”才是王道。
常用负载均衡算法:
- 随机
- 轮询
- 加权轮询
- 最少活跃连接
- 原地址/目标地址hash(一致性hash)
集群中的经典问题:
脑裂(可以通过协调器选举算法、仲裁节点等方式来解决)
网络分区、一致性、可用性(CAP)
现在太多公司选择直接购买公有云服务,基本不用太关心很多基础设施和中间件的部署、运维细节。但是这些技术以及背后的原理是非常重要的。
RabbitMQ分布式架构模式
主备模式
也叫Warren(兔子窝)模式,同一时刻只有一个节点在工作(备份节点不能读写),当主节点发生故障后会将请求切换到备份节点上(主恢复后成为备份节点)。需要借助HAProxy之类的(VIP模式)负载均衡器来做健康检查和主备切换,底层需要借助共享存储(如SAN设备)。
这不是RabbitMQ官方或者开源社区推荐方案,适用于访问压力不是特别大但是又有高可用架构需求(故障切换)的中小规模的系统来使用。首先有一个节点闲置,本身就是资源浪费,其次共享存储往往需要借助硬件存储,或者分布式文件系统。
Shovel铲子模式
Shovel是一个插件,用于实现跨机房数据复制,或者数据迁移,故障转移与恢复等。
如下图,用户下单的消费先是投递在Goleta Broker实例中,当Goleta实例达到触发条件后(例
如:消息堆积数达到阈值)会将消息放到Goleta实例的backup_orders备份队列中,并通过Shovel插件从Goleta的backup_orders队列中将消息拉取到Carpinteria实例存储。
RabbitMQ集群
RabbitMQ集群允许消费者和生产者在RabbitMQ单个节点崩溃的情况下继续运行,并可以通过添
加更多的节点来线性扩展消息通信的吞吐量。当失去一个RabbitMQ节点时,客户端能够重新连接到集
群中的任何其他节点并继续生产和消费。
RabbitMQ集群中的所有节点都会备份所有的元数据信息,包括:
- 队列元数据:队列的名称及属性;
- 交换器:交换器的名称及属性;
- 绑定关系元数据:交换器与队列或者交换器与交换器之间的绑定关系;
-
vhost元数据:为vhost内的队列、交换器和绑定提供命名空间及安全属性。
基于存储空间和性能的考虑,RabbitMQ集群中的各节点存储的消息是不同的(有点儿类似分片集群,各节点数据并不是全量对等的),各节点之间同步备份的仅仅是上述元数据以及Queue Owner(队列所有者,就是实际创建Queue并保存消息数据的节点)的指针。当集群中某个节点崩溃后,该节点的队列进程和关联的绑定都会消失,关联的消费者也会丢失订阅信息,节点恢复后(前提是消息有持久化)消息可以重新被消费。虽然消息本身也会持久化,但如果节点磁盘存储设备发生故障那同样会导致消息丢失。
总的来说,该集群模式只能保证集群中的某个Node挂掉后应用程序还可以切换到其他Node上继续地发送和消费消息,但并无法保证原有的消息不丢失,所以并不是一个真正意义的高可用集群。
这是RabbitMQ内置的集群模式,Erlang语言天生具备分布式特性,所以不需要借助类似
Zookeeper之类的组件来实现集群(集群节点间使用cookie来进行通信验证,所有节点都必须使用相同的.erlang.cookie 文件内容),不同节点的Erlang、RabbitMQ版本必须一致。
镜像队列模式
前面我们讲了,RabbitMQ内置的集群模式有丢失消息的风险,“镜像队列”可以看成是对内置默认集群模式的一种高可用架构的补充。可以将队列镜像(同步)到集群中的其他broker上,相当于是多副本冗余。如果集群中的一个节点失效,队列能自动地切换到集群中的另一个镜像节点上以保证服务的可用性,而且消息不丢失。
在RabbitMQ镜像队列中所谓的master和slave都仅仅是针对某个queue而言的,而不是node。一个queue第一次创建所在的节点是它的master节点,其他节点为slave节点。如果master由于某种原因失效,最先加入的slave会被提升为新的master。
无论客户端请求到达master还是slave,最终数据都是从master节点获取。当请求到达master节点时,master节点直接将消息返回给client,同时master节点会通过GM(Guaranteed Multicast)协议将queue的最新状态广播到slave节点。GM保证了广播消息的原子性,即要么都更新要么都不更新。当请求到达slave节点时,slave节点需要将请求先重定向到master节点,master节点将消息返回给client,同时master节点会通过GM协议将queue的最新状态广播到slave节点。
集群管理
前面我们讲了几种RabbitMQ分布式/集群架构的模式,下面我们结合Rabbit集群+ 镜像队列,并借助HAProxy 实现负载均衡的集群。
- 在node2、node3、node4三台Linux虚拟机中安装RabbitMQ。
- 从node2拷贝.erlang.cookie到node3、node4的相应目录
如果没有该文件,手动创建/var/lib/rabbitmq/.erlang.cookie ,生成Cookie字符串,或者启
动一次RabbitMQ自动生成该文件。生产中推荐使用第三方工具生成。
我们首先在node2上通过命令systemctl start 1 rabbitmq-server启动单机版RabbitMQ,以生成Cookie文件。
开始准备同步.erlang.cookie 文件。RabbitMQ的集群依赖Erlang的分布式特性,需要保持
Erlang Cookie一致才能实现集群节点的认证和通信,我们直接使用scp命令从node1远程传输。
scp /var/lib/rabbitmq/.erlang.cookie root@node3:/var/lib/rabbitmq/
scp /var/lib/rabbitmq/.erlang.cookie root@node4:/var/lib/rabbitmq/
修改node3和node4上该文件的所有者为rabbitmq:rabbitmq:
chown rabbitmq:rabbitmq /var/lib/rabbitmq/.erlang.cookie
注意.erlang.cookie文件权限为400
启动node3和node4上的RabbitMQ,使用命令:systemctl start rabbitmq-server
将node3和node4这两个节点加入到集群中,分别执行如下命令:
# 停止Erlang VM上运行的RabbitMQ应用,保持Erlang VM的运行
rabbitmqctl stop_app
# 移除当前RabbitMQ虚拟主机中的所有数据:重置
rabbitmqctl reset
# 将当前RabbitMQ的主机加入到rabbit@node2这个虚拟主机的集群中。一个节点也是集群。
rabbitmqctl join_cluster rabbit@node2
# 启动当前Erlang VM上的RabbitMQ应用
rabbitmqctl start_app
- rabbit@node2 表示RabbitMQ节点名称,默认前缀就是rabbit , @ 之后是当前虚拟主机所
在的物理主机hostname 。 - 注意检查下hostname要可以相互ping通
- join_cluster默认是使用disk模式,后面可以加入参数--ram启用内存模式
移出集群节点使用:
# 将虚拟主机(RabbitMQ的节点)rabbit@node3从集群中移除,但是rabbit@node3还保留集群信息
# 还是会尝试加入集群,但是会被拒绝。可以重置rabbit@node3节点。
rabbitmqctl forget_cluster_node rabbit@node3
#修改集群名称(任意节点执行都可以)
rabbitmqctl set_cluster_name
#查看集群状态(任意节点执行都可以)
rabbitmqctl cluster_status
在三个RabbitMQ节点上的任意一个添加用户,设置用户权限,设置用户标签,即可
rabbitmqctl add_user root 123456
rabbitmqctl set_permissions --vhost "/" root ".*" ".*" ".*"
rabbitmqctl set_user_tags --vhost "/" root administrator
可以到web控制台查看集群信息,如果要看到所有RabbitMQ节点上的运行情况,都需要启用
rabbitmq_management 插件。
RabbitMQ镜像集群配置
RabbitMQ中队列的内容是保存在单个节点本地的(声明队列的节点)。跟交换器和绑定不同,它们是对于集群中所有节点的。如此,则队列内容存在单点故障,解决方式之一就是使用镜像队列。在多个节点上拷贝队列的副本。
每个镜像队列包含一个master,若干个镜像。
master存在于称为master的节点上。
所有的操作都是首先对master执行,之后广播到镜像。
这涉及排队发布,向消费者传递消息,跟踪来自消费者的确认等。
镜像意味着集群,不应该WAN使用。
发布到队列的消息会拷贝到该队列所有的镜像。消费者连接到master,当消费者对消息确认之后,
镜像删除master确认的消息。
队列的镜像提供了高可用,但是没有负载均衡。
HTTP API和CLI工具中队列对象的字段原来使用的是slave代表secondaries,现在盖字段的存在仅是为了向后兼容,后续版本会移除。
可以使用策略随时更改队列的类型,可以首先创建一个非镜像队列,然后使用策略将其配置为镜像队列或者反过来。非镜像队列没有额外的基础设施,因此可以提供更高的吞吐率。
master选举策略:
- 最长的运行镜像升级为主镜像,前提是假定它与主镜像完全同步。如果没有与主服务器同步的镜像,则仅存在于主服务器上的消息将丢失。
- 镜像认为所有以前的消费者都已突然断开连接。它重新排队已传递给客户端但正在等待确认的所有消息。这包括客户端已为其发出确认的消息,例如,确认是在到达节点托管队列主节点之前在线路上丢失了,还是在从主节点广播到镜像时丢失了。在这两种情况下,新的主服务器都别无选择,只能重新排队它尚未收到确认的所有消息。
- 队列故障转移时请求通知的消费者将收到取消通知。当镜像队列发生了master的故障转移,
系统就不知道向哪些消费者发送了哪些消息。已经发送的等待确认的消息会重新排队 - 重新排队的结果是,从队列重新使用的客户端必须意识到,他们很可能随后会收到已经收到的消息。
- 当所选镜像成为主镜像时,在此期间发布到镜像队列的消息将不会丢失(除非在提升的节点上发生后续故障)。发布到承载队列镜像的节点的消息将路由到队列主服务器,然后复制到所有镜像。如果主服务器发生故障,则消息将继续发送到镜像,并在完成向主服务器的镜像升级后将其添加到队列中。
-
即使主服务器(或任何镜像)在正在发布的消息与发布者收到的确认之间失败,由客户端使用发布者确认发布的消息仍将得到确认。从发布者的角度来看,发布到镜像队列与发布到非镜像队列没有什么不同。
给队列添加镜像的操作如下(谨慎使用):
接下来,启用镜像队列:
# 对/节点配置镜像队列,使用全局复制
rabbitmqctl set_policy ha-all "^" '{"ha-mode":"all"}'
# 配置过半(N/2 + 1)复制镜像队列
rabbitmqctl set_policy ha-halfmore "queueA" '{"ha-mode":"exactly", "haparams":2}'
# 指定优先级,数字越大,优先级越高
rabbitmqctl set_policy --priority 1 ha-all "^" '{"ha-mode":"all"}'
在任意一个节点上面执行即可。默认是将所有的队列都设置为镜像队列,在消息会在不同节点之间复制,各节点的状态保持一致。
其他的细节可以查看官方文档https://www.rabbitmq.com/ha.html。