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 中相关变化对应的事务位置,例如 mzxid、pzxid。

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、幂等、事务和对账。