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

【etcd】MVCC 数据模型:Revision、keyIndex 与 generation

文章导航

分类入口
distributedkubernetes
标签入口
#etcd#mvcc#revision#keyindex#generation#treeindex#v3#v3.5.33

目录

分布式系统百科 · 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.goserver/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 服务,引入:

  1. 全局单调 Revision:每次 mutating 事务递增 main
  2. 多版本保留:旧版本驻留 bbolt,直到 compaction。
  3. 持久 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 语义

全序比较由 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 subrevBytesLen = 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 的一个「生命周期段」:

因此 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 扫描

读路径典型步骤:

  1. treeIndex 按 key/range 找到候选 revision 列表。
  2. bbolt 按 revision 取 value 字节。
  3. 对 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_revisioncreate_revisionversion compare 字段都读 keyIndex 元数据:

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

开放问题

  1. auto-compaction 间隔与 Watch SLO:保留多少 main revision 取决于 --auto-compaction-retention;间隔过长 WAL+bbolt 膨胀,过短 historical Watch 易 ErrCompacted——无 workload 无关默认值。
  2. generation 与 long-lived key:高频更新同一 key 单 generation 内 revision 列表变长,compaction 前 memory footprint——官方建议 key 设计分散热点,但缺少形式化 cost model。
  3. MVCC 与 Lease 键:Lease 绑定 key 也占 revision;大规模 Node Lease 对 main 递增率的影响是 K8s 大规模集群运维话题(B 级实践文),机制见第 10、13 篇。

七、本篇不写什么


参考资料

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

论文(A)

站内对照(A/B)

实验台账


上一篇WAL 与快照

下一篇bbolt 后端

读完这篇,下一步读什么

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


By .