6.1 脑裂:两个法庭同时宣判 本节摘要:脑裂指网络分区下出现两套同时有效的裁定。ZooKeeper 集群本身靠多数派天然防脑裂——凑不出过半的一侧整体罢工;真正会裂的是客户端:过时名单、假死复活、跨分区写。本节给出分区推演、证据判定与客户端侧的三道防线。 案发现场:一纸荒唐的重复扣款 复盘记录上这样写:某日上午 10 时 17 分,机房交换机固件升级触发 90 秒网络分区,支付集群五台实例被切在两侧。恢复后对账发现,同一笔订单被扣款两次。两次扣款来自两台不同实例,各自都确信"自己是主节点"。这是脑裂最典型的现场——不是集群出了两个主,而是客户端各自抱残守缺。 集群侧推演:多数派裁定天然防裂 先把集群本身的行为推演干净。
本节摘要:脑裂指网络分区下出现两套同时有效的裁定。ZooKeeper 集群本身靠多数派天然防脑裂——凑不出过半的一侧整体罢工;真正会裂的是客户端:过时名单、假死复活、跨分区写。本节给出分区推演、证据判定与客户端侧的三道防线。
复盘记录上这样写:某日上午 10 时 17 分,机房交换机固件升级触发 90 秒网络分区,支付集群五台实例被切在两侧。恢复后对账发现,同一笔订单被扣款两次。两次扣款来自两台不同实例,各自都确信"自己是主节点"。这是脑裂最典型的现场——不是集群出了两个主,而是客户端各自抱残守缺。
先把集群本身的行为推演干净。设五台 Z1 至 Z5,分区后 Z1、Z2 一侧,Z3、Z4、Z5 一侧:
# 分区期间的两侧视角(用四字命令模拟验证) $ echo srvr | nc 10.0.0.3 2181 | findstr Mode # 三台一侧 Mode: leader # 原集群 Leader 恰在多数侧,或经选举产生新主,服务照常 $ echo srvr | nc 10.0.0.1 2181 | findstr Mode # 两台一侧 Mode: follower # 两台凑不出 3/5 多数派 # 该侧所有读写被拒绝,客户端收到连接拒绝或超时 —— 1.3 节 CP 承诺的现场兑现
两台侧为什么连读都不给?假设它给读:客户端 C1 在小侧读到"主节点仍是 Z1",基于此继续派活;多数侧 Z3 已当选新主并接管。同一个"主节点"职务出现两位持有者,第五章的判例全部作废。所以小侧必须闭庭——集群侧的脑裂防线就是多数派本身,无需额外配置。值得注意的是分区愈合后的行为:小侧节点自动加入多数侧的庭,按 zxid 追平日志,全程无需人工干预。

回到案发。旧主实例 P1(与 Z1、Z2 同侧)在分区瞬间的处境:连接 SUSPENDED,本地缓存的"我是主节点"身份还在,任务调度器还在跑。如果代码正确处理了 LOST——P1 会在租约到期的瞬间停掉调度——案子不会发生。坏在 P1 的连接状态处理只写了"断线重连",没有处理 LOST:租约早已被服务端吊销,它浑然不觉。多数侧 Z3、Z4、Z5 在分区几秒内完成换届,新主实例 P2 上任开跑。P1 与 P2 并行期,就是重复扣款的窗口。
修复清单在图里,执行优先级如下:第一,所有主节点行为挂接连接状态监听,LOST 即停(5.3 节 Curator 监听器写法);第二,任务执行前重验资格——读一次选举节点确认自己仍在位;第三,业务层杜绝"写失败静默吞掉",分区期间客户端写必然失败,失败必须显式处理。
值班时收到"疑似双主"的工单,按这条证据链走:
# 1. 服务端侧:确认分区时段内是否有换届发生(看各节点日志时间戳) $ findstr /C:"LEADING" /C:"LOOKING" zk1.log # INFO [QuorumPeer[myid=3]...:QuorumPeer] - LEADING <-- 10:17:05 换届 # 2. 客户端侧:复查肇事实例的连接状态日志,找 LOST 与 GC 停顿的叠影 $ findstr /C:"LOST" /C:"SUSPENDED" pay-service.log # 10:16:40 SUSPENDED <-- 分区开始 # 10:18:10 LOST <-- 租约吊销(晚了 90 秒:期间职责未停即案发窗口) # 3. 业务侧:比对两次扣款的实例 id 与时间差,落在 10:17 至 10:18 之间即坐实
三条证据的交汇处就是案发窗口。这套排查在 6.3 节会再深化一层——那里会把 GC 停顿的计量方法补全。
问:能否配置 ZooKeeper 让小侧也继续服务? 不能,也不该想。这是 CP 的底座,1.3 节讲过的取舍没有开关可关。需要分区期可用的场景,把协调层换成 AP 型方案(7.3 节),并把一致性责任上移到业务。
下一桩冤案规模更小、频率更高:一个节点的变化,为什么能让整个集群跟着打摆。
脑裂的修复方案写完不等于生效——三宗罪的每一宗都要靠演练验证。测试环境用故障注入工具制造分区,观察系统各组件的行为是否与设计一致:
# Linux 测试机:用 tc 在指定网卡上丢包,模拟分区(恢复用第二条命令) $ tc qdisc add dev eth0 root netem loss 100% $ tc qdisc del dev eth0 root # 演练剧本:对主实例所在机器断网 90 秒(覆盖一次会话超时),逐步核对 # 1. 断网期间:新主在多数侧当选,业务切换无重复执行 # 2. 断网中:旧主的调度器应在 LOST 后停止(看日志时间戳) # 3. 恢复后:旧主自觉降级,不与新主抢任务
演练的产出不只是"通过/不通过",还有量化的窗口数据:从掉线到新主上任的实际秒数。把这个数写进容量与告警文档——业务方问"主挂了多久恢复",你要能给出实测值而不是理论值。
更严格的系统会在选主之外引入 fencing token:每次当选从 ZooKeeper 读一次版本号(或序号),持有者在操作下游资源时必须出示该 token,下游拒绝旧 token 的操作。Hadoop 的面包屑节点(7.1 节)就是它的数据自证变体。这套机制把"复活者闯入"从"靠自觉退位"升级为"凭票入场",代价是下游资源要配合校验。高风险写场景(资金、库存)值得加,普通调度场景用资格重验即可。
问:客户端有没有可能"看不到分区"从而无感? 少数可能。若客户端恰好连着多数侧且会话未受影响,分区对它完全透明——这是好事。危险的不是分区本身,而是"持有关键身份(主、锁)的客户端恰好被切到少数侧且未正确处理 LOST",排查视角永远先对准身份持有者。