分布式系统的核心矛盾在于,当部分节点不可达时,集群既不能草率地继续写入,也不应该完全停摆。Quorum仲裁就是在这两者之间划出可操作边界的一组规则。它要求任何写操作或选主行为,必须获得超过半数节点的确认,这个超过半数的集合称为多数派。多数派的意义在于,任意两个多数派必然至少有一个节点重叠,从而能传递最新状态或阻止冲突决议。理解这一点之后,脑裂预防配置就不再是玄学,而是围绕多数派能否形成、由谁参与投票、以及少数派是否被彻底隔离展开。

一、Quorum多数派模型如何决定容错能力
多数派模型的容错能力通常用节点总数 n 与可容忍故障数 f 的关系描述:n = 2f + 1。也就是说,三节点集群最多容忍一个节点故障,五节点集群最多容忍两个节点故障。这个公式的推导来自多数派判定条件:可用节点数必须严格大于总节点数的一半。以三节点为例,过半数是 2,只有挂掉 1 台时剩余 2 台还能继续形成多数派;挂掉 2 台后剩余 1 台不满足过半条件,集群必须停止接受写入或降级为只读。
这里有一个容易踩的坑:四节点集群和五节点集群看起来规模接近,但容错能力完全不同。四节点集群的过半数是 3,只能容忍 1 台故障;而四节点发生网络对半分区时,两边各 2 台,谁都凑不出 3 票,结果整个集群可能完全不可用。相反,五节点如果对半分,也会出现 2:3 的不对称分区,拥有 3 台的一方可以继续工作。因此生产环境通常建议节点数为奇数,且最少从 3 开始,就是为了避免偶数节点在分区时出现两边都无法形成多数派的情况。
判断一个集群是否具备多数派,并不需要所有节点都参与数据复制或选主,只需要统计当前在线且具备投票权的节点。下面这段伪代码展示了最基础的多数派判断逻辑,实际系统中还会叠加故障检测延时、租约时间等条件。
def has_quorum(online_voters, total_voters):
return online_voters > total_voters // 2
# 三节点集群:在线 2 台时满足多数派
assert has_quorum(2, 3) is True
# 三节点集群:在线 1 台时不满足多数派
assert has_quorum(1, 3) is False
还需要注意,多数派只是做出决议的必要条件,并不保证决议一定被少数派接受。当网络分区发生时,少数派节点虽然还活着,但无法获得多数确认,因此不应该继续接受客户端写操作。如果少数派节点因为配置错误仍然认为自己是主节点,就会形成脑裂。所以多数派模型的核心不只是统计数量,还要求少数派必须主动放弃服务能力。
二、脑裂是如何产生的以及为什么必须阻断
脑裂并不是指进程崩溃,而是集群被划分为两个或多个无法互相通信的分区,每个分区都认为自己是合法的主分区。触发条件非常常见:核心交换机瞬断、虚拟机热迁移导致网络短暂不可达、JVM 垃圾回收停顿超过心跳超时、防火墙规则误拦截心跳端口,都会让节点之间失去联系。举例来说,一个三节点的 ZooKeeper 集群中,如果节点 1 与节点 2、3 之间的网络同时断开,节点 1 会发起新的选举但无法获得多数票,理论上它会拒绝服务;但如果旧 Leader 与客户端之间仍然保持着旧连接,客户端可能继续向旧 Leader 写入数据,直到客户端发现会话失效。
双主写入是脑裂最危险的结果。数据库主备复制、消息队列控制器、分布式文件系统元数据服务,一旦出现两个节点同时以主节点身份对外提供写服务,数据就会在两侧分别分叉。后续即使网络恢复,系统也很难自动合并冲突,往往需要人工介入或从备份恢复。更严重的是共享存储场景,例如双节点高可用集群同时挂载同一个块设备,如果没有隔离机制,两边同时写文件系统可能直接导致元数据损坏。
很多脑裂事故并非因为算法缺失,而是因为配置错误地放宽了选举条件。例如旧版 Elasticsearch 中如果把 discovery.zen.minimum_master_nodes 设置为 1,意味着任意一个 master 节点都可以单独成为主节点。网络抖动后,三个 master 节点互相失联,每个节点都只有自己在线,于是都把自己选为主节点,集群瞬间分裂成三个独立实例。
discovery.zen.ping.unicast.hosts: ["node1", "node2", "node3"] discovery.zen.minimum_master_nodes: 1
上面的配置就是典型的高风险设置。在旧版 Elasticsearch 中,正确值应当设置为 master 候选节点总数的多数派,也就是 (master_eligible_nodes / 2) + 1。如果 master 候选节点是 3 个,就应该设置成 2;如果是 5 个,就设置成 3。这样可以保证任何分区中最多只有一个分区能够达到选举门槛,从源头上避免双主。
三、主流组件的Quorum与防脑裂配置要点
ZooKeeper:严格过半存活
ZooKeeper 的写请求必须由 Leader 发起,并且要同步给超过半数的 Follower 才能提交。当一个 ZooKeeper 集群失去多数派节点后,Leader 会主动降级,不再接受客户端写入,部分读取也可能被拒绝。生产上 ZooKeeper 通常配置为 3 台或 5 台,分别容忍 1 台或 2 台宕机。下面是一个三节点集群的 zoo.cfg 关键内容。
tickTime=2000 initLimit=10 syncLimit=5 dataDir=/var/lib/zookeeper clientPort=2181 server.1=zk1:2888:3888 server.2=zk2:2888:3888 server.3=zk3:2888:3888
这里的 2888 是 Follower 与 Leader 之间的同步端口,3888 是选举端口。每个节点还需要在 dataDir 目录下写入与 server.x 对应的 myid 文件。ZooKeeper 不要求所有节点配置完全一致,但 server 列表必须完整,否则节点之间无法发现彼此。很多人尝试用两台 ZooKeeper 做高可用,实际上两节点集群的多数派是 2,任意一台失联后集群立即不可用,断网时两边又都只有 1 票,无法形成多数派,不仅没有提高可用性,还增加了脑裂概率。
etcd:Raft多数派与选举超时
etcd 使用 Raft 协议,提交一条日志必须复制到多数节点。它的脑裂预防依赖任期号。每个节点在发起选举时会增加本地任期号,只有拿到多数票的候选者才能成为新 Leader。旧 Leader 收到更高任期号的心跳或请求后,会自动降级为 Follower。因此即使旧 Leader 与多数派发生网络分区,它也无法继续提交新的日志。
etcd --name infra0 \ --initial-advertise-peer-urls http://10.0.0.1:2380 \ --listen-peer-urls http://10.0.0.1:2380 \ --listen-client-urls http://10.0.0.1:2379 \ --advertise-client-urls http://10.0.0.1:2379 \ --initial-cluster-token etcd-cluster-1 \ --initial-cluster infra0=http://10.0.0.1:2380,infra1=http://10.0.0.2:2380,infra2=http://10.0.0.3:2380 \ --initial-cluster-state new
实际部署中,--heartbeat-interval 和 --election-timeout 需要根据网络质量调整。如果选举超时设置得太短,偶发网络延迟就会触发无意义的 Leader 切换;设置得太长,故障期间集群不可用时间会拉长。一般建议选举超时至少是心跳间隔的 5 到 10 倍,并且要明显大于运维中观察到的最大网络 RTT。
Elasticsearch:从minimum_master_nodes到initial_master_nodes
Elasticsearch 7.x 之后移除了 discovery.zen.minimum_master_nodes,改为第一次启动时通过 cluster.initial_master_nodes 引导主节点,后续选举则自动使用多数派规则。这样做的好处是避免运维忘记手动修改配置,但也带来一个新问题:第一次引导时的配置如果残留,可能在集群数据目录清空后重新启动时误把旧节点纳入新集群。
cluster.name: es-cluster node.name: node-1 discovery.seed_hosts: - 10.0.0.11 - 10.0.0.12 - 10.0.0.13 cluster.initial_master_nodes: - node-1 - node-2 - node-3
这套配置适用于第一次启动或完全重建集群的场景。集群一旦成功选出主节点,就应当从配置中移除 cluster.initial_master_nodes,或者确保后续启动不会把它当作新集群引导参数。否则当所有节点同时重启且网络发生变化时,节点可能各自用旧列表引导,产生多个集群。日常滚动重启不需要设置这个参数,节点会自动从 discovery.seed_hosts 发现已有集群并加入。
四、Fencing与仲裁盘:比多数派更进一步的隔离
多数派规则可以阻止少数派提交新的决议,但不能强制少数派停止向外部客户端提供旧数据。如果客户端缓存了旧 Leader 地址,或者应用层没有检查集群状态,少数派节点仍可能在一段时间内响应读请求甚至接收写请求。要真正避免这种风险,需要引入 fencing 机制。fencing 的核心思想是:当集群判定某个节点已经不属于合法多数派时,通过电源管理、IPMI、虚拟化管理接口等方式强制隔离或重启该节点,让它无法继续访问共享资源。
在高可用集群软件中,fencing 通常通过 STONITH 实现,字面意思是“击毙其他节点”。以 Pacemaker 为例,可以给每个节点配置 IPMI 隔离设备。当心跳判定节点异常后,集群不会只把服务切换到备用节点,还会对异常节点执行重启,防止它继续写共享盘。
pcs property set stonith-enabled=true pcs stonith create fence_node1 fence_ipmilan ipaddr=10.0.0.21 login=admin passwd=secret action=reboot
对于只有两个节点的集群,多数派模型天然存在缺陷:任意一台失联后剩余一台无法形成过半票数。此时可以引入仲裁盘,让外部设备充当第三张票。例如 Corosync 的 QDevice 可以部署在独立主机上,当两个节点的网络分裂时,仲裁盘只给其中一方投票,帮助该方形成多数派继续服务,另一方则被隔离。
pcs quorum device add model net host=10.0.0.100 algorithm=ffsplit
仲裁盘适合双节点加独立小主机的轻量级部署,例如两个数据库节点配一台低配服务器运行 QDevice。但需要注意仲裁盘本身不能和业务节点放在同一网络设备或同一机柜,否则交换机故障时仲裁盘也一并失联,就失去了额外投票的意义。生产上能使用三节点或五节点时,仍然优先选择奇数节点方案,因为它的行为和故障模型更简单。
五、脑裂预防配置检查清单
无论是新集群上线还是老集群加固,都可以从下面几个方面快速检查是否具备基本的脑裂防护能力。第一,节点总数是否满足 2n+1 且至少为 3,投票节点和数据节点是否分离。第二,超时参数是否合理:心跳超时、选举超时、会话超时之间是否有足够余量,是否大于已知的最大 GC 停顿时间和网络 RTT。第三,是否配置了 fencing 或等价的隔离手段,确保少数派不会继续访问共享资源。
验证脑裂预防效果不能只靠看配置文件,建议主动进行故障演练。可以用 iptables 模拟网络分区,把集群节点分成两个互不相通的组,观察少数派一侧是否自动停止写入、Leader 是否正常降级、客户端能否快速切换到多数派一侧。例如三节点集群中,把其中一个节点与另外两个完全隔离,观察被隔离节点是否会在几十秒内变为只读或离线状态。恢复网络后还要检查集群是否能够重新合并,选举过程是否平稳。
脑裂预防的核心不是追求永远不出现网络分区,而是保证一旦出现分区,系统只会有一个合法的主分区对外提供写服务,其他分区必须主动放弃写入。Quorum 规则给出判断多数派的数学边界,fencing 负责物理隔离少数派,仲裁盘解决偶数节点的投票困境,超时设置则决定系统多快能感知并完成切换。把这些参数与具体组件行为对应起来,集群才能在故障来临时保持数据一致与服务可用。