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

💡 原文中文,约6900字,阅读约需17分钟。
📝

内容提要

本文介绍etcd v3.5.33中WAL日志与快照的实现机制。WAL通过CRC保护的64MB段文件持久化Raft日志,快照在applied index每增10万时触发并截断旧日志。崩溃恢复按WAL重放、加载快照、追赶日志顺序进行。committedIndex与appliedIndex分离,前者由Raft推进,后者由apply协程推进,二者差值过大时可能导致Watch延迟或重启后未就绪。

🔎

延伸解读

WAL 与 MVCC 的分工

WAL 记录的是 Raft 层字节,而非 MVCC 键值。MVCC 数据存储在 bbolt 中,WAL 是共识日志的权威来源。这种分离意味着,即使 WAL 已持久化,MVCC 应用也可能滞后,导致客户端已收到提交响应但 Watch 事件未推送。理解这一分工有助于定位“Raft index 正常但 Watch 延迟高”的问题。

commit 与 apply 解耦的代价

committedIndex 由 Raft 推进,appliedIndex 由 apply 协程推进,二者有意解耦以提升吞吐。但解耦带来副作用:当差值增大时,Raft 层看似健康,实际存储可能未跟上。排障时需同时关注两个指标,不能仅凭 Raft index 判断系统状态。

快照频率的权衡

默认每 10 万 applied index 触发一次快照,但该值并非与 workload 无关。快照过频会增加 IO 负担,过疏则导致 WAL 膨胀、重启重放变长。生产环境应根据实际写入量和恢复时间目标调整 --snapshot-count,而非盲目使用默认值。

Q&A

etcd 中 WAL 日志的作用是什么?

WAL(预写日志)用于持久化 Raft 日志,确保崩溃后可以重放日志,恢复状态机。它记录的是 Raft 层的字节,而不是 MVCC 的键值对。

etcd 的 WAL 段文件是如何组织的?

WAL 由多个递增命名的段文件组成,每个段文件默认大小为 64MB。段文件包含多种记录类型,如元数据、Raft HardState、日志条目、快照元数据和 CRC 校验。写满一个段后创建新段,旧段只读。

etcd 中快照触发的条件是什么?

默认情况下,当 applied index 每增加 100000(可通过 --snapshot-count 调整)时触发快照。此外,当 follower 落后过多时,leader 也会通过 InstallSnapshot 推送快照。

etcd 节点崩溃恢复的步骤是什么?

节点重启时,恢复顺序为:1. 打开 WAL 并读取最新的 HardState;2. 重放 WAL 条目到 committed index;3. 如果存在快照,加载快照作为 MVCC 起点;4. 重放快照之后的 WAL 条目并应用;5. 启动 raftNode 并加入集群;6. 追赶 leader 上尚未应用的 committed 条目。

committedIndex 和 appliedIndex 有什么区别?

committedIndex 表示 Raft 已提交的日志位置,由 Raft 推进;appliedIndex 表示已应用到 MVCC/bbolt 的日志位置,由 apply 协程推进。两者解耦,committedIndex 可以大于 appliedIndex。

为什么 committedIndex 和 appliedIndex 会分叉?

因为 etcd 故意将 commit 和 apply 解耦,以支持批量应用、快照和 defrag 等操作。apply 协程可以批量处理条目,而快照或 defrag 可能短暂阻塞 apply,但不影响 Raft 的 commit。

当 Raft index 正常但 applied index 不涨时,可能是什么问题?

可能表明 apply 协程被阻塞,例如 bbolt 操作、快照或 defrag 导致 apply stall。此时客户端可能已收到 commit 响应,但 Watch 事件尚未推送。

etcd 中 WAL 的 fsync 延迟过高会有什么影响?

WAL 的 fsync 延迟过高会直接放大写延迟,导致 Put 操作变慢,即使 CPU 使用率很低。这通常与云盘的高延迟有关,是 WAL 轴上的性能瓶颈。

🏷️

标签

➡️

继续阅读