Skip to content

ZooKeeper ZAB全过程:选主、事务广播、zxid、崩溃恢复与一致性

ZooKeeper 不是因为“把数据复制三份”就天然一致。它使用 Leader、事务提议、法定多数、事务日志和恢复同步,让多个非拜占庭节点在宕机、网络分区、消息延迟和重启后对已提交事务的顺序达成一致。

ZAB 通常解释为 ZooKeeper Atomic Broadcast。它的核心不只是正常阶段广播写事务,还包括 Leader 变化后的恢复:新 Leader 必须先与法定多数建立合法新时期,并让参与广播的节点在安全历史上同步,才能继续接受新写入。

一、学习目标

  1. 区分 Leader Election、ZAB恢复同步和原子广播。
  2. 区分 Leader、Follower、Observer、Participant和Client。
  3. 解释写请求从Server接收到法定多数提交的全过程。
  4. 解释 zxid 的 epoch/counter 结构和它不是什么。
  5. 证明多数派交集为什么阻止两个分区独立提交。
  6. 解释 Leader 崩溃后怎样选择具有安全历史的新 Leader。
  7. 解释恢复同步中的 DIFF、TRUNC、SNAP。
  8. 区分 ZooKeeper 写顺序、一致系统映像和可能陈旧的本地读。
  9. 处理客户端响应丢失导致的提交结果未知。

二、集群角色与投票资格

角色是否参与投票是否处理客户端连接主要职责
Leader排序写事务、发起Proposal、收集ACK、发布Commit
Follower转发写、持久化Proposal、ACK、应用Commit
Observer接收并应用事务、分担读流量,不计入写法定多数
Client不适用连接任一可服务Server并发起读写请求

Participant 通常指参与选举和法定多数的 Leader/Follower。Observer 虽然复制数据,但不能拿来凑多数派;增加 Observer 可以扩展读连接,却不会提升写入可容忍的投票节点故障数。

三、把ZooKeeper运行分成三个阶段

mermaid
flowchart TD
    A["Leader Election"] --> B["选出候选Leader和投票多数"]
    B --> C["Discovery与新Epoch建立"]
    C --> D["Synchronization历史同步"]
    D --> E["Broadcast正常事务广播"]
    E --> F{"Leader或Quorum是否失效"}
    F -- "否" --> E
    F -- "是" --> A

3.1 Leader Election

目标是选出下一任 Leader 候选。ZooKeeper 常用 FastLeaderElection,比对投票中的逻辑时期、候选数据新旧和 Server ID 等信息。具体字段和比较细节随版本实现变化,核心安全目标是不能选出一个会丢失已提交历史的 Leader。

3.2 Discovery与Synchronization

候选 Leader 与法定多数确认新 epoch,并比较日志历史;Follower 可能补齐缺失事务、截断不应保留的未提交尾部,或接收完整 Snapshot。只有同步到安全起点,集群才进入正常广播。

3.3 Broadcast

Leader 为写请求分配全序事务标识,广播 Proposal,收到法定多数 ACK 后提交,再通知副本应用。

四、为什么需要Leader排序写事务

如果每个 Server 都能独立决定写顺序:

text
Server A先看到 set /config v2,再看到 delete /config
Server B先看到 delete /config,再看到 set /config v2

两个状态机最终结果不同。Leader 把并发写入转换成统一事务顺序,使副本按照同一顺序应用。

Leader 不是单点数据副本:它不能只在自己内存中写完就回复成功。提交仍需要投票成员构成法定多数,Leader 失去多数派后不能继续推进安全写入。

五、一次写请求的完整路径

mermaid
flowchart TD
    A["Client向任一Server发送写请求"] --> B{"接收者是否Leader"}
    B -- "否" --> C["Follower转发请求给Leader"]
    B -- "是" --> D["Leader进入请求处理链"]
    C --> D
    D --> E["校验Session、ACL、版本和请求语义"]
    E --> F["Leader生成事务记录并分配zxid"]
    F --> G["广播Proposal给Follower"]
    G --> H["Follower写事务日志并发送ACK"]
    H --> I{"Leader获得投票成员法定多数ACK"}
    I -- "否" --> J["无法提交,等待或进入重新选举"]
    I -- "是" --> K["Leader推进Commit并通知副本"]
    K --> L["各Server按zxid顺序应用到内存数据树"]
    L --> M["完成客户端响应和相关Watch通知"]

不同版本的请求处理器类名和日志刷盘策略存在实现差异,但不可省略的安全逻辑是:统一排序、持久化可恢复事务、法定多数确认、按序提交和应用。

六、Proposal、ACK、Commit不是同一个状态

状态含义能否作为对客户端已成功的事实
Leader产生Proposal已分配事务顺序不能
单个Follower记录并ACK一个副本持久化/接受不能
获得法定多数ACK提交条件已形成是提交安全性的关键点
副本收到Commit并应用本地内存数据树可见该副本可返回新状态
Client收到成功响应客户端明确知道成功最终对调用方可确认

客户端没有收到成功响应,不代表事务没有达到多数派提交。Leader 可能在提交后、回复前崩溃,或响应在网络中丢失。

七、zxid是什么

zxid 是 ZooKeeper 事务顺序标识,常表示为 64 位整数:

text
高32位:epoch
低32位:该epoch内事务计数器

示意:

text
0x00000005_0000002A
         │        └─ epoch 5中的第42个计数位置
         └────────── Leader时期epoch 5

7.1 zxid能说明什么

  • 两个事务的全局先后顺序。
  • 日志同步时哪一方缺少后缀。
  • Leader 选举和恢复时数据历史的新旧程度。
  • Watch/Stat 中相关变化对应的事务位置,例如 mzxidpzxid

7.2 zxid不能说明什么

  • 它不是毫秒时间戳。
  • 不能换算为业务发生时间。
  • 不能跨两个完全独立ZooKeeper集群比较业务顺序。
  • 不是业务幂等键。
  • 不等于 znode 的 version

八、zxid与Stat版本字段的区别

字段含义
czxid创建该znode的事务zxid
mzxid最后修改该znode数据的事务zxid
pzxid最后修改该znode子节点集合的事务zxid
version该znode数据版本,供setData/delete CAS
cversion子节点集合变化版本
aversionACL变化版本

业务使用 setData(path, data, expectedVersion) 做 CAS 时,通常比较的是局部 version,不是手工提交 zxid。

九、法定多数怎样计算

对普通多数派配置,N 个投票成员所需法定多数:

text
quorum = floor(N / 2) + 1
投票节点数多数派可容忍同时故障投票节点数
321
431
532
642
743

偶数集群不会比前一个奇数规模多容忍故障。例如 4 节点仍只能容忍 1 个故障,却增加写确认成本,生产常选 3、5、7 个投票成员。

ZooKeeper 还支持层级或自定义 QuorumVerifier 等高级配置,不能把所有部署都简化成普通数量多数;本页表格针对最常见的多数派配置。

十、为什么多数派交集能阻止双提交

5 个投票节点中,任意多数集合至少有 3 个。两个大小为 3 的集合必然至少共享一个节点:

text
Q1 = {A, B, C}
Q2 = {C, D, E}
交集 = {C}

网络分成 3 节点和 2 节点时,只有 3 节点一侧能形成多数。2 节点一侧的旧 Leader 即使进程仍活着,也不能获得足够 ACK 推进新提交。

mermaid
flowchart TD
    A["5个投票节点发生网络分区"] --> B["分区一拥有3个节点"]
    A --> C["分区二拥有2个节点"]
    B --> D["可以形成Quorum并选主"]
    C --> E["不能形成Quorum"]
    E --> F["旧Leader不能继续提交写事务"]

交集只是数学基础,还必须配合 epoch、投票比较、日志同步和节点不能对冲突历史同时确认等协议规则,才能形成完整安全性。

十一、Leader选举为什么不能只选Server ID最大

选举必须优先保证日志历史安全。概念上需要比较:

  1. 当前选举逻辑时期。
  2. 候选节点已接受事务历史的新旧,例如最新 zxid。
  3. 历史相同时再使用 Server ID 等稳定规则打破平局。

如果只选编号最大但日志落后的节点,可能丢失已经由多数派确认的事务。

选举阶段决定“谁有资格成为新 Leader”;恢复阶段还要让其他参与者与新 Leader 历史同步。选出进程不等于已经可以接受写请求。

十二、新Leader为什么要建立新epoch

epoch 区分不同 Leader 时期。新 Leader 与法定多数完成时期协商后,后续事务使用更高 epoch:

text
旧Leader:epoch=8
新Leader:epoch=9

这样旧分区延迟到达的 epoch 8 Proposal 不会被误认为新时期事务。新 Leader 在没有获得法定多数认可前不能安全进入广播阶段。

十三、Leader崩溃后的恢复全过程

mermaid
flowchart TD
    A["Leader失联或失去Quorum"] --> B["投票节点进入LOOKING"]
    B --> C["交换选票并比较时期、zxid和ServerId"]
    C --> D["法定多数支持同一Leader候选"]
    D --> E["建立新epoch"]
    E --> F["新Leader与Follower比较历史"]
    F --> G["执行DIFF、TRUNC或SNAP同步"]
    G --> H["Follower确认同步完成"]
    H --> I["集群进入Broadcast"]
    I --> J["继续处理新写入"]

恢复的核心目标:

  • 已经提交的事务不能丢。
  • 未提交且与新 Leader 冲突的尾部不能错误保留。
  • 所有进入广播的 Participant 必须建立在兼容历史上。

十四、DIFF、TRUNC、SNAP分别解决什么

14.1 DIFF:Follower只缺少一段后缀

text
Leader:   101 102 103 104 105
Follower: 101 102 103

Follower 历史是 Leader 的前缀,只需补发 104、105,成本最低。

14.2 TRUNC:Follower保留了不应继续存在的尾部

text
Leader安全历史: 101 102 103 106
Follower历史:   101 102 103 104 105

Follower 的 104、105 可能属于旧 Leader 未提交或不再安全的分支,需要先截断到共同安全点,再接收新历史。不能简单把两边日志做并集,否则会让状态机出现不存在于新全序中的事务。

14.3 SNAP:无法仅靠短日志差异修复

当 Follower 落后太多、Leader 已没有所需事务日志、历史差异复杂或实现判断需要完整同步时,发送数据树 Snapshot,再配合后续事务恢复。

SNAP 成本更高,会消耗网络、序列化、内存和磁盘;Follower 长期落后会增加恢复时间。

14.4 名称是恢复策略,不是业务接口

DIFF、TRUNC、SNAP 是 ZooKeeper Leader/Follower 同步协议中的概念。业务应用通常不直接调用它们,但运维需要理解同步耗时、日志保留和节点长期落后的关系。

十五、已提交事务为什么不会因Leader切换丢失

一个提交事务已被旧 Leader 和法定多数接受。新 Leader 也必须获得法定多数支持。两个多数派有交集,交集节点携带已接受历史;选举的新旧比较和恢复同步规则要求新 Leader 历史覆盖安全事务。

如果承载交集信息的磁盘数据被人为删除、多个节点同时损坏或违反协议直接修改事务日志,法定多数算法无法替管理员恢复不存在的数据。因此共识不能替代备份和磁盘可靠性。

十六、为什么ZooKeeper读快但可能读到旧值

读请求通常由客户端当前连接的 Server 本地处理,不必每次经过 Leader 和多数派。这带来低延迟和高读扩展性,但 Follower 可能还没应用最新已提交事务。

mermaid
flowchart TD
    A["Client写入v2并提交"] --> B["Leader已提交"]
    B --> C["Follower A已应用v2"]
    B --> D["Follower B仍短暂显示v1"]
    E["另一个Client连接Follower B读取"] --> D

因此“ZooKeeper写是法定多数提交”不能推导出“任意Follower本地读都线性一致”。

十七、sync()能提供什么

客户端可以先调用 sync(path),等待所连接 Server 与 Leader 的状态推进,再执行读取,用于确保看见调用 sync 前已经完成的相关更新。它不是对所有未来并发写加全局锁,也不能把多次业务读写自动变成事务。

准确使用时应:

  1. 等待 sync 回调成功。
  2. 再在同一 Session/连接语义下读取。
  3. 处理连接丢失和Session过期。
  4. 对跨znode业务不变量使用 multi、version CAS或上层状态机。

不同版本、只读模式和连接切换边界应按官方文档和故障测试验证。

十八、ZooKeeper一致性保证怎样准确表述

ZooKeeper 文档常描述以下保证:

保证含义
Sequential Consistency单个客户端提交的更新按发送顺序应用
Atomicity更新要么成功,要么失败,没有部分结果
Single System Image客户端无论连接哪个Server,按会话保证观察到一致演进的系统映像
Reliability更新一旦成功,其结果不会被后续故障回滚,除非被新更新覆盖
Timeliness客户端视图在有界时间内趋于最新,边界与系统运行状态相关

这些术语不能随意扩张成“所有读取都是线性一致”或“跨多个业务系统自动强一致”。

18.1 读己之写与单调读

同一客户端成功完成写后,后续读取不会回到该客户端已观察过的更旧状态。若换成新 Session、新客户端或跨系统读取,则需要重新分析可见性边界。

18.2 multi事务边界

ZooKeeper multi 可以把多个 znode 操作作为一个原子事务提交,例如检查版本、更新数据、创建审计节点。它只覆盖同一个 ZooKeeper 集群内的操作,不能与 MySQL、Redis、MQ 组成自动原子事务。

十九、ZooKeeper与CAP怎样准确回答

发生网络分区时,只有拥有法定多数的分区能继续形成 Leader 并提交写;少数派为了避免冲突写会停止写服务。因此 ZooKeeper 在分区下更偏向一致性和分区容忍,即常说的 CP。

但不能简单说“ZooKeeper永远不可用”:

  • 多数派侧仍可服务。
  • Follower/Observer可以扩展读。
  • 某些部署可启用只读模式让少数派提供可能陈旧的读,语义必须明确。
  • 客户端是否可用还取决于它能否连接多数派侧Server。

CAP 的 A 指网络分区发生时每个非故障请求都得到非错误响应,不等于日常 SLA 百分比。

二十、响应丢失后的UNKNOWN状态

时间线:

text
Client发送 setData(expectedVersion=7)
Leader获得多数ACK并提交version=8
响应返回途中网络断开
Client收到ConnectionLoss

Client 不能直接判定失败并用新值再次覆盖。正确恢复:

  1. 重新连接或恢复Session。
  2. 读取节点当前数据、version和业务操作ID。
  3. 若已是目标结果,按幂等成功处理。
  4. 若仍是旧版本,使用原预期版本有条件重试。
  5. 若被其他操作推进,进入冲突处理,不能覆盖。

ZooKeeper 保证事务提交安全,不会替业务判断 ConnectionLoss 对应请求是否已经提交。

二十一、事务日志与Snapshot怎样共同恢复

ZooKeeper 数据树主要在内存中提供读服务,但写事务要记录到磁盘事务日志,并周期性生成 Snapshot。

启动恢复可抽象为:

text
加载最近可用Snapshot

重放Snapshot之后的事务日志

恢复内存DataTree和Session相关状态

Snapshot 生成期间数据树可能继续变化,因此实现需要依赖事务日志重放使最终状态一致。不能把 Snapshot 文件当作某个毫秒瞬间所有内存对象的简单拷贝。

磁盘关注:

  • 事务日志顺序写延迟直接影响写确认。
  • 日志与Snapshot清理不当会填满磁盘。
  • 自动清理参数不能替代备份策略。
  • 手工删除事务日志可能破坏恢复历史。

二十二、ZAB、Raft和Paxos的关系

对比ZABRaftPaxos/Multi-Paxos
主要使用ZooKeeper原子广播通用复制状态机共识理论与多种系统实现
领导时期epochtermballot/round
正常复制Proposal/ACK/CommitAppendEntries/commitIndexLeader驱动Accept
恢复关注Discovery、Sync、Broadcast日志匹配、选主和复制提案承诺与已接受值

三者都利用法定多数交集维护非拜占庭环境下的安全,但消息、状态机和恢复规则不同。不能用 Raft 的每个字段硬套 ZAB,也不能说它们“只是名字不同”。

二十三、JDK 8 Demo:zxid和多数派提交判定

java
public class ZabQuorumDemo {

    static long zxid(long epoch, long counter) {
        return (epoch << 32) | (counter & 0xffffffffL);
    }

    static long epoch(long zxid) {
        return zxid >>> 32;
    }

    static long counter(long zxid) {
        return zxid & 0xffffffffL;
    }

    static int quorum(int votingMembers) {
        return votingMembers / 2 + 1;
    }

    static boolean canCommit(int votingMembers, int acknowledgements) {
        return acknowledgements >= quorum(votingMembers);
    }

    public static void main(String[] args) {
        long id = zxid(5L, 42L);
        System.out.println("zxid=0x" + Long.toHexString(id));
        System.out.println("epoch=" + epoch(id));
        System.out.println("counter=" + counter(id));
        System.out.println("fiveNodesTwoAck=" + canCommit(5, 2));
        System.out.println("fiveNodesThreeAck=" + canCommit(5, 3));
        System.out.println("observersDoNotIncreaseVotingMembers=true");
    }
}

预期输出:

text
zxid=0x50000002a
epoch=5
counter=42
fiveNodesTwoAck=false
fiveNodesThreeAck=true
observersDoNotIncreaseVotingMembers=true

该 Demo 只演示编码和普通多数派条件;真实 ZAB 还需要投票合法性、日志持久化、epoch、顺序、同步和状态机应用规则。

二十四、商业场景怎样使用一致性语义

配置版本发布

  • 使用 znode version 做 CAS,防止并发覆盖。
  • Watch 只触发重读。
  • 客户端按 version 原子替换配置。

服务注册

  • 临时节点负责Session所有权。
  • Watch/Cache驱动地址快照收敛。
  • RPC仍需超时、熔断和幂等应对陈旧地址。

Leader任务

  • 顺序临时节点选主。
  • Session断连时进入安全模式。
  • 外部资源使用Fencing Token拒绝旧Leader。

ZooKeeper 共识保证的是 ZooKeeper 数据树事务顺序,不自动保证订单库、支付库和MQ的跨系统事务。

二十五、生产排查Runbook

25.1 集群无法写入

  1. 确认投票成员数量和当前可达数,Observer不计入。
  2. 查看是否存在稳定Leader,节点是否反复LOOKING。
  3. 检查选举端口、集群通信、防火墙和DNS。
  4. 查看事务日志磁盘延迟、磁盘满和fsync异常。
  5. 检查JVM长GC、CPU throttling和请求队列。
  6. 不要通过删除日志或强行指定旧节点为Leader“修复”。

25.2 Follower长期同步

  1. 比较Follower最新zxid和Leader历史。
  2. 判断正在DIFF、TRUNC还是SNAP。
  3. 检查日志保留是否足够、Snapshot大小和网络带宽。
  4. 检查磁盘读取/写入延迟。
  5. 避免所有落后节点同时重启加入造成Leader压力。

25.3 读到旧配置

  1. 确认读客户端和Session。
  2. 比较节点Stat version/mzxid。
  3. 检查Follower是否落后、客户端是否处于只读连接。
  4. 对必须看见先前写入的流程使用正确sync/read或同Session语义。
  5. 检查应用本地Cache是否比ZooKeeper本身更陈旧。

25.4 ConnectionLoss后是否重试写

先读取事实和版本,再决定幂等完成、条件重试或冲突处理。不要无条件 setData(..., -1),否则可能覆盖其他合法更新。

二十六、关键指标

  • 当前Leader、Follower、Observer数量。
  • LOOKING持续时间和Leader切换次数。
  • 最新zxid、Follower落后事务数。
  • Proposal、ACK、Commit延迟。
  • outstanding requests。
  • 事务日志fsync延迟和磁盘空间。
  • Snapshot生成和传输时间。
  • DIFF/TRUNC/SNAP同步次数。
  • 客户端请求延迟、连接数、Session过期数。
  • Watch数量和本地缓存版本年龄。

二十七、常见错误及后果

错误为什么错后果
Leader本地写完就算提交必须满足Quorum安全条件Leader宕机后数据丢失
Observer也算投票节点Observer不参与Quorum高估容错能力
zxid是时间戳它是epoch和计数顺序标识错误比较业务时间
新Leader只选ServerId最大必须优先保证日志历史安全丢失已提交事务
Follower日志都做并集冲突未提交尾部需要TRUNC状态机分叉
任意Follower读都是线性一致本地读可能短暂落后关键决策读取旧值
ConnectionLoss就是写失败请求可能已经提交无条件重试覆盖新状态
ZooKeeper CP等于所有业务强一致共识只覆盖ZK内部事务跨DB/MQ仍然不一致

二十八、面试标准回答

ZooKeeper 使用 ZAB 让投票节点对事务顺序达成一致。写请求无论进入哪个Server,最终由Leader校验、分配包含epoch和计数器的zxid并广播Proposal;Follower持久化后ACK,Leader获得法定多数ACK才推进Commit,各副本再按zxid顺序应用。Leader故障后先通过选举找到具有安全历史的候选,再建立新epoch并执行同步:Follower缺少后缀可DIFF补齐,保留冲突尾部要TRUNC,差异过大则SNAP同步,完成后才进入Broadcast。Observer复制数据但不参与Quorum。ZooKeeper本地读通常不走多数派,因此可能短暂陈旧;sync可帮助读取看到之前已完成更新,但不能把任意业务流程自动变成线性事务。客户端写响应丢失时结果是UNKNOWN,应按znode version和业务事实查询,而不是盲目覆盖重试。

二十九、关联知识点

本章小结

ZAB 的完整链路是“选举安全Leader → 建立新epoch → DIFF/TRUNC/SNAP同步历史 → Proposal/ACK/Commit广播新事务”。多数派交集阻止两个网络分区同时推进写提交,zxid为事务提供时期和全序位置,事务日志与Snapshot支持崩溃恢复。它保证ZooKeeper内部状态机的安全演进,但Follower本地读仍可能落后,客户端响应仍可能丢失,跨系统业务仍需要版本CAS、幂等、事务和对账。