RabbitMQ 网络分区

一、网络分区的意义

  当出现网络分区时,不同分区里的节点会认为不属于自身所在分区的节点都已经挂(down)了,对于队列、交换器、绑定的操作仅对当前分区有效。
  在 RabbitMQ 的默认配置下,即使网络恢复了也不会自动处理网络分区带来的问题,从 3.1 版本开始会自动探测网络分区,并且提供了相应的配置来解决这个问题。
  当网络恢复时,网络分区的状态还是会保持,除非你采取了一些措施去解决它。网络分区带来的影响大多是负面的,极端情况下不仅会造成数据丢失,还会影响服务的可用性。那么为什么 RabbitMQ 还要引入网络分区的设计理念呢?主要是出于它本身的数据一致性复制原理及其性能的考虑。
  RabbitMQ 采用的镜像队列是一种环形链表的逻辑结构,除发送消息外的所有动作都只会向 master 发送,然后再由 master 将命令执行的结果广播给各个 slave,数据操作沿着环形链路传播,每个节点都确认完成,最后回到 master 才算操作成功。这种复制原理可以保证更强的一致性,但如果出现网络波动或者网络故障等异常情况,那么整个数据链的性能就会大大降低。如果某个节点网络异常,那么整个数据链就会被阻塞,继而相关服务也会被阻塞,所以需要引入网络分区来将异常的节点剥离出整个分区,以确保 RabbitMQ 服务的可用性及可靠性。等待网络恢复之后,可以进行相应的处理来将此前的异常节点加入集群中。
 

二、网络分区的判定

1. 判定原理

  RabbitMQ 集群节点内部通信端口默认为 25672,两两节点之间都会有信息交互。如果某节点出现网络故障,或者是端口不通,则会致使与此节点的交互出现中断,这里就会有个超时判定机制,继而判定网络分区。
  对于网络分区的判定是与 net_ticktime 这个参数息息相关的,此参数默认值为 60 秒。在 RabbitMQ 集群内部的每个节点之间会每隔四分之一的 net_ticktime 计一次应答(tick)。如果有任何数据被写入节点中,则此节点被认为已经被应答(ticked)了。如果连续 4 次,某节点都没有被 ticked,则可以判定此节点已处于“down”状态,其余节点可以将此节点剥离出当前分区。
  RabbitMQ 不仅会将队列、交换器及绑定等信息存储在 Mnesia 数据库中,而且许多围绕网络分区的一些细节也都和这个 Mnesia 的行为相关。如果一个节点不能在连续 4 次 tick 时间内连上另一个节点,那么 Mnesia 通常认为这个节点已经挂了,就算之后两个节点又重新恢复了内部通信,但是这两个节点都会认为对方已经挂了,Mnesia 此时认定了发生网络分区的情况。

2. 检测方法

(1)RabbitMQ 服务日志
(2)rabbitmqctl cluster_status:根据 partitions 项是否有内容来判断
(3)Web管理界面:若发生网络分区,会提示 Network partition detected
(4)HTTP API /api/nodes:获取节点信息,根据 partitions 项是否有内容来判断
 

三、网络分区的影响

  网络分区的影响根据消息队列是否配置镜像而有所不同。

1. 未配置镜像

  对于未配置镜像的集群,网络分区发生之后,队列也会伴随着宿主节点而分散在各自的分区之中。对于消息发送方而言,可以成功发送消息,但是会有路由失败的现象,需要配合 mandatory 等机制保障消息的可靠性。对于消息消费方来说,有可能会有诡异、不可预知的现象发生,比如对于已消费消息的 ack 会失效。如果网络分区发生之后,客户端与某分区重新建立通信链路,其分区中如果没有相应的队列进程,则会有异常报出。如果从网络分区中恢复之后,数据不会丢失,但是客户端会重复消费。

2. 已配置镜像

  对于已配置镜像的集群,网络分区发生之后,master 与 slave 节点之间的角色可能会发生转换,且同一个队列在不同分区会产生多个不同的 master,从网络分区中恢复之后,可能会发生数据丢失。
  若配置 ha-sync-mode=automatic,当有新的 slave 出现时,此 slave 会自动同步 master 中的数据。在同步的过程中,集群的整个服务都不可用,客户端连接会被阻塞。如果 master 中有大量的消息堆积,必然会造成 slave 的同步时间增长,进一步影响了集群服务的可用性。如果配置 ha-sync-mode=manual,有新的 slave 创建的同时不会去同步 master 上旧的数据,如果此时 master 节点又发生了异常,那么此部分数据将会丢失。同样 ha-promote-on-shutdown 这个参数的影响也需要考虑进来。
  网络分区的发生可能会引起消息的丢失,当然这点也有办法解决。首先消息发送端要有能够处理 Basic.Return 的能力。其次,在监测到网络分区发生之后,需要迅速地挂起所有的生产者进程。之后连接分区中的每个节点消费分区中所有的队列数据。在消费完之后再处理网络分区。最后在从网络分区中恢复之后再恢复生产者的进程。整个过程可以最大程度上保证网络分区之后的消息的可靠性。同样也要注意的是,在整个过程中会伴有大量的消息重复,消费者客户端需要做好相应的幂等性处理。当然也可以采用集群迁移的方法,将所有旧集群的资源都迁移到新集群来解决这个问题。
 

四、手动处理网络分区

  为了从网络分区中恢复,首先需要挑选一个信任分区,这个分区才有决定 Mnesia 内容的权限,发生在其他分区的改变将不会被记录到 Mnesia 中而被直接丢弃。在挑选完信任分区之后,重启非信任分区中的节点,如果此时还有网络分区的告警,紧接着重启信任分区中的节点。

1. 如何挑选信任分区

  挑选信任分区一般可以按照这几个指标进行:分区中要有 disk 节点;分区中的节点数最多;分区中的队列数最多;分区中的客户端连接数最多。优先级从前到后。

2. 如何重启节点

  RabbitMQ 中有两种重启方式:
(1)
rabbitmqctl stop
rabbitmq-server-detached

(2)

rabbitmqctl stop_app
rabbitmqctl start_app
  第一种方式需要同时重启 Erlang 虚拟机和 RabbitMQ 应用,而第二种方式只是重启 RabbitMQ 应用。两种方式都可以从网络分区中恢复,但是更加推荐使用第二种方式。

3. 重启的顺序有何考究

  必须在以下两种重启顺序中择其一进行重启操作:
(1)停止其他非信任分区中的所有节点,然后再启动这些节点。如果此时还有网络分区的告警,则再重启信任分区中的节点以去除告警。
(2)关闭整个集群中的节点,然后再启动每一个节点,这里需要确保启动的第一个节点在信任的分区之中。
  在选择哪种重启顺序之前,还需要考虑两个问题:
(1)如果选择第二种重启顺序,会有一个严重的问题,即 Mnesia 内容权限的归属问题。节点重启后若 Mnesia 数据向非信任分区靠齐,导致最终集群中的 Mnesia 数据是非信任分区,就会造成无法估量的损失。所以第二种重启顺序有可能会引起二次网络分区的发生。
(2)如果集群配置了镜像队列,随着节点的重启,会出现所有的队列的 master 都“漂移”到了同一个节点上,这样大部分压力都集中到了这个 master 节点上,从而不能很好地实现负载均衡。为了防止这个现象的发生,可以在重启之前先删除镜像队列的配置,这样能够在一定程度上阻止队列的“过分漂移”。

image

  需要在每个分区上都执行删除镜像队列配置的操作,以确保每个分区中的镜像都被删除。

4. 手动处理网络分区的步骤

(1)挂起生产者和消费者进程。这样可以减少消息不必要的丢失,如果进程数过多,情形又比较紧急,也可跳过此步骤。
(2)删除镜像队列的配置。
(3)挑选信任分区。
(4)关闭非信任分区中的节点。采用 rabbitmqctl stop_app 命令关闭。
(5)启动非信任分区中的节点。采用与步骤4对应的 rabbitmqctl start_app 命令启动。
(6)检查网络分区是否恢复,如果已经恢复则转步骤8;如果还有网络分区的报警则转步骤7。
(7)重启信任分区中的节点。
(8)添加镜像队列的配置。
(9)恢复生产者和消费者的进程。
 

五、自动处理网络分区

  RabbitMQ 提供了三种方法自动地处理网络分区:pause-minority 模式、pause-if-all-down 模式和 autoheal 模式。默认是 ignore 模式,即不自动处理网络分区,所以在这种模式下,当网络分区的时候需要人工介入。在 rabbitmq.config 配置文件中配置 cluster_partition_handling 参数即可实现相应的功能。默认的 ignore 模式的配置如下(注意最后有个点号):

image

1. pause-minority 模式

  在 pause-minority 模式下,当发生网络分区时,集群中的节点在观察到某些节点“down”的时候,会自动检测其自身是否处于“少数派”(分区中的节点小于或者等于集群中一半的节点数),RabbitMQ 会自动关闭“少数派”中所有节点的 RabbitMQ 应用,而 Erlang 虚拟机并不关闭,类似于执行了 rabbitmqctl stop_app 命令。处于关闭的节点会每秒检测一次是否可连通到剩余集群中,如果可以则启动自身的应用。相当于执行 rabbitmqctl start_app 命令。
  当对等分区出现时,会关闭这些分区内的所有节点。只有等待网络恢复之后,才会自动启动所有的节点以求从网络分区中恢复。

image

2. pause-if-all-down 模式

  在 pause-if-all-down 模式下,会配置一个信任节点列表,RabbitMQ 集群中的节点在和所配置的列表中的任何节点都不能交互时才会关闭(受信节点列表):

image

  如果一个节点与信任节点列表中任何节点都无法通信时,则会关闭自身的 RabbitMQ 应用。如果是信任节点列表中的节点本身发生了故障造成网络不可用,而其他节点都是正常的情况下,这种规则会让所有的节点中 RabbitMQ 应用都关闭,待信任节点列表中的节点的网络恢复之后,各个节点再启动自身应用以从网络分区中恢复。
  注意到 pause-if-all-down 模式下有 ignore 和 autoheal 两种不同的配置。考虑一种情形,node1 和 node2 部署在机架 A 上,而 node3 和 node4 部署在机架 B 上。此时配置{cluster_partition_handling,{pause_if_all_down,[′rabbit@node1′,′rabbit@node3′],ignore}},那么当机架 A 和机架 B 的通信出现异常时,由于 node1 和 node2 保持着通信,node3 和 node4 保持着通信,这 4 个节点都不会自行关闭,但是会形成两个分区,所以这样不能实现自动处理的功能。所以如果将配置中的 ignore 替换成 autoheal 就可以处理此种情形。

3. autoheal 模式

  在 autoheal 模式下,当认为发生网络分区时,RabbitMQ 会自动决定一个获胜(winning)的分区,然后重启不在这个分区中的节点来从网络分区中恢复。一个获胜的分区是指客户端连接最多的分区,如果产生一个平局,即有两个或者多个分区的客户端连接数一样多,那么节点数最多的一个分区就是获胜分区。如果此时节点数也一样多,将以节点名称的字典序来挑选获胜分区。

image

  对于 pause-minority 模式,关闭节点的状态是在网络故障时,也就是判定出 net_tick_timeout 之时,会关闭“少数派”分区中的节点,等待网络恢复之后,即判定出网络分区之后,启动关闭的节点来从网络分区中恢复。autoheal 模式在判定出 net_tick_timeout 之时不做动作,要等到网络恢复之时,在判定出网络分区之后才会有相应的动作,即重启非获胜分区中的节点。
  在 autoheal 模式下,如果集群中有节点处于非运行状态,那么当发生网络分区的时候,将不会有任何自动处理的动作。

4. 挑选哪种模式

  允许 RabbitMQ 能够自动处理网络分区并不一定会有正面的成效,也有可能会带来更多的问题。网络分区会导致 RabbitMQ 集群产生众多的问题,需要对遇到的问题做出一定的选择。
  如果置 RabbitMQ 于一个不可靠的网络环境下,需要使用 Federation 或者 Shovel。就算从网络分区中恢复了之后,也要谨防发生二次网络分区。
  ignore 模式:发生网络分区时,不做任何动作,需要人工介入。
  pause-minority 模式:对于对等分区的处理不够优雅,可能会关闭所有的节点。一般情况下,可应用于非跨机架、奇数节点数的集群中。
  pause-if-all-down 模式:对于受信节点的选择尤为考究,尤其是在集群中所有节点硬件配置相同的情况下。此种模式可以处理对等分区的情形。
  autoheal 模式:可以处于各个情形下的网络分区。但是如果集群中有节点处于非运行状态,则此种模式会失效。
 
 
 
 
参考:
《RabbitMQ实战指南》
 
posted @ 2025-08-05 22:54  疯一样的狼人  阅读(37)  评论(0)    收藏  举报