ZooKeeper ZAB全过程:选主、事务广播、zxid、崩溃恢复与一致性
ZooKeeper 不是因为“把数据复制三份”就天然一致。它使用 Leader、事务提议、法定多数、事务日志和恢复同步,让多个非拜占庭节点在宕机、网络分区、消息延迟和重启后对已提交事务的顺序达成一致。
ZAB 通常解释为 ZooKeeper Atomic Broadcast。它的核心不只是正常阶段广播写事务,还包括 Leader 变化后的恢复:新 Leader 必须先与法定多数建立合法新时期,并让参与广播的节点在安全历史上同步,才能继续接受新写入。
一、学习目标
- 区分 Leader Election、ZAB恢复同步和原子广播。
- 区分 Leader、Follower、Observer、Participant和Client。
- 解释写请求从Server接收到法定多数提交的全过程。
- 解释
zxid的 epoch/counter 结构和它不是什么。 - 证明多数派交集为什么阻止两个分区独立提交。
- 解释 Leader 崩溃后怎样选择具有安全历史的新 Leader。
- 解释恢复同步中的 DIFF、TRUNC、SNAP。
- 区分 ZooKeeper 写顺序、一致系统映像和可能陈旧的本地读。
- 处理客户端响应丢失导致的提交结果未知。
二、集群角色与投票资格
| 角色 | 是否参与投票 | 是否处理客户端连接 | 主要职责 |
|---|---|---|---|
| Leader | 是 | 是 | 排序写事务、发起Proposal、收集ACK、发布Commit |
| Follower | 是 | 是 | 转发写、持久化Proposal、ACK、应用Commit |
| Observer | 否 | 是 | 接收并应用事务、分担读流量,不计入写法定多数 |
| Client | 否 | 不适用 | 连接任一可服务Server并发起读写请求 |
Participant 通常指参与选举和法定多数的 Leader/Follower。Observer 虽然复制数据,但不能拿来凑多数派;增加 Observer 可以扩展读连接,却不会提升写入可容忍的投票节点故障数。
三、把ZooKeeper运行分成三个阶段
flowchart TD
A["Leader Election"] --> B["选出候选Leader和投票多数"]
B --> C["Discovery与新Epoch建立"]
C --> D["Synchronization历史同步"]
D --> E["Broadcast正常事务广播"]
E --> F{"Leader或Quorum是否失效"}
F -- "否" --> E
F -- "是" --> A3.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 都能独立决定写顺序:
Server A先看到 set /config v2,再看到 delete /config
Server B先看到 delete /config,再看到 set /config v2两个状态机最终结果不同。Leader 把并发写入转换成统一事务顺序,使副本按照同一顺序应用。
Leader 不是单点数据副本:它不能只在自己内存中写完就回复成功。提交仍需要投票成员构成法定多数,Leader 失去多数派后不能继续推进安全写入。
五、一次写请求的完整路径
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 位整数:
高32位:epoch
低32位:该epoch内事务计数器示意:
0x00000005_0000002A
│ └─ epoch 5中的第42个计数位置
└────────── Leader时期epoch 57.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 | 子节点集合变化版本 |
aversion | ACL变化版本 |
业务使用 setData(path, data, expectedVersion) 做 CAS 时,通常比较的是局部 version,不是手工提交 zxid。
九、法定多数怎样计算
对普通多数派配置,N 个投票成员所需法定多数:
quorum = floor(N / 2) + 1| 投票节点数 | 多数派 | 可容忍同时故障投票节点数 |
|---|---|---|
| 3 | 2 | 1 |
| 4 | 3 | 1 |
| 5 | 3 | 2 |
| 6 | 4 | 2 |
| 7 | 4 | 3 |
偶数集群不会比前一个奇数规模多容忍故障。例如 4 节点仍只能容忍 1 个故障,却增加写确认成本,生产常选 3、5、7 个投票成员。
ZooKeeper 还支持层级或自定义 QuorumVerifier 等高级配置,不能把所有部署都简化成普通数量多数;本页表格针对最常见的多数派配置。
十、为什么多数派交集能阻止双提交
5 个投票节点中,任意多数集合至少有 3 个。两个大小为 3 的集合必然至少共享一个节点:
Q1 = {A, B, C}
Q2 = {C, D, E}
交集 = {C}网络分成 3 节点和 2 节点时,只有 3 节点一侧能形成多数。2 节点一侧的旧 Leader 即使进程仍活着,也不能获得足够 ACK 推进新提交。
flowchart TD
A["5个投票节点发生网络分区"] --> B["分区一拥有3个节点"]
A --> C["分区二拥有2个节点"]
B --> D["可以形成Quorum并选主"]
C --> E["不能形成Quorum"]
E --> F["旧Leader不能继续提交写事务"]交集只是数学基础,还必须配合 epoch、投票比较、日志同步和节点不能对冲突历史同时确认等协议规则,才能形成完整安全性。
十一、Leader选举为什么不能只选Server ID最大
选举必须优先保证日志历史安全。概念上需要比较:
- 当前选举逻辑时期。
- 候选节点已接受事务历史的新旧,例如最新 zxid。
- 历史相同时再使用 Server ID 等稳定规则打破平局。
如果只选编号最大但日志落后的节点,可能丢失已经由多数派确认的事务。
选举阶段决定“谁有资格成为新 Leader”;恢复阶段还要让其他参与者与新 Leader 历史同步。选出进程不等于已经可以接受写请求。
十二、新Leader为什么要建立新epoch
epoch 区分不同 Leader 时期。新 Leader 与法定多数完成时期协商后,后续事务使用更高 epoch:
旧Leader:epoch=8
新Leader:epoch=9这样旧分区延迟到达的 epoch 8 Proposal 不会被误认为新时期事务。新 Leader 在没有获得法定多数认可前不能安全进入广播阶段。
十三、Leader崩溃后的恢复全过程
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只缺少一段后缀
Leader: 101 102 103 104 105
Follower: 101 102 103Follower 历史是 Leader 的前缀,只需补发 104、105,成本最低。
14.2 TRUNC:Follower保留了不应继续存在的尾部
Leader安全历史: 101 102 103 106
Follower历史: 101 102 103 104 105Follower 的 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 可能还没应用最新已提交事务。
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 前已经完成的相关更新。它不是对所有未来并发写加全局锁,也不能把多次业务读写自动变成事务。
准确使用时应:
- 等待 sync 回调成功。
- 再在同一 Session/连接语义下读取。
- 处理连接丢失和Session过期。
- 对跨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状态
时间线:
Client发送 setData(expectedVersion=7)
Leader获得多数ACK并提交version=8
响应返回途中网络断开
Client收到ConnectionLossClient 不能直接判定失败并用新值再次覆盖。正确恢复:
- 重新连接或恢复Session。
- 读取节点当前数据、version和业务操作ID。
- 若已是目标结果,按幂等成功处理。
- 若仍是旧版本,使用原预期版本有条件重试。
- 若被其他操作推进,进入冲突处理,不能覆盖。
ZooKeeper 保证事务提交安全,不会替业务判断 ConnectionLoss 对应请求是否已经提交。
二十一、事务日志与Snapshot怎样共同恢复
ZooKeeper 数据树主要在内存中提供读服务,但写事务要记录到磁盘事务日志,并周期性生成 Snapshot。
启动恢复可抽象为:
加载最近可用Snapshot
↓
重放Snapshot之后的事务日志
↓
恢复内存DataTree和Session相关状态Snapshot 生成期间数据树可能继续变化,因此实现需要依赖事务日志重放使最终状态一致。不能把 Snapshot 文件当作某个毫秒瞬间所有内存对象的简单拷贝。
磁盘关注:
- 事务日志顺序写延迟直接影响写确认。
- 日志与Snapshot清理不当会填满磁盘。
- 自动清理参数不能替代备份策略。
- 手工删除事务日志可能破坏恢复历史。
二十二、ZAB、Raft和Paxos的关系
| 对比 | ZAB | Raft | Paxos/Multi-Paxos |
|---|---|---|---|
| 主要使用 | ZooKeeper原子广播 | 通用复制状态机 | 共识理论与多种系统实现 |
| 领导时期 | epoch | term | ballot/round |
| 正常复制 | Proposal/ACK/Commit | AppendEntries/commitIndex | Leader驱动Accept |
| 恢复关注 | Discovery、Sync、Broadcast | 日志匹配、选主和复制 | 提案承诺与已接受值 |
三者都利用法定多数交集维护非拜占庭环境下的安全,但消息、状态机和恢复规则不同。不能用 Raft 的每个字段硬套 ZAB,也不能说它们“只是名字不同”。
二十三、JDK 8 Demo:zxid和多数派提交判定
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");
}
}预期输出:
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 集群无法写入
- 确认投票成员数量和当前可达数,Observer不计入。
- 查看是否存在稳定Leader,节点是否反复LOOKING。
- 检查选举端口、集群通信、防火墙和DNS。
- 查看事务日志磁盘延迟、磁盘满和fsync异常。
- 检查JVM长GC、CPU throttling和请求队列。
- 不要通过删除日志或强行指定旧节点为Leader“修复”。
25.2 Follower长期同步
- 比较Follower最新zxid和Leader历史。
- 判断正在DIFF、TRUNC还是SNAP。
- 检查日志保留是否足够、Snapshot大小和网络带宽。
- 检查磁盘读取/写入延迟。
- 避免所有落后节点同时重启加入造成Leader压力。
25.3 读到旧配置
- 确认读客户端和Session。
- 比较节点Stat version/mzxid。
- 检查Follower是否落后、客户端是否处于只读连接。
- 对必须看见先前写入的流程使用正确sync/read或同Session语义。
- 检查应用本地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和业务事实查询,而不是盲目覆盖重试。
二十九、关联知识点
- 分布式共识:Raft、Paxos与ZAB
- ZooKeeper数据模型与CAS
- ZooKeeper Watcher与Session
- ZooKeeper锁、选主与Fencing
- ZooKeeper运维排查
- ZooKeeper面试题
本章小结
ZAB 的完整链路是“选举安全Leader → 建立新epoch → DIFF/TRUNC/SNAP同步历史 → Proposal/ACK/Commit广播新事务”。多数派交集阻止两个网络分区同时推进写提交,zxid为事务提供时期和全序位置,事务日志与Snapshot支持崩溃恢复。它保证ZooKeeper内部状态机的安全演进,但Follower本地读仍可能落后,客户端响应仍可能丢失,跨系统业务仍需要版本CAS、幂等、事务和对账。
