Raft 论文(Ongaro & Ousterhout, USENIX ATC
2014)把持久化日志与快照定义为复制状态机落地的两个支柱:日志保证
crash 后可重放;快照防止日志无限增长。etcd v3.5.33 的
server/wal 包把这一模型具体化为 CRC
保护的段文件;EtcdServer 则用
committedIndex 与
appliedIndex
两个原子计数器,把「Raft 已提交」与「MVCC
已应用」显式分开。
单 Raft
组与服务器角色 已说明 raftNode 与
EtcdServer 边界。本篇钉 WAL 段布局、snapshot
与日志截断、崩溃恢复顺序,以及 commit vs apply
分叉——这是「Raft index 正常但 Watch
延迟高」「重启后长时间 not ready」的第一归因轴。
本篇在系列中的位置
篇目 核心内容 第 2 篇 · 单 Raft 组 Leader/Follower/Learner;request 路由 第 3 篇 · WAL 与快照 WAL 段、snapshot、commit vs applied 第 4 篇 · MVCC 数据模型 Revision、keyIndex、generation
版本锚定:etcd v3.5.33(tag
v3.5.33)。WAL 实现见server/wal/wal.go;默认段大小SegmentSizeBytes = 64 * 1000 * 1000(64MB)。DefaultSnapshotCount = 100000见server/etcdserver/server.go。无真实集群则不粘贴伪造 WAL 目录列表或 recovery 日志。
一、WAL 在 etcd 栈中的位置
一次写请求在持久化轴上的路径:
Leader propose Entry
→ raftNode 收到 Ready().Entries
→ WAL.Save(HardState + Entries,fsync)
→ 复制到 Follower(Follower 同样 WAL.Save)
→ quorum ACK → committedIndex 推进
→ apply 协程读取 Entry → MVCC/bbolt
→ appliedIndex 推进
WAL 记录的是 Raft 层字节,不是 MVCC key-value。MVCC 数据在 bbolt 中(第 5 篇);WAL 是共识日志的权威来源——丢失未 fsync 的 WAL 意味着已「逻辑提交」的条目可能在 crash 后消失,破坏 Raft safety。
flowchart LR
raft["raft.Node Ready"] --> wal["WAL segments"]
wal --> disk["fsync to disk"]
raft --> commit["committedIndex"]
commit --> apply["apply goroutine"]
apply --> mvcc["MVCC plus bbolt"]
apply --> applied["appliedIndex"]
Li et al.(;login: 2017)强调 etcd 把 Raft 日志与 BoltDB
事务分离——WAL 负责共识顺序,后端负责键值布局。这一分工在
v3.5.33 仍然成立,只是包路径为 server/wal 与
server/mvcc/backend。
二、WAL 段文件布局
server/wal/wal.go 把 WAL
定义为逻辑上的稳定存储,物理上由递增命名的段文件组成:
// server/wal/wal.go(节选)
const (
metadataType int64 = iota + 1
entryType
stateType
crcType
snapshotType
)
var SegmentSizeBytes int64 = 64 * 1000 * 1000 // 64MB2.1 记录类型
| 类型 | 含义 |
|---|---|
metadataType |
集群元数据(每个段文件头部) |
stateType |
Raft HardState(term、vote、commit) |
entryType |
Raft 日志条目 |
snapshotType |
Snapshot 元数据 |
crcType |
CRC 校验 |
每条记录带 CRC32(Castagnoli 多项式);读路径遇到
ErrCRCMismatch 即 WAL 损坏——需要 snapshot +
后续 WAL 恢复,或从备份重建。
2.2 段切换
单个段文件预分配约
64MB(SegmentSizeBytes)。写满后创建新段,旧段只读。目录下文件名递增,便于
Open 时顺序 replay。
2.3 fsync 与云盘
WAL 在 Save 路径上调用
fsync(除非测试场景设置 unsafeNoSync)。超过
warnSyncDuration(1s)会打 warning——这是
WAL 轴上云盘高延迟的直接信号,不是 MVCC
慢。
工程间隙:Raft 论文实验常用本地 SSD;生产 K8s 控制面 etcd 常见云盘 + 限 IOPS——fsync 尾延迟会直接放大 write latency,与「CPU 很低但 Put 慢」并存。本篇不做 benchmark;口径见官方 Hardware guidelines(B 级)。
三、Snapshot:触发、内容与日志截断
3.1 何时触发
EtcdServer 默认在 applied index
每增加 DefaultSnapshotCount(100000)
时触发 snapshot(可通过 --snapshot-count
调整)。此外,slow follower 落后过多时,Leader 也会经 Raft
InstallSnapshot
推送快照(DefaultSnapshotCatchUpEntries = 5000
控制保留条目数以助 catch-up)。
Snapshot 包含:
- Raft 状态(term、index 等)
- MVCC 后端的一致视图(bbolt 数据文件 + 必要元数据)
3.2 与 WAL 的关系
Snapshot 成功后,Raft 可以 compact 旧日志条目——只保留 snapshot index 之后的 WAL。这是 Ongaro 论文 §7 Log Compaction 的工程实现。
流程概要:
appliedIndex 达到 snapshot 阈值
→ 生成 backend snapshot 文件
→ 写入 WAL snapshotType 记录
→ raft compact 旧 entries
→ 删除已截断 WAL 段(purge 协程)
Follower 若落后超过保留窗口,Leader 发
InstallSnapshot;Follower 加载 snapshot
并重放后续 WAL——这比逐条 catch-up 快,但也是 member
replace / 新 Learner 运维的关键路径(第 14
篇)。
3.3 失败模式
| 错误 | 典型原因 |
|---|---|
ErrSnapshotMismatch |
snapshot 元数据与 backend 不一致 |
ErrSnapshotNotFound |
缺失 snapshot 文件但 WAL 已截断 |
| snapshot 期间 NOSPC | 磁盘满;与 alarm 联动(第 12 篇) |
四、Crash recovery:重启时发生什么
节点重启时 EtcdServer
大致按以下顺序恢复(细节见
server/etcdserver/server.go 启动路径与
server/etcdserver/raft.go 的
restartNode):
1. Open WAL:从最新 HardState 读 term / commit
2. Replay WAL entries:恢复到 committed index
3. 若存在 snapshot:加载 snapshot 作为 MVCC 起点
4. 重放 snapshot index 之后的 WAL entries → apply
5. 启动 raftNode,加入集群
6. 追赶 Leader 上尚未 apply 的 committed entries
关键不变量:WAL 中已 fsync 的 committed 条目必须在重启后仍能被 apply——否则 Raft safety 破功。etcd 在 apply 前先把 Entry 写 WAL,正是为了 crash 后重放。
Learner / Follower 的 recovery 与 Leader 相同——都要先本地 WAL + snapshot 一致,再经网络 catch-up。
五、committed index vs applied index
EtcdServer 维护两个独立计数器(均需 64-bit
atomic 访问):
// server/etcdserver/server.go(节选)
type EtcdServer struct {
appliedIndex uint64 // MVCC 已应用到的 Raft index
committedIndex uint64 // Raft 已提交的 index
// ...
}| 指标 | 含义 | 由谁推进 |
|---|---|---|
committedIndex |
多数派已 ACK、可安全应用的日志位置 | Raft / raftNode |
appliedIndex |
已执行 MVCC/bbolt 变更的位置 | apply 协程 |
5.1 为何会分叉
commit 与 apply ** intentionally 解耦**:
- 批量 apply:apply 协程可批量处理 entries,提高 bbolt 吞吐。
- snapshot 与 heavy apply:snapshot 或 defrag 可能短暂 stall apply,而不影响 Raft commit(已持久化在 WAL)。
- ReadIndex:线性一致读需确认 applied 至少到达 ReadIndex 要求的 commit 点——二者差值影响读延迟。
当 committedIndex - appliedIndex
持续增大:
- Raft 层「健康」(复制无 lag)
- 客户端可能已收到 commit 响应,但 Watch 事件尚未推送(apply 未完成)
etcdctl endpoint status的RAFT APPLIED INDEX明显低于RAFT INDEX(字段名因版本略有差异——有环境时对照官方文档,本篇不贴伪造输出)
5.2 排障口令
| 现象 | 优先怀疑 |
|---|---|
| Raft index 涨、applied 不涨 | apply stall(bbolt、snapshot、defrag) |
| 二者同步涨但 Put 慢 | WAL fsync / 磁盘 |
| 重启后 applied 远小于 committed | recovery 中途;等待 catch-up |
| Follower applied lag 大 | 网络或磁盘慢;InstallSnapshot |
不要把「Raft INDEX 正常」等同于「存储已追上」——这是 WAL 轴与 MVCC 轴的分界。
六、与 Raft 论文及开放问题的关系
Ongaro 论文 §5.4 要求 Leader 在 commit 前持久化条目;§7 要求 snapshot 后丢弃旧日志。etcd WAL 是这两条的具体实现。
争论与开放问题:
- Async apply vs 尾延迟:Raft 库允许 committed 后异步 apply(distributed/13 讨论 Async Apply 对吞吐的影响);etcd 选择解耦 commit/apply 换吞吐,代价是 Watch 与读可能短暂落后于 commit——对 K8s controller 是否可接受取决于 SLO,无统一答案。
- Snapshot
频率权衡:
snapshot-count过小 → snapshot IO 频繁;bbolt 快照与 WAL 截断窗口变短。过大 → WAL 膨胀、重启 replay 变长。官方默认 100000 是经验值,不是 workload 无关常数。 - 云盘 fsync:WAL 轴性能强依赖磁盘 fsync 语义——local SSD vs 网络盘在论文实验与生产之间的 gap,是 etcd 运维文档反复提及、但学术论文很少给出跨云厂商统一模型的部分。
七、本篇不写什么
- MVCC Revision 与 bbolt bucket 布局(见第 4–5 篇)。
- Apply 管道与 watch 通知触发点(见第 6 篇)。
- member add / backup restore 操作手册(见第 14 篇)。
- 伪造 WAL 目录、
etcdctl输出或 fsync 延迟数字。
参考资料
规范 / 官方文档 / 源码(A)
- etcd v3.5.33,tag
v3.5.33 server/wal/wal.go— WAL 类型、段大小、CRCserver/etcdserver/server.go—DefaultSnapshotCount、appliedIndex/committedIndexserver/etcdserver/raft.go—apply结构、restartNode- etcd 官方文档 · Monitoring — metrics 字段语义
论文(A)
- Ongaro & Ousterhout, In Search of an Understandable Consensus Algorithm, USENIX ATC 2014 — §5.4 持久化、§7 快照
- Li et al., etcd: A Distributed Key-Value Store, ;login: 2017 — WAL 与后端分离
站内对照(A/B)
- distributed/13 · Raft 深度重写 — Async Apply、快照
- 第 2 篇 · 单 Raft 组
实验台账
- 本篇无集群实测;无伪造 WAL/
etcdctl输出。
上一篇:单 Raft 组与服务器角色
下一篇:MVCC 数据模型
读完这篇,下一步读什么
优先读同系列或同问题的下一篇,把单篇消费变成主题集群。
【etcd】生产全景:缺口、五轴坐标系与 16 篇路线
相对 distributed/50、39、13 钉清 etcd 生产内核缺口;定义 Raft/WAL/MVCC/Watch/Lease 五轴排障坐标系,给出 16 篇阅读路线与 K8s 控制面耦合指针。版本锚定 v3.5.33。
【etcd】排障五轴:Raft/WAL/MVCC/Watch/Lease 口令表
按 Raft、WAL、MVCC、Watch、Lease 五轴做症状否证;解释 etcdctl endpoint status/health 字段语义;并对照 K8s 控制面 apiserver 超时与 Node Lease 漂移。
【etcd】单 Raft 组与服务器角色:Leader、Follower、Learner 与 request 路由
钉清 etcd v3.5.33 单 Raft 组拓扑、EtcdServer 与 go.etcd.io/raft/v3 边界、Leader/Follower/Learner 角色及 gRPC 写读路由;与 distributed/13 分工。
【etcd】MVCC 数据模型:Revision、keyIndex 与 generation
拆解 etcd v3.5.33 的 Revision (main, sub) 全序、keyIndex/generation 生命周期与 treeIndex 分工;简要对照 v2 平面键空间,交代 MVCC 与 Watch/compaction 的语义基础。