4.1 ZAB 原子广播:一张提案的全票通过之路


文档摘要

4.1 ZAB 原子广播:一张提案的全票通过之路 本节摘要:ZAB(ZooKeeper Atomic Broadcast)是集群的表决规程:Leader 为写提案编上单调递增的事务号,广播给全部 Follower,收齐过半确认即提交。本节拆解提案从受理到落卷的全链路,以及崩溃恢复时按事务号对齐卷宗的规则,顺带讲清支撑这一切的日志与快照。 从一条 set 命令的旅程说起 3.2 节敲下的那句同步 create,客户端视角只花了几毫秒。现在把这毫秒拆开。假设集群三台,你的请求落在 Follower F1 上: 这短短几毫秒里藏着全节的主题。ZAB 的名字已经说明一切:原子(要么全庭生效要么全庭无效)、广播(Leader 发、Follower 接)。

4.1 ZAB 原子广播:一张提案的全票通过之路

本节摘要:ZAB(ZooKeeper Atomic Broadcast)是集群的表决规程:Leader 为写提案编上单调递增的事务号,广播给全部 Follower,收齐过半确认即提交。本节拆解提案从受理到落卷的全链路,以及崩溃恢复时按事务号对齐卷宗的规则,顺带讲清支撑这一切的日志与快照。

从一条 set 命令的旅程说起

3.2 节敲下的那句同步 create,客户端视角只花了几毫秒。现在把这毫秒拆开。假设集群三台,你的请求落在 Follower F1 上:

# 三节点集群,请求到达 Follower F1 [zk] set /config "v3" # F1 内部:发现自己是 Follower,将请求转发给 Leader # Leader 内部:受理,生成提案,进入广播流程 # 客户端最终收到:返回码 0(成功),总耗时约 3 至 8 毫秒(同机房)

这短短几毫秒里藏着全节的主题。ZAB 的名字已经说明一切:原子(要么全庭生效要么全庭无效)、广播(Leader 发、Follower 接)。它与 Paxos 的血缘常被讨论,工程上你只需记住分工:ZAB 专为 ZooKeeper 的"主备日志复制"场景设计,一个时刻只有一个 Leader 在提案权的岗位上。

广播阶段:两轮消息,过半即决

一次写请求的处理分为两轮消息传递。第一轮,Leader 把写请求封装成提案(Proposal)随事务号 zxid 发给所有 Follower,Follower 落盘事务日志后回 ACK;第二轮,Leader 收到过半 ACK 即提交——给所有 Follower 发 COMMIT,各节点把提案应用到内存树并回包客户端。

图:一次写入的两轮确认链路

图:一次写入的两轮确认链路

zxid 的结构值得记一笔:64 位,高 32 位是纪元号 epoch(每次新 Leader 上任加一,可以理解为"第几届法庭"),低 32 位是届内计数器。这个两层结构让任何两个提案都能比出全序——先比届,同届比序号。4.2 节选举时判断"谁的数据新",比的就是这组数字。

崩溃恢复:按编号对齐卷宗

Leader 宕机或失联时,ZAB 切入恢复阶段,目标是让新庭成立时所有成员对已提交的提案达成一致。规则有两条,都围着 zxid 转:已过半确认的提案必须全部保留,未过半确认的提案必须全部作废。恢复完成后,新 Leader 带着最高纪元上任,所有 Follower 与它对齐日志,集群才重新对外。

对齐动作依赖两样持久化产物。事务日志:每个提案到达 Follower 时先顺序写盘再 ACK,这是"多数派已持久化"承诺的物理基础。快照:内存树周期性全量dump,恢复时先加载快照、再重放其后日志,避免从创世重放。这也解释了 2.3 节为什么强调 dataLogDir 用独立高速盘——广播路径上每次 ACK 前都有一次 fsync,磁盘快慢直接写进写延迟。

# 数据目录里能直接看到这两类产物(version-2 为当前版本布局) $ ls /var/lib/zookeeper/version-2 snapshot.2a0000004f # 快照,文件名尾部是打快照时的 zxid 十六进制 log.2a00000040 # 事务日志,尾部是本日志起始 zxid acceptedEpoch, currentEpoch # 纪元持久化文件,恢复阶段校验用 # 四字命令概览落盘情况 $ echo mntr | nc node1 2181 | findstr /i "snapshot log" zk_last_snapshot_size 10240

快照文件名里的 zxid 有个实用推论:想看某个历史时点的数据,找到不超过该时刻的最大快照号即可。运维恢复误删节点时,这套文件就是全部依据。

观察证据:zxid 在你眼前递增

机制讲完,回到可观察层面。1.3 节用过的四字命令在这里能当"表决记录仪"用:

# 每次写操作前后各看一次 zxid,差值即为消耗的事务数 $ echo stat | nc node1 2181 | findstr Zxid Zxid: 0x2a0000001f [zk] create /audit "x" Created /audit $ echo stat | nc node1 2181 | findstr Zxid Zxid: 0x2a00000021 # 注意跳了两个:create 占一届内两个计数点并非必然, # 内部会话事务也会消耗计数,别按 1:1 换算

顺带修正一个常见误读:zxid 计数不是"每次写恰好加一"——会话管理与内部事务同样记账。把它当单调时钟用(判断先后、判断新旧),别当计数器用(统计写请求数)。

要点回顾

  • 两轮消息:提案与 ACK 一轮,COMMIT 一轮;过半 ACK 即提交,等不到全员的慢节点事后补齐。
  • zxid 两层结构:高 32 位纪元、低 32 位届内序号,构成全序;选举比新旧、日志对齐都靠它。
  • 恢复两规则:已过半的提案必保留,未过半的必作废;快照加日志重放完成重建。
  • 落盘即延迟:每提案 ACK 前有一次 fsync,事务日志独立高速盘是最有效的写优化。

常见疑问

问:读请求也走 ZAB 吗? 不走。读由各节点本地内存树直接应答,这正是读快的原因;代价是该节点可能短暂落后于 Leader——上一节 1.3 说的"顺序一致而非线性一致"就是这份账单。对强一致读有要求的场景,可在读前先执行一次 sync。

下一案件:Leader 的席位一旦空缺,法庭如何在几秒内完成换届。

两个易混概念的对账

提案与事务:一次写请求在服务端是"先转提案、表决通过后成事务"的两段身份。客户端视角只有一个请求,服务端视角是提案广播加提交两个动作——讨论日志时说提案,讨论内存树时说事务,混用会造成理解错位。
顺序写与流水线:Leader 对每个提案的表决是按序的,但对不同客户端的请求可以流水线化接收——等上一条 COMMIT 的同时受理下一条提案。这解释了为何单 Leader 写吞吐仍有数千笔每秒:等待过半 ACK 的空档没有被浪费。

观察证据补充:日志文件的增长节奏

事务日志的增长速度是写入负载的镜子。压测时盯一眼数据目录:

# 每隔五秒看日志目录体积,估算写入速率 $ for i in 1 2 3 4 5 6; do du -sk /data/zk/log/version-2 | findstr "log"; sleep 5; done # 每五秒增量稳定在若干百 KB,即可反推单笔提案的平均字节数 # 增量忽大忽小:查是否有批量写或大节点写入 —— 1 MB 上限的滥用会在这里现形

延伸追问

问:过半确认时,慢的那台落后了多少? 逻辑上最多落后"提交点之前的全部未应用提案"。物理上各 Follower 收到 COMMIT 后应用内存树是毫秒级动作,常规负载下落后窗口以毫秒计;持续落后则会被 syncLimit 判定掉队踢出表决圈,恢复后按日志追平。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U