一、基本原理

1. 工作原理

  搭建 RabbitMQ 集群以后,尽管交换器和绑定关系能够在单点故障问题上幸免于难,但是队列及其存储的消息却不行,这是因为队列进程及其内容仅仅维持在单个节点之上,所以一个节点的失效表现为其对应的队列不可用。

如果集群中的一个节点失效了,队列能自动地切换到镜像中的另一个节点上以保证服务的可用性。在通常的用法中,针对每一个配置镜像的队列都包含一个主节点(master)和若干个从节点(slave):

rabbitmq docker 镜像 rabbitmq镜像集群原理_客户端

  slave 会准确地按照 master 执行命令的顺序进行动作,故 slave 与 master 上维护的状态应该是相同的。如果 master 由于某种原因失效,那么“资历最老”的 slave 会被提升为新的 master 。根据 slave 加入的时间排序,时间最长的 slave 即为“资历最老”。发送到镜像队列的所有消息会被同时发往 master 和所有的 slave 上,如果此时 master 挂掉了,消息还会在 slave 上,这样 slave 提升为 master 的时候消息也不会丢失。除发送消息(Basic.Publish)外的所有动作都只会向 master 发送,然后再由 master 将命令执行的结果广播给各个 slave。

  如果消费者与 slave 建立连接并进行订阅消费,其实质上都是从 master 上获取消息,只不过看似是从 slave 上消费而已。比如消费者与 slave 建立了 TCP 连接之后执行一个 Basic.Get 的操作,那么首先是由 slave 将 Basic.Get 请求发往 master,再由 master 准备好数据返回给 slave,最后由 slave 投递给消费者。

2. 负载均衡

  镜像队列接受请求时,除发送消息外的所有动作都只会向 master 发送,然后再由 master 将命令执行的结果广播给各个 slave,大多的读写压力都落到了master上,那么这样是否负载会做不到有效的均衡?

  注意这里的 master 和 slave 是针对队列而言的,而队列可以均匀地散落在集群的各个 Broker 节点中以达到负载均衡的目的,因为真正的负载还是针对实际的物理机器而言的,而不是内存中驻留的队列进程。所以,只要确保镜像队列的 master 节点均匀散落在集群中的各个 Broker 节点即可确保很大程度上的负载均衡(每个队列的流量会有不同,因此均匀散落各个队列的 master 也无法确保绝对的负载均衡)。例如:

rabbitmq docker 镜像 rabbitmq镜像集群原理_操作命令_02

3. 事务与确认机制

  RabbitMQ 的镜像队列同时支持 publisher confirm 和事务两种机制。在事务机制中,只有当前事务在全部镜像中执行之后,客户端才会收到 Tx.Commit-Ok 的消息。同样的,在 publisher confirm 机制中,生产者进行当前消息确认的前提是该消息被全部镜像队列所接收了。

 

二、实现原理

  不同于普通的非镜像队列,镜像队列的 backing_queue 比较特殊,其实现并非是 rabbit_variable_queue,它内部包裹了普通 backing_queue 进行本地消息消息持久化处理,在此基础上增加了将消息和 ack 复制到所有镜像的功能。如下图:

rabbitmq docker 镜像 rabbitmq镜像集群原理_rabbitmq docker 镜像_03

  其中,master 的 backing_queue 采用的是 rabbit_mirror_queue_master,而 slave 的 backing_queue 实现是 rabbit_mirror_queue_slave。

  所有对 rabbit_mirror_queue_master 的操作都会通过组播GM(Guaranteed Multicast)的方式同步到各个 slave 中。GM 负责消息的广播,rabbit_mirror_queue_slave 负责回调处理,而 master 上的回调处理是由 coordinator 负责完成的。如前所述,除了 Basic.Publish,所有的操作都是通过 master 来完成的,master 对消息进行处理的同时将消息的处理结果通过 GM 广播给所有的 slave, slave 的 GM 收到消息后,通过回调交由 rabbit_mirror_queue_slave 进行实际的处理。

  GM 模块实现的是一种可靠的组播通信协议,该协议能够保证组播消息的原子性,即保证组中活着的节点要么都收到消息要么都收不到,它的实现大致为:将所有的节点形成一个循环链表,每个节点都会监控位于自己左右两边的节点,当有节点新增时,相邻的节点保证当前广播的消息会复制到新的节点上;当有节点失效时,相邻的节点会接管以保证本次广播的消息会复制到所有的节点。在 master 和 slave 上的这些 GM 形成一个组(gm_group),这个组的信息会记录在 Mnesia 中。不同的镜像队列形成不同的组。操作命令从 master 对应的 GM 发出后,顺着链表传送到所有的节点。由于所有节点组成了一个循环链表,master 对应的 GM 最终会收到自己发送的操作命令,这个时候 master 就知道该操作命令都同步到了所有的 slave 上。每当一个节点加入或者重新加入到镜像链路中时,之前队列保存的内容会被全部清空。

 

三、异常故障

1. slave 挂掉

  当 slave 挂掉之后,除了与 slave 相连的客户端连接全部断开,没有其他影响。

2. master 挂掉

  当 master 挂掉之后,会有以下连锁反应:

(1)与 master 连接的客户端连接全部断开。

(2)选举最老的 slave 作为新的 master。如果此时所有 slave 处于未同步状态,则未同步的消息会丢失。

(3)出于消息可靠性的考虑,新的 master 会重新入队所有 unack 的消息,此时客户端可能会有重复消息。

(4)如果客户端连接着 slave,并且 Basic.Consume 消费时指定了 x-cancel-on-ha-failover 参数,那么断开之时客户端会收到一个 Consumer Cancellation Notification 的通知,消费者客户端中会回调 Consumer 接口的 handleCancel 方法。如果未指定 x-cancel-on-ha-failover 参数,那么消费者将无法感知 master 宕机。

x-cancel-on-ha-failover 参数的使用示例如下:

rabbitmq docker 镜像 rabbitmq镜像集群原理_客户端_04

3. 重启

  镜像队列中最后一个停止的节点会是 master,启动顺序必须是 master 先启动。如果 slave先 启动,它会有 30 秒的等待时间,等待 master 的启动,然后加入到集群中。如果 30 秒内 master 没有启动,slave 会自动停止。当所有节点因故(断电等)同时离线时,每个节点都认为自己不是最后一个停止的节点,要恢复镜像队列,可以尝试在 30 秒内启动所有节点。

 

四、配置使用

  镜像队列的配置主要是通过添加相应的 Policy 来完成的,完整命令为:

rabbitmqctl set_policy [-p vhost] [——priority priority] [——apply-to apply-to] {name} {pattern} {definition}

  配置示例:(对队列名称以“queue_”开头的所有队列进行镜像,并在集群的两个节点上完成镜像)

rabbitmq docker 镜像 rabbitmq镜像集群原理_客户端_05

definition 部分参数解析:

(1)ha-mode:指明镜像队列的模式,有效值为 all、exactly、nodes,默认为 all。all 表示在集群中所有的节点上进行镜像;exactly 表示在指定个数的节点上进行镜像,节点个数由 ha-params 指定; nodes 表示在指定节点上进行镜像,节点名称通过 ha-params 指定,节点的名称通常类似于 rabbit@hostname。

  ha-mode 参数对排他(exclusive)队列并不生效,因为排他队列是连接独占的,当连接断开时队列会自动删除,所以实际上这个参数对排他队列没有任何意义。

(2)ha-params:不同的 ha-mode 配置中需要用到的参数。

(3)ha-sync-mode:队列中消息的同步方式,有效值为 automatic 和 manual。

<1>manual:默认值,镜像队列中的消息不会主动同步到新的 slave 中,除非显式调用同步命令。当调用同步命令后,队列开始阻塞,无法对其进行其他操作,直到同步完成。

<2>automatic:新加入的 slave 会默认同步已知的镜像队列。

(4)ha-promote-on-shutdown:定义 slave 接管 master 的行为,有效值为 when-synced 和 always。

<1>when-synced:默认值,如果 master 因为主动原因停掉,比如通过 rabbitmqctl stop 命令或者优雅关闭操作系统,那么 slave 不会接管 master,也就是此时镜像队列不可用;但是如果 master 因为被动原因停掉,比如 Erlang 虚拟机或者操作系统崩溃,那么 slave 会接管 master。这个配置项隐含的价值取向是保证消息可靠不丢失,同时放弃了可用性。

<2>always:不论 master 因为何种原因停止,slave 都会接管 master,优先保证可用性,不过消息可能会丢失。

  查看哪些 slaves 已经完成同步:

rabbitmqctl list_queues {name} slave_pids synchronised_slave_pids

  通过手动方式同步一个队列:

rabbitmqctl sync_queue {name}

  取消某个队列的同步操作:

rabbitmqctl cancel_sync_queue {name}