本文已参与「新人创作礼」活动,一起开启掘金创作之路
简介
集群可用于实现不同的目标:通过复制提高数据安全性、提高客户端操作的可用性、提高整体吞吐量等。对于不同的目的,选择不同的配置。
集群成员之间的网络连接故障会影响客户端操作的数据一致性和可用性(如 CAP 定理)。由于不同的应用程序对一致性有不同的要求,并且可以不同程度地容忍不可用,因此可以使用不同的分区处理策略。
CAP原理介绍
C:Consistency
即一致性,访问所有的节点得到的数据应该是一样的。注意,这里的一致性指的是强一致性,也就是数据更新完,访问任何节点看到的数据完全一致,要和弱一致性,最终一致性区分开来。
A:Availability
即可用性,所有的节点都保持高可用性。注意,这里的高可用还包括不能出现延迟,比如如果节点B由于等待数据同步而阻塞请求,那么节点B就不满足高可用性。
也就是说,任何没有发生故障的服务必须在有限的时间内返回合理的结果集。
P:Partition tolerance
即分区容忍性,这里的分区是指网络意义上的分区。由于网络是不可靠的,所有节点之间很可能出现无法通讯的情况,在节点不能通信时,要保证系统可以继续正常服务。 以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择
CAP原理说,一个数据分布式系统不可能同时满足C和A和P这3个条件。所以系统架构师在设计系统时,不要将精力浪费在如何设计能满足三者的完美分布式系统,而是应该进行取舍。由于网络的不可靠性质,大多数开源的分布式系统都会实现P,也就是分区容忍性,之后在C和A中做抉择。
看到网上很多文章说CAP原理是分布式系统的基石,但是CAP原理其实是对分布式数据存储系统的一个定论。我们假设一个分布式系统各个节点都读写同一个mysql实例,那么对于这个分布式系统来说,讨论CAP原理是没有意义的。因为各个节点之间可以不用因为数据复制而进行通信,满足分区容忍性(P),可以随时响应请求,满足可用性(A),同时因为访问的是一个数据库实例,本身已经保证了数据一致性(C)。
因此,在讨论CAP原理的时候,更多的是针对那些有数据存储、数据复制场景的分布式存储系统,也就是我们熟悉的NoSql数据库。
CAP原理简单证明
假设有节点data1和节点data2,一开始有个数据number=1。之后向data1提交更新,将数据number设置为2。
接着data1就需要将更新推送给data2,让data2也更新number数据。
接下来我们分3个场景分析:
- 在保证C和P的情况下
为了保证数据一致性,data1需要将数据复制给data2,即data1和data2需要进行通信。但是由于网络是不可靠的,我们系统又保证了分区容忍性,也就是说这个系统是可以容忍网络的不可靠的。这时候data2就不一定能及时的收到data1的数据复制消息,当有请求向data2访问number数据时,为了保证数据的一致性,data2只能阻塞等待数据真正同步完成后再返回,这时候就没办法保证高可用性了。
所以,在保证C和P的情况下,是无法同时保证A的。
- 在保证A和P的情况下
为了保证高可用性,data1和data2都有在有限时间内返回。同样由于网络的不可靠,在有限时间内,data2有可能还没收到data1发来的数据更新消息,这时候返回给客户端的可能是旧的数据,和访问data1的数据是不一致的,也就是违反了C。
也就是说,在保证A和P的情况下,是无法同时保证C的。
- 在保证A和C的情况下
如果要保证高可用和一致性,只有在网络情况良好且可靠的情况下才能实现。这样data1才能立即将更新消息发送给data2。但是我们都知道网络是不可靠的,是会存在丢包的情况的。所以要满足即时可靠更新,只有将data1和data2放到一个区内才可以,也就丧失了P这个保证。其实这时候整个系统也不能算是一个分布式系统了。
理解CAP理论的最简单方式是想象两个节点分处分区两侧。允许至少一个节点更新状态会导致数据不一致,即丧失了C性质。如果为了保证数据一致性,将分区一侧的节点设置为不可用,那么又丧失了A性质。除非两个节点可以互相通信,才能既保证C又保证A,这又会导致丧失P性质。
rabbitmq检测网络分区
如果一个节点node1异常挂起,另一个节点node2在一段时间内(默认为 60 秒)无法联系到node1,则会认为node1已经关闭。当node1恢复后,两个节点都认为对方已关闭,则集群发生网络分区。rabbitmq日志会进行提示:
2020-05-18 06:55:37.324 [error] <0.341.0> Mnesia(rabbit@warp10): ** ERROR ** mnesia_event got {inconsistent_database, running_partitioned_network, rabbit@hostname2}
可以通过服务器日志、HTTP API(用于监控)和 CLI 命令来识别分区存在:
rabbitmq-diagnostics cluster_status
正常情况下,rabbitmq-diagnostics cluster_status 会显示一个空的分区列表:
rabbitmq-diagnostics cluster_status
# => Cluster status of node rabbit@warp10 ...
# => Basics
# =>
# => Cluster name: local.1
# =>
# => ...edited out for brevity...
# =>
# => Network Partitions
# =>
# => (none)
# =>
# => ...edited out for brevity...
但是,如果发生网络分区,则有关分区的信息将出现在输出结果中:
rabbitmqctl cluster_status
# => Cluster status of node rabbit@warp10 ...
# => Basics
# =>
# => Cluster name: local.1
# =>
# => ...edited out for brevity...
# =>
# => Network Partitions
# =>
# => Node flopsy@warp10 cannot communicate with hare@warp10
# => Node rabbit@warp10 cannot communicate with hare@warp1
HTTP API 将在 GET /api/nodes 端点的分区下返回每个节点的分区信息。
如果发生分区,管理 UI 将在overview 页面上显示警告。
网络分区期间的行为
当出现网络分区时,集群的两(或更多!)批节点可以独立发展,双方都认为对方已经崩溃。这种情况称为脑裂。队列、绑定、交换可以单独创建或删除。
跨分区拆分的经典镜像队列最终将在分区的每一侧都有一个leader,双方各自为政。 Quorum 队列将在多数派中选举一个新的leader。少数方的 Quorum 队列副本将不再接受新消息、发送消费者等,所有这些工作都将由新的leader完成。
除非配置了分区处理策略,例如 pause_minority,否则即使网络连接恢复,脑裂现象会一直存在,这也是因为rabbitmq默认是对网络分区行为不进行处理的。
挂起和恢复引起的分区
虽然我们提到“网络”分区,但实际上分区是指集群的不同节点可在没有任何节点失败的情况中断通信。除了网络故障之外,针对运行中的集群节点挂起和恢复整个操作系统也会导致分区 - 因为挂起的节点不会认为自己故障,但集群中的其他节点会认为它已经故障了。
注意,某些虚拟化功能(例如将虚拟机从一台主机迁移到另一台主机)往往会导致虚拟机被挂起。
由挂起和恢复引起的分区往往是不对称的 - 挂起的节点不一定会看到其他节点已关闭,但会被集群的其余部分视为已关闭。这对 pause_minority 模式有特别的影响。
从脑裂中恢复
要从脑裂中恢复,首先选择一个最信任的分区。该分区将成为系统正常运行的状态;丢弃其他分区上发生的任何更改。
停止其他分区中的所有节点,然后重新启动它们。当他们重新加入集群时,他们将从受信任的分区恢复状态。
最后,您还应该重新启动受信任分区中的所有节点以清除警告。
停止整个集群并重新启动它可能更简单;如果是这样,请确保您启动的第一个节点来自受信任的分区。 简单来说,就是选择一个受信任的分区,将其他分区中的节点进行重启。主要是在节点上运行以下命令:
rabbitmqctl stop_app
rabbitmqctl start_app
分区处理策略
RabbitMQ 提供了三种自动处理网络分区的方法:pause-minority 模式、pause-if-all-down 模式和 autoheal 模式。默认行为称为ignore 模式。
在 pause-minority 模式下,RabbitMQ 会在看到其他节点宕机后自动暂停少数派(即少于或等于节点总数的一半)的集群节点。因此,它从 CAP 定理中选择分区容差而不是可用性。这确保了在发生网络分区的情况下,最多单个分区中的节点将继续运行。少数节点将在分区开始时暂停,并在分区结束时重新启动。此配置可防止脑裂,因此能够自动从网络分区中恢复而不会出现不一致。(当集群中的多个节点宕机时,少数节点将暂停,服务不可用,在其他节点恢复后,或恢复的节点超过半数时,将自动恢复暂停的节点)
在 pause-if-all-down 模式下,需要配置列出一系列节点,RabbitMQ 将自动暂停无法到达任何列出的节点的集群节点。换句话说,所有列出的节点都必须关闭,RabbitMQ 才能暂停集群节点。这接近于暂停少数模式,但是,它允许管理员决定首选哪些节点,而不是依赖于系统自动选择。例如,如果集群由机架 A 中的两个节点和机架 B 中的两个节点组成,并且机架之间的链接丢失,则暂停少数模式将暂停所有节点。在 pause-if-all-down 模式下,如果管理员列出了机架 A 中的两个节点,则只有机架 B 中的节点会暂停。请注意,列出的节点可能会在分区的两侧分裂:在这种情况下,没有节点会暂停。这就是为什么有一个额外的忽略/自动修复参数来指示如何从分区中恢复。
在 autoheal 模式下,如果一个分区被认为已经发生,RabbitMQ 将自动决定一个获胜的分区,并将重新启动所有不在获胜分区中的节点。与 pause_minority 模式不同,它在分区结束时生效,而不是在分区开始时生效。
获胜的分区是连接的客户端最多的分区(或者如果这产生平局,则具有最多节点的分区;如果仍然产生平局,则以未指定的方式选择其中一个分区)。 当多个节点同时挂起,而逐步恢复时,可能会存在节点不会重新启动的情况(rabbitmq-server正常运行,但app未运行)
您可以通过在配置文件中将 rabbit 应用程序的配置参数 cluster_partition_handling 设置为:
- autoheal
- pause_minority
- pause_if_all_down
如果使用 pause_if_all_down 模式,则需要附加参数:
- nodes: 应该无法暂停的节点
- recover: 恢复动作,可以忽略或自动修复
使用 pause_if_all_down 的示例配置片段:
cluster_partition_handling = pause_if_all_down
## Recovery strategy. Can be either 'autoheal' or 'ignore'
cluster_partition_handling.pause_if_all_down.recover = ignore
## Node names to check
cluster_partition_handling.pause_if_all_down.nodes.1 = rabbit@myhost1
cluster_partition_handling.pause_if_all_down.nodes.2 = rabbit@myhost2
选择哪种模式?
重要的是要理解允许 RabbitMQ 自动处理网络分区需要权衡取舍。
要通过通常不可靠的链接连接 RabbitMQ 集群,首选 Federation 或 Shovel插件来进行集群节点管理。
整体选择原则:
- ignore: 当网络可靠性实际上可能最高且节点可用性最重要时使用。例如,所有集群节点可以在同一个机架或同等设备上,连接一个交换机,该交换机也是通往外界的路由。
- pause_minority: 适合在单个区域中跨机架或可用区域进行集群时,一次丢失大多数节点(区域)的概率被认为非常低。如果/当丢失的节点回来时,这种模式会牺牲一些可用性来自动恢复。
- autoheal: 适用于更关注服务连续性而不是节点间数据一致性的情况。
结论
实际配置rabbitmq集群中,在每个节点的配置文件中加入以下内容,即可实现对脑裂问题的自动处理。
cluster_partition_handling = autoheal #或者其他的模式
不过需要注意,部分rabbitmq安装后自动的配置文件是基于json格式的,在新版本中实际上都不采用了,可以直接将原来的配置文件删除(如果没有什么其他配置内容的话),直接新建一个配置文件,写入上面的配置即可。
基于rpm安装或者yum安装的rabbitmq,一般rabbitmq的配置文件是没有的,可以将/usr/share/doc/rabbitmq-server-3.7.18/rabbitmq.config.example文件复制到/etc/rabbitmq/目录下,更改名字为:rabbitmq.conf。也可以直接创建一个新文件,名字为rabbitmq.conf