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

【etcd】treeIndex 与 Apply 管道:propose → commit → apply

文章导航

分类入口
distributedkubernetes
标签入口
#etcd#apply#treeIndex#mvcc#watch#raft#v3.5

目录

第 5 篇 钉了 bbolt 的 batch 与单写者;Apply 管道回答:Raft committed 的 entry 如何变成 MVCC revision、何时触发 Watch、以及 applied index 落后 commit index 时客户端为何仍可能读到旧数据(线性读会等待)。

本篇是五轴中 「Raft 轴 vs MVCC/存储轴」 的分界篇:proposal 卡在复制还是 apply,证据包不同。

本篇在系列中的位置

篇目 核心内容
第 5 篇 · bbolt 后端 mmap、batch、单写者
第 6 篇 · Apply 管道 propose → commit → apply、Watch 触发
第 7 篇 · 写入路径 Txn、mod revision、quota
系列目录 关键问题、阅读路径

版本锚定:etcd v3.5.33(tag v3.5.33)。机制对齐 server/etcdserver/server.goapplyAll / applyEntries / apply)、server/mvcc/index.goserver/mvcc/watchable_store_txn.go无真实集群则不粘贴伪造 endpoint status 的 index 字段。


一、三段索引:propose、commit、apply

客户端 Put/Txn 经 gRPC 进入 Leader 的 raftRequestPropose 写入 WAL 并复制 → 多数派 ack 后 commit index 前进 → 本地 apply 例程消费 committed entry,调用 applyV3.Apply

sequenceDiagram
  participant client as gRPC client
  participant leader as EtcdServer Leader
  participant raft as Raft plus WAL
  participant apply as apply goroutine
  participant mvcc as MVCC plus treeIndex
  client->>leader: Put or Txn
  leader->>raft: Propose InternalRaftRequest
  raft->>raft: replicate and commit
  raft->>apply: committed entries
  apply->>mvcc: applyV3.Apply
  mvcc->>mvcc: treeIndex plus bbolt
  mvcc-->>client: response after apply

图:写请求在 apply 完成 后才对客户端可见(raftRequestOnce 等待 applyResult)。commit ≠ apply:follower 可能已 commit 但 apply 滞后;Serializable 读不等待 apply 对齐(第 8 篇)。

索引 含义 典型排障
committed index Raft 已提交日志位置 复制 lag、磁盘慢
applied index 已映射到 MVCC 的位置 apply 慢、snapshot 恢复
currentRev MVCC 逻辑 revision compaction、Watch start rev

二、applyAll 与 applyEntries

EtcdServer.applyAll 是每个 tick 的核心循环(server.go):

  1. applySnapshot — 若有新快照,替换 backend、重建 store;
  2. applyEntries — 消费 apply.entries 中尚未 apply 的 committed entry;
  3. applyWait.Trigger(appliedi) — 唤醒等待线性读的 goroutine;
  4. 等待 Raft 侧磁盘写入通知后 triggerSnapshot

applyEntries 计算 ents = apply.entries[appliedi+1-firsti:],调用 apply(ents, confState)。若 firsti > appliedi+1 直接 panic——说明 WAL/快照与内存状态不一致,属于数据损坏级故障。

apply() 逐条处理 EntryNormalEntryConfChange


三、treeIndex:内存中的 key → revision

treeIndexserver/mvcc/index.go)是 google/btree 实现的内存索引,与 bbolt 分工明确:

操作 treeIndex bbolt key bucket
Put Put(key, idxRev) 记录 revision 链 UnsafeSeqPut(revBytes, marshaled KV)
Range Revisions(key, end, atRev, limit) 按 revision 取 value
Compact Compact(rev) 裁剪旧 revision 索引 异步物理删除(compaction 调度)

每个 user key 对应 keyIndex 结构,维护 (main, sub) revision 序列与 tombstone。Range 路径:先在 treeIndex 过滤 revision,再批量读 bbolt——避免全表扫描。

Revision 分配在 storeTxnWrite.End()currentRev++ 作为本次 txn 的 main;txn 内第 \(n\) 个 op 的 sub = len(changes)(见 kvstore_txn.go put)。因此单个 Txn 内多条写共享同一 ModRevision(main),以 sub 区分顺序——这是 第 7 篇 mod revision 语义的基础。


四、applyV3 分层与 consistent index

applyEntryNormale.Index > consistIndex.ConsistentIndex() 时设置 ShouldApplyV3 = ApplyBoth,并通过 txPostLockInsideApplyHook 在 bbolt 事务锁内更新 consistent index(与 Raft applied index 对齐)。重复启动时,已在 backend 持久化的 entry 可跳过 MVCC 重放,加速 recovery。

applier 栈(apply.go):

quotaApplierV3 → authApplierV3 → applierV3 → mvcc/store

写 apply 在 LockInsideApply 内完成 bbolt 写;consistent index 更新确保 crash 后不会重复 apply 已持久化的 v3 状态。


五、Watch 触发点:watchableStore

MVCC store 之上包装 watchableStorewatchable_store.go)。写事务结束于 watchableStoreTxnWrite.End()

// server/mvcc/watchable_store_txn.go @ v3.5.33(节选)
func (tw *watchableStoreTxnWrite) End() {
    changes := tw.Changes()
    // ... 构造 mvccpb.Event 列表 ...
    tw.s.mu.Lock()
    tw.s.notify(rev, evs)
    tw.TxnWrite.End()
    tw.s.mu.Unlock()
}

要点:

  1. notify 在 MVCC txn End 时同步调用,在 watchableStore.mu 保护下;
  2. 事件 revision 为 tw.Rev()+1(apply 后的新 main rev);
  3. synced / unsynced watcher 队列、victims 处理在 syncWatchersLoop 异步进行(第 9 篇展开)。

因此 Watch 延迟 = apply 延迟 + 通知队列调度,不是 Raft commit alone。apiserver Watch 卡住时,应先查 applied index 与 MVCC rev,再查 watcher 背压。


六、committed vs applied:排障分列

Raft lag(commit 慢)与 apply lag(MVCC/bbolt 慢)症状可能相似(写超时),证据包不同:

观察 倾向轴
committed index 落后 leader index Raft 复制 / WAL
applied index 明显小于 committed index apply / backend batch / 大 txn
线性读慢、写正常 ReadIndex + applyWait(第 8 篇)
Watch 事件断流但写成功 watchableStore / 客户端 rev

processInternalRaftRequestOncecommitted - applied > maxGapBetweenApplyAndCommitIndex 时返回 ErrTooManyRequests,背压客户端 proposal——这是 apply 滞后直接反馈到写路径的门。

Snapshot apply 会阻塞整条管道:applySnapshotInProgress 指标为 1 期间,normal entry 不推进。恢复窗口运维见第 14 篇。


参考资料

规范 / 源码(A)

站内对照

争论 / 开放问题


上一篇bbolt 后端

下一篇写入路径深读

读完这篇,下一步读什么

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


By .