Skip to content

ZooKeeper 集群脑裂问题与过半机制

2015 年,某个知名的互联网公司发生了一次故障。

故障的原因很简单:他们的 ZooKeeper 集群有 5 台机器,其中 2 台因为网络问题和其他 3 台断开了连接。

按照预期,这 2 台机器应该停止服务,让另外 3 台继续工作。

但实际上,5 台机器都在各自运行,都认为自己是「主节点」。

这就是分布式系统中最著名的问题之一:脑裂(Split-Brain)

什么是脑裂?

在分布式系统中,「脑裂」指的是集群被网络分区拆分成多个部分,每个部分都认为自己是唯一的集群,各自独立运行。

类比一下:如果把 ZooKeeper 集群比作一个公司的董事会,脑裂就像是电话会议断线后,两个会议室里的人各自以为自己在主持会议,做出了互相矛盾的决策。

脑裂会导致什么问题?

  • 数据不一致:两个「主节点」各自处理请求,写入不同的数据
  • 重复执行:同一个任务被两个节点执行两次
  • 服务不可用:客户端连接了错误的节点,获取到错误的数据

过半机制:ZooKeeper 的解决方案

ZooKeeper 用一个简单而优雅的方案解决了脑裂问题:过半机制(Majority Quorum)

只有当集群中超过半数的节点正常运行并能相互通信时,集群才能对外提供服务。

为什么是过半?

这要从 N = 2F + 1 说起。

假设集群有 N 台机器,最多能容忍 F 台机器故障而仍然正常工作。那么:

  • N = 1,最多容忍 0 台故障(1 = 2×0 + 1)✓
  • N = 3,最多容忍 1 台故障(3 = 2×1 + 1)✓
  • N = 5,最多容忍 2 台故障(5 = 2×2 + 1)✓
  • N = 4,最多容忍 1 台故障(4 ≠ 2×1 + 1)

为什么 N 要是奇数?

因为 4 台机器的集群,最多容忍 1 台故障。但 1 < 4/2,所以当 2 台机器故障时,剩下的 2 台都无法组成「过半」。

所以 4 台和 5 台机器的容灾能力是一样的,但 5 台多了一次「容错机会」。

过半机制如何避免脑裂?

回到开头的场景:

5 台机器的 ZooKeeper 集群,网络分区后变成 2 + 3。

分区一:2 台机器
分区二:3 台机器

过半机制判断:

  • 分区一(2 台):2 < 5/2,不是「过半」,不能提供服务
  • 分区二(3 台):3 > 5/2,是「过半」,可以继续服务

只有 3 台的那个分区能继续工作,2 台的那个分区会停止服务。

这就是 ZooKeeper 的「要么全有,要么全无」策略。

ZooKeeper 的读写机制

理解过半机制后,再来看 ZooKeeper 的读写流程:

写操作:需要过半节点确认

客户端发送写请求到 Leader

Leader 提案(propose)写入数据

Follower 接收提案,响应「收到」

当有过半节点(包括 Leader 自己)都确认后

Leader 提交(commit)事务

返回成功给客户端

为什么需要过半确认?

因为只有过半节点确认了,即使 Leader 挂了,新的 Leader 也一定包含这条数据。

数学证明: 如果有 N=2F+1 台机器,过半就是 F+1 台。当 Leader 挂了,新 Leader 一定是从 F+1 台中产生,而旧数据在这 F+1 台中至少有一份。

读操作:可以读任意节点

ZooKeeper 的读操作可以在任意节点上执行:

java
// 读请求可以直接发给 Follower
byte[] data = zk.getData("/config/app", false, stat);

这意味着读操作可能读到过期数据。

但这通常是可接受的——配置服务、选举服务对一致性要求高,读多台可以接受短暂的不一致。

如果需要强一致读,可以强制读 Leader:

java
// 强制读取 Leader
zk.sync("/config/app");
byte[] data = zk.getData("/config/app", false, stat);

Leader 选举:重新组建集群

当 Leader 挂了,或者集群启动时,需要选举新的 Leader。

选举算法:ZAB 协议

ZooKeeper 使用 ZAB(ZooKeeper Atomic Broadcast) 协议进行 Leader 选举。

选举过程可以简化为「比大小」:

每个服务器投票时,携带两个信息:

  1. myid:服务器 ID(配置文件中指定,越大越优先)
  2. zxid:最后一条事务的 ID(越大说明数据越新)

投票规则:

  • 先比较 zxid,大的优先(数据越完整越优先)
  • zxid 相同则比较 myid,大的优先

选举过程

假设 5 台机器的集群,其中 Leader 挂了:

时刻 1:每个服务器投票给自己
  Server1: (zxid=100, myid=1) → 票数:1
  Server2: (zxid=100, myid=2) → 票数:1
  Server3: (zxid=100, myid=3) → 票数:1
  Server4: (zxid=200, myid=4) → 票数:1  (zxid 最大)
  Server5: (zxid=200, myid=5) → 票数:1

时刻 2:Server1、2、3 收到 Server4、5 的票
  Server1: 收到 (zxid=200) → 改投 Server4 → 票数:2
  Server2: 收到 (zxid=200) → 改投 Server4 → 票数:3
  Server3: 收到 (zxid=200) → 改投 Server4 → 票数:4

时刻 3:Server4 获得过半票数,成为新 Leader

zxid 大的优先,保证了新 Leader 拥有最完整的数据。

网络分区与恢复

网络分区发生时

正常状态:
[Client] → [LoadBalancer] → [Server1, Server2, Server3, Server4, Server5]

                                   [ZooKeeper Cluster]
                                   (5 台,Leader 正常运行)

网络分区:
[Client] → [LoadBalancer] → [Server1, Server2, Server3]  ← 还能工作
                          [Server4, Server5]           ← 停止服务

分区后的 ZooKeeper:
[Server1, Server2, Server3] → 3 台,过半,继续服务
[Server4, Server5]           → 2 台,不过半,停止服务

网络恢复后

当网络恢复后,被隔离的服务器会重新加入集群:

  1. 发现 Leader 还在,主动同步 Leader 的数据
  2. 丢弃自己在隔离期间收到的写请求(因为没有过半确认)
  3. 恢复服务

ZooKeeper 的设计哲学:不要相信在网络分区期间产生的数据。 因为那些数据只有少数节点知道,没有过半确认,不是「合法」的数据。

过半机制的设计权衡

优点

  1. 简单高效:不需要复杂的分布式事务协议
  2. 强一致性:写入需要过半确认,保证数据不丢失
  3. 容错能力强:N=2F+1 的设计,最多容忍 F 台故障

缺点

  1. 不能容忍「半死不活」:比如 5 台机器,3 台正常、2 台响应很慢,整个集群性能会被拖慢
  2. 读取不是强一致:可能读到旧数据
  3. 必须奇数部署:偶数台没有意义

实际配置建议

集群规模容灾能力适用场景
3 台容忍 1 台故障开发测试环境
5 台容忍 2 台故障生产环境推荐
7 台容忍 3 台故障对可用性要求极高的场景

一般建议用 5 台,既保证了一定的容灾能力,又不会像 7 台那样增加运维复杂度。

总结

ZooKeeper 通过过半机制巧妙地解决了脑裂问题:

  1. 写操作需要过半节点确认,保证数据不会丢失
  2. 只有过半节点才能组成合法集群,避免多个分区同时工作
  3. 选举时比较 zxid 和 myid,保证新 Leader 数据最完整

这个设计的核心思想是:宁可停止服务,也不要产生错误的数据。

在分布式系统中,「正确性」永远比「可用性」更重要。ZooKeeper 选择了 CP(一致性 + 分区容忍),而不是 AP(可用性 + 分区容忍)。


留给你的问题:

假设你有一个 5 台机器的 ZooKeeper 集群,突然有 3 台机器同时宕机。剩下的 2 台还能服务吗?

如果此时有客户端连接这 2 台服务器,它读取到的数据和真正的数据可能一致吗?

这个问题涉及到 CAP 定理的权衡,你有兴趣深入研究吗?

基于 VitePress 构建