分布式系统百科 ·
etcd 深度解剖 已从使用者视角讲过 Revision 与 Watch;WAL
与快照 讲过 commit 与 apply 的分叉。本篇从
v3.5.33 源码 钉 MVCC
的三层概念:Revision (main, sub)
全序、keyIndex 上的 generation
生命周期、以及内存 treeIndex 与
bbolt 后端的职责分工——这是 Watch 从任意 revision 重放、Txn
compare 可见性、以及 ErrCompacted
的共同语义基础。
与 ZooKeeper 的对照基线:Hunt et al.(USENIX ATC 2010)的 znode 是覆盖写 + 一次性 Watch;etcd v3 选择 保留历史版本 直到 compaction——这是持久 Watch 的设计前提,也是磁盘占用的直接来源。
本篇在系列中的位置
篇目 核心内容 第 3 篇 · WAL 与快照 WAL 段、commit vs applied 第 4 篇 · MVCC 数据模型 Revision、keyIndex、generation 第 5 篇 · bbolt 后端 mmap、batch、单写者
版本锚定:etcd v3.5.33(tag
v3.5.33)。核心类型见server/mvcc/revision.go、server/mvcc/key_index.go;store 逻辑见server/mvcc/。无真实集群则不粘贴伪造etcdctl get --write-out=json的 revision 字段。
一、为什么 v3 需要 Revision MVCC
etcd v2
使用平面键空间(/foo →
value),Watch 基于「相对路径 + 一次性触发」,与 ZooKeeper
类似。v3 把 API 收到 gRPC KV 服务,引入:
- 全局单调 Revision:每次 mutating
事务递增
main。 - 多版本保留:旧版本驻留 bbolt,直到 compaction。
- 持久 Watch:客户端带
start_revision订阅,断开可续。
etcd v2:key → 当前 value;Watch 一次性
etcd v3:key → revision 序列;Watch 持久 + 历史回放
v2 仍存在于 v3.5.33 的
v2store(/0、/1
前缀等内部用途),但 K8s 与生产客户端只应使用 v3
API。本篇不对 v2 展开——只需记住:排障时若看到 v2
路径相关 metrics,与 K8s 对象存储通常无关。
二、Revision
(main, sub):全序与原子边界
server/mvcc/revision.go 定义:
// server/mvcc/revision.go(节选)
type revision struct {
// main:一组原子变更共享的主 revision
main int64
// sub:同一原子变更内的操作序号,递增
sub int64
}2.1 语义
main:全局事务 ID。每个 mutating 事务(含单 key Put——视为单 op 事务)使main加 1。sub:同一事务内各操作的序号,从 0 递增。Txn 中多个 compare/mod 共享同一main,以不同sub区分。
全序比较由 GreaterThan 实现:先比
main,再比 sub:
func (a revision) GreaterThan(b revision) bool {
if a.main > b.main { return true }
if a.main < b.main { return false }
return a.sub > b.sub
}2.2 持久化编码
Revision 在 bbolt 中编码为 17 字节:8 字节
big-endian main + '_' +
8 字节 big-endian
sub(revBytesLen = 8 + 1 + 8)。这使得
按 revision 排序的 range scan
可在后端高效执行——Watch 从历史 revision
追赶依赖这一编码。
2.3 与 Raft index 的关系
| 概念 | 层级 | 关系 |
|---|---|---|
| Raft index | 共识日志位置 | 一条 Entry 可含一个 v3 请求 |
Revision main |
MVCC 事务 ID | apply 成功后递增 |
Revision sub |
事务内 op | 单 Put 时 sub = 0 |
通常 appliedIndex 前进与 main revision 递增同步,但二者不可互换:internal 操作、lease checkpoint 等也会消耗 revision(第 10 篇)。排障时用 Revision 对齐 Watch 客户端;用 Raft index 对齐 WAL 轴。
三、keyIndex 与 generation
每个 key 在内存
treeIndex(B-tree,github.com/google/btree)中对应一个
keyIndex:
// server/mvcc/key_index.go(节选)
type keyIndex struct {
key []byte
modified revision // 最后一次修改的 revision
generations []generation
}key_index.go 文件头注释给出了经典例子——对
key "foo" 依次
put(1.0); put(2.0); tombstone(3.0); put(4.0); tombstone(5.0):
key: "foo"
modified: rev 5
generations:
{empty} // 当前无 live 版本
{4.0, 5.0(t)} // 一代:创建 4.0,墓碑 5.0
{1.0, 2.0, 3.0(t)} // 上一代:1.0, 2.0,墓碑 3.0
3.1 generation 是什么
generation 表示 key 的一个「生命周期段」:
- 创建:该代第一个
put记录createdrevision。 - 修改:同代内后续
put追加 revision。 - 删除:
delete写入 tombstone revision,并 开启新的空 generation。
因此 delete 不是抹掉历史,而是标记墓碑并开启新代——Watch
可观察到 DELETE_EVENT,historical query
仍可查旧版本(compaction 前)。
3.2 put 不变量
keyIndex.put 要求新 revision
严格大于 modified;否则
panic——这说明 MVCC 层假设 apply 顺序与 revision 单调一致,与
Raft 日志顺序绑定。
3.3 compaction 如何修剪 keyIndex
Compaction 按 main revision
回收旧版本:删除 rev 小于等于 compact
点的版本,但保留该 compact
点内的最大版本。若某 generation
被清空,则移除该代;所有代清空则移除整个 keyIndex。
注释中的 compact(6) 例子显示:key 最终从
treeIndex 删除——这与 bbolt 后端物理删除配合,释放 quota(第
12 篇)。
四、treeIndex 与 bbolt 的分工
flowchart TB
apply["Raft apply"] --> store["mvcc store"]
store --> ti["treeIndex in memory"]
store --> bb["bbolt backend on disk"]
ti -->|"key to revision mapping"| store
bb -->|"revision to value bytes"| store
store --> watch["watchableStore notify"]
| 组件 | 存储内容 | 作用 |
|---|---|---|
| treeIndex | key → keyIndex(generations) | 范围查询、当前 key 集合、revision 查找 |
| bbolt | revision → (key, value, tombstone) | 持久化、historical revision 扫描 |
读路径典型步骤:
- treeIndex 按 key/range 找到候选 revision 列表。
- bbolt 按 revision 取 value 字节。
- 对 historical read,过滤已 compact 的 revision → 否则
ErrCompacted。
单写者约束在 bbolt 层(第 5 篇):treeIndex 更新与 bbolt 事务在 apply 协程内串行——这是 etcd 写扩展性上限的另一根支柱,与单 Raft 组并列。
五、与 Watch、Txn、K8s resourceVersion 的接口
5.1 Watch
Watch 的 start_revision 即 MVCC Revision 的
main(子 revision 用于事件排序)。synced
watcher 等待 apply 后的新 revision;unsynced watcher 从
bbolt 扫描
[start_revision, current)——generation 结构决定
DELETE 与 PUT 事件如何还原。
5.2 Txn compare
Txn 的
mod_revision、create_revision、version
compare 字段都读 keyIndex 元数据:
mod_revision↔︎keyIndex.modified.maincreate_revision↔︎ 当前 generation 的created.mainversion↔︎ 当前 generation 内 live 版本数
compare 失败不产生新 revision——这是 MVCC 上 CAS 语义的基础(第 11 篇)。
5.3 Kubernetes resourceVersion
apiserver 把 etcd 对象的
mod_revision(或等价 MVCC revision)暴露为
metadata.resourceVersion。controller
用它判断对象是否更新——语义上依赖 Revision
全序,不是 Raft index。etcd 与 apiserver 之间的
compaction 窗口配置不当会导致 Watch 大面积
ErrCompacted → apiserver relist(第 9、13
篇)。
六、谱系、争论与开放问题
Bernstein & Goodman, SIGMOD 1983(MVCC 经典理论)
→ 各数据库实现(Postgres/InnoDB 等)
→ ZK 覆盖写 + 一次性 Watch(Hunt et al. 2010)
→ etcd v3:Revision + 保留历史 + 持久 Watch
→ 仍争:历史保留 vs 磁盘/quota;bbolt 单写者 vs LSM
| 争论 | A 侧 | B 侧 | 本系列落点 |
|---|---|---|---|
| 历史版本保留 | Watch 可回放、审计友好 | 磁盘与 compaction 运维成本 | 09、12 |
| treeIndex 全内存 | 范围查 O(log N) | 极大 keyspace 内存占用 | 05、12 |
| Revision vs Hybrid Logical Clock | 单 Raft 组下简单全序 | 多主/多组需其他时钟 | 16 对照 TiKV |
开放问题:
- auto-compaction 间隔与 Watch
SLO:保留多少 main revision 取决于
--auto-compaction-retention;间隔过长 WAL+bbolt 膨胀,过短 historical Watch 易ErrCompacted——无 workload 无关默认值。 - generation 与 long-lived key:高频更新同一 key 单 generation 内 revision 列表变长,compaction 前 memory footprint——官方建议 key 设计分散热点,但缺少形式化 cost model。
- MVCC 与 Lease 键:Lease 绑定 key 也占 revision;大规模 Node Lease 对 main 递增率的影响是 K8s 大规模集群运维话题(B 级实践文),机制见第 10、13 篇。
七、本篇不写什么
- bbolt bucket 名与 mmap 细节(见第 5 篇)。
- watchableStore synced/unsynced 实现(见第 9 篇)。
- Compaction 运维参数与 defrag 窗口(见第 12 篇)。
- v2 API 迁移史全书。
- 伪造 revision 序列或 benchmark 数字。
参考资料
规范 / 官方文档 / 源码(A)
- etcd v3.5.33,tag
v3.5.33 server/mvcc/revision.go—revision、revBytesLenserver/mvcc/key_index.go—keyIndex、generation、compaction 注释server/mvcc/store.go— MVCC store 接口- etcd 官方文档 · Data model
论文(A)
- Li et al., etcd: A Distributed Key-Value Store, ;login: 2017 — v3 MVCC 设计
- Hunt et al., ZooKeeper: Wait-free Coordination for Internet-scale Systems, USENIX ATC 2010 — 协调服务对照基线
- Bernstein & Goodman, Multiversion Concurrency Control, ACM Computing Surveys 1983 — MVCC 理论起源(概念层引用)
站内对照(A/B)
- distributed/50 · etcd 深度解剖 — Watch/MVCC 百科
- 第 3 篇 · WAL 与快照
实验台账
- 本篇无集群实测;无伪造
etcdctlrevision 输出。
上一篇:WAL 与快照
下一篇:bbolt 后端
读完这篇,下一步读什么
优先读同系列或同问题的下一篇,把单篇消费变成主题集群。
【etcd】生产全景:缺口、五轴坐标系与 16 篇路线
相对 distributed/50、39、13 钉清 etcd 生产内核缺口;定义 Raft/WAL/MVCC/Watch/Lease 五轴排障坐标系,给出 16 篇阅读路线与 K8s 控制面耦合指针。版本锚定 v3.5.33。
【etcd】treeIndex 与 Apply 管道:propose → commit → apply
走读 etcd v3.5.33 从 Raft commit 到 MVCC apply 的串行管道:treeIndex 与 bbolt 分工、consistent index、watchableStore 通知触发点,以及 committed/applied 分列排障。
【etcd】Watch 机制:watchableStore、synced/unsynced 与 ErrCompacted
钉 etcd v3.5.33 watchableStore 的 synced/unsynced/victims 三分法、Apply 后 notify 与历史追赶 syncWatchersLoop,以及 CompactRevision 触发 ErrCompacted 时的客户端重同步边界。
【etcd】Txn 与并发语义:compare/mod 与 Jepsen 边界
钉 etcd v3.5.33 Txn 的 compareToPath、MOD/CREATE/VERSION 比较与写 Txn 的 Raft 单槽 apply,并对照 Jepsen 3.4.3 的 strict-serializable 与 Lease 锁不安全子集。