土法炼钢 · 系统与基础设施

【etcd】WAL 与快照:预写日志、crash recovery 与 commit vs applied

文章导航

分类入口
distributedkubernetes
标签入口
#etcd#wal#snapshot#crash-recovery#committed-index#applied-index#persistence#v3.5.33

目录

Raft 论文(Ongaro & Ousterhout, USENIX ATC 2014)把持久化日志快照定义为复制状态机落地的两个支柱:日志保证 crash 后可重放;快照防止日志无限增长。etcd v3.5.33 的 server/wal 包把这一模型具体化为 CRC 保护的段文件;EtcdServer 则用 committedIndexappliedIndex 两个原子计数器,把「Raft 已提交」与「MVCC 已应用」显式分开。

单 Raft 组与服务器角色 已说明 raftNodeEtcdServer 边界。本篇钉 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 = 100000server/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/walserver/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 // 64MB

2.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 包含:

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.gorestartNode):

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 解耦**:

  1. 批量 apply:apply 协程可批量处理 entries,提高 bbolt 吞吐。
  2. snapshot 与 heavy apply:snapshot 或 defrag 可能短暂 stall apply,而不影响 Raft commit(已持久化在 WAL)。
  3. ReadIndex:线性一致读需确认 applied 至少到达 ReadIndex 要求的 commit 点——二者差值影响读延迟。

committedIndex - appliedIndex 持续增大:

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 是这两条的具体实现。

争论与开放问题

  1. Async apply vs 尾延迟:Raft 库允许 committed 后异步 apply(distributed/13 讨论 Async Apply 对吞吐的影响);etcd 选择解耦 commit/apply 换吞吐,代价是 Watch 与读可能短暂落后于 commit——对 K8s controller 是否可接受取决于 SLO,无统一答案。
  2. Snapshot 频率权衡snapshot-count 过小 → snapshot IO 频繁;bbolt 快照与 WAL 截断窗口变短。过大 → WAL 膨胀、重启 replay 变长。官方默认 100000 是经验值,不是 workload 无关常数。
  3. 云盘 fsync:WAL 轴性能强依赖磁盘 fsync 语义——local SSD vs 网络盘在论文实验与生产之间的 gap,是 etcd 运维文档反复提及、但学术论文很少给出跨云厂商统一模型的部分。

七、本篇不写什么


参考资料

规范 / 官方文档 / 源码(A)

论文(A)

站内对照(A/B)

实验台账


上一篇单 Raft 组与服务器角色

下一篇MVCC 数据模型

读完这篇,下一步读什么

优先读同系列或同问题的下一篇,把单篇消费变成主题集群。


By .