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

【etcd】Watch 机制:watchableStore、synced/unsynced 与 ErrCompacted

文章导航

分类入口
distributedkubernetes
标签入口
#etcd#watch#watchableStore#mvcc#ErrCompacted#v3.5.33

目录

第 8 篇 分列线性一致读与 Serializable 读;distributed/50 百科 讲过 Watch 与 Revision 直觉。生产排障里,apiserver 或 controller 常见误判是「Put 成功但下游没反应」——要先问:Watch 落在 Watch 轴的 synced、unsynced 还是 victims;startRev 是否已被 compaction 裁掉;客户端消费是否背压到 chanBufLen

本篇钉 watchableStore 内核:synced/unsynced 分组、syncWatchersLoop/syncVictimsLoop 双 goroutine、notify 与 channel 满时的 victims,以及 CompactRevisionErrCompacted 的重同步策略。不写未测 Watch 延迟数字。

本篇在系列中的位置

篇目 核心内容
第 8 篇 · 读路径与一致性 ReadIndex、Serializable、Lease read
第 9 篇 · Watch 机制 watchableStore、synced/unsynced、ErrCompacted
第 10 篇 · Lease 与 KeepAlive grant/KeepAlive/checkpoint、Leader 切换
系列目录 五轴、阅读路径

版本锚定:etcd v3.5.33(源码 tag v3.5.33)。机制对齐 server/mvcc/watchable_store.gowatcher.gowatcher_group.go @ etcd-io/etcd v3.5.33无真实集群则不粘贴伪造 Watch 事件流或 etcdctl watch 输出。


一、watchableStore 在 Apply 链上的位置

MVCC store 负责 Revision 与 bbolt 读写;watchableStore 在其外包一层 Watch 状态机(watchable_store.go):

type watchableStore struct {
    *store
    victims  []watcherBatch
    victimc  chan struct{}
    unsynced watcherGroup  // 追赶历史 revision
    synced   watcherGroup  // 已与 store 进度对齐
}

Apply 写路径在 storeTxnWrite.End() 提交后调用 notify(rev, evs),只遍历 synced 组里匹配 key/前缀的 watcher。Lease 过期触发的批量 Delete 同样走 SetRangeDeleter 回调,保证 Lease 轴删除也会推送 DELETE 事件(第 10 篇)。

第 6 篇 的分工:Raft commit → apply → MVCC 写 → notify 是 Watch 轴唯一热路径;unsynced 追赶不阻塞 Apply,由后台 loop 分批完成。

flowchart TD
  apply["Raft Apply → MVCC Put/Delete"] --> notify["watchableStore.notify"]
  notify --> synced["synced watchers → ch"]
  apply --> rev["currentRev++"]
  loop["syncWatchersLoop 100ms"] --> unsynced["unsynced → 读 bbolt 历史"]
  unsynced --> synced
  chfull["ch 满"] --> victims["victims + syncVictimsLoop"]
  victims --> unsynced

二、建立 Watch:synced 与 unsynced 的判据

watch() 根据 startRevcurrentRev 决定分组(watchable_store.go):

WatchStream 多路复用:一个 gRPC 双向流可挂多个 watch_idwatcher.go)。chanBufLen 默认为 128(测试可改);多个 watcher 可共享同一 ch,背压会连带影响同流上所有 Watch。

Progress 通知RequestProgress / RequestProgressAll 仅在 watcher 处于 synced 且 rev >= startRev 时发送空事件、Revision 为当前 store revision——用于客户端确认「已追到 head」,apiserver watch cache 重连时常用(第 13 篇指针)。


三、syncWatchers 与 victims:背压落格

3.1 历史追赶

syncWatchersLoop 默认每 100ms 唤醒(有剩余 unsynced 且上一轮有进展时,等待时间可缩至上一轮 syncDuration,避免长期霸占 store 锁)。每轮最多处理 maxWatchersPerSync = 512 个 unsynced watcher:

  1. choose() 取批次,计算 minRev
  2. minRev < compactMainRev,该 watcher 需要的 revision 已被 compaction 裁掉(见第四节);
  3. 否则 UnsafeRange 读 bbolt,经 kvsToEvents 过滤 key 后 send
  4. 成功则移入 synced;若 eb.moreRev != 0 表示单批未读完,留在 unsynced。

3.2 victims

notifysyncWatchers 调用 watcher.send() 时,若目标 ch 非阻塞发送失败

排障口令etcd_debugging_mvcc_slow_watcher_total(Prometheus slowWatcherGauge)上升 → 客户端消费慢或同流 Watch 过多,不是 Raft lag。与 MVCC 轴 database space exceeded 分列(第 12 篇)。

症状 更可能落格 不像
Put 成功、synced Watch 无事件 victims 或 filter 掉事件 Raft 未 commit
重连后大量历史事件、内存涨 unsynced 从旧 startRev 追赶 compaction 本身
Watch 收到 CompactRevision startRev < compactRev 网络瞬断 alone
同 Pod 多 Watch 全断 共享 ch 背压 单 key 未写入

四、ErrCompacted 与 CompactRevision

MVCC compaction 更新 compactMainRev 后,早于该 revision 的 Range/Watch 历史不可再读。store 层错误为:

mvcc: required revision has been compacted

Watch 路径不走 Range 错误,而在 watcher_group.chooseAll:当 w.minRev < compactRev 时,向 ch 发送:

WatchResponse{WatchID: w.id, CompactRevision: compactRev}

并设 w.compacted = true、从 watcher 组删除。客户端(client/v3)将其映射为 rpctypes.ErrCompacted。若此时 ch 已满,发送失败则 下轮 syncWatchers 重试(注释明确:避免 compaction 通知丢失在 channel 背压下被静默跳过)。

客户端策略(机制事实,非 SLA):

  1. 记录最后成功事件的 mod_revision / header revision;
  2. 收到 ErrCompactedList 全量或按 prefix 重建本地缓存,再以 Rev=current 重新 Watch;
  3. 不要把「compaction 间隔内断线重连」假设为一定能从旧 rev 续传——auto-compaction 与 manual compact 都会裁历史(第 12 篇)。

Restore 后所有 synced watcher 会被标记 restore=true 并移入 unsynced,需重新追赶;这与 Leader 切换后 apiserver 大规模 resync 的成本相关(第 13 篇)。


五、与读写一致性的交界


六、谱系与开放问题

ZK 一次性 Watch(Hunt et al. 2010)→ 重注册间隙丢事件
  → etcd v3:持久 Watch + Revision 回溯
  → watchableStore:synced 热路径 + unsynced 扫盘 + victims 背压
  → compaction 与 ErrCompacted:历史窗口 vs 磁盘 SLO 的硬 trade-off

开放问题(工程判断):K8s 控制面在 apiserver watch cache + etcd Watch 双层的 端到端 resync SLO 尚无与 compaction 间隔统一的公开建模;chanBufLen=128 对超大规模单流 Watch 是否足够,官方 issue #11906 讨论过背压语义——本篇无实测,不写成调参结论。

本篇不写什么:未跑的 Watch 延迟/QPS;apiserver storage 实现全书(第 13 篇);伪造 etcdctl watch 输出。


参考资料

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

论文 / 对照(A/B)

站内对照


上一篇读路径与一致性

下一篇Lease 与 KeepAlive

读完这篇,下一步读什么

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


By .