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

【kube-apiserver】Watch cache / cacher:dispatch、bookmark 与穿透 etcd

文章导航

分类入口
kubernetesdistributed
标签入口
#kubernetes#apiserver#watch-cache#cacher#bookmark#storage#resourceVersion#v1.30.3

目录

第 4 篇 钉了 resourceVersion 与 etcd mod revision 的映射;etcd/09 钉了 watchableStore 的 synced/unsynced 分组。生产里「Pod 更新成功但 controller 没收到 Watch 事件」常被误判为 etcd 问题——实际上可能是 cacher 的 Ready 门还没打开,或客户端 resourceVersion 太旧让 cacher 决定直穿 etcd,而 etcd 侧完全正常。

本篇钉 staging/src/k8s.io/apiserver/pkg/storage/cacher/ 的核心结构:watchCache 滑动窗口、cacher 的 Ready 门(启动同步)、dispatch 热路径、bookmark 推送,以及 cache miss 回退到 etcd3 的判据。

本篇在系列中的位置

篇目 核心内容
第 4 篇 · resourceVersion 映射 mod revision、continue 语义
第 5 篇 · Watch cache / cacher dispatch、bookmark、cache miss
第 6 篇 · List / Pagination continue token、etcd Range 成本
系列目录 五轴、阅读路径

版本锚定:Kubernetes v1.30.3(源码 tag v1.30.3)。机制钉 staging/src/k8s.io/apiserver/pkg/storage/cacher/ @ kubernetes/kubernetes v1.30.3。etcd 侧 Watch 机制见 etcd/09(v3.5.33)。无真实集群则不粘贴伪造 Watch 事件流或 apiserver metrics 截图。


一、cacher 在 storage.Interface 链上的位置

storage.Interfacestaging/src/k8s.io/apiserver/pkg/storage/interfaces.go)定义 CreateGetListWatchGuaranteedUpdate 等契约。etcd3 store(pkg/storage/etcd3/)是底层实现;Cacherpkg/storage/cacher/cacher.go)在其外包一层缓存,仍实现同一 storage.Interface

flowchart TD
  client["REST handler / List / Watch"]
  cacher["Cacher\n(storage.Interface)"]
  wc["watchCache\n滑动窗口"]
  etcd3["etcd3 store\n(storage.Interface)"]
  etcd["etcd v3.5.x"]

  client -->|"Watch / List"| cacher
  cacher -->|"cache hit"| wc
  cacher -->|"cache miss / not ready"| etcd3
  etcd3 --> etcd
  etcd -->|"Watch stream"| cacher

Cacher 启动时向 etcd3 发起一个持久的 Watch(内部称 listerWatcher),将所有事件写入 watchCache。对上层的多个客户端 Watch 请求,Cacher 在内存中 多路分发(dispatch),不为每个客户端单独向 etcd 开一条 Watch 流——这是 watch cache 相对直连 etcd 的核心差异。


二、watchCache:滑动窗口与容量

watchCachecacher/watch_cache.go)是一个固定容量的环形 buffer,保存最近的 WatchCacheEvent(含 Object、Type、ResourceVersion)。

type watchCache struct {
    capacity   int              // 默认 100,可由 --watch-cache-sizes 按 resource 调整
    cache      []*watchCacheEvent
    startIndex int
    endIndex   int
    store      cache.Store      // 全量 object 的 local store
    resourceVersion uint64
    // ...
}

容量语义endIndex - startIndex 就是当前窗口中保留的事件数;新事件超过容量时,最老的事件被覆盖(Ring Buffer 语义)。一次请求需要回放的 startRev 如果已落出窗口,Cacher 就无法从 watchCache 满足该 Watch,需要判断是返回 410 Gone 还是重新 List。

store(local object store):独立于 ring buffer,保存当前全量对象快照(cache.Store 接口),供 List 使用。每次事件进入 watchCache 后,store 也会同步 Apply/Delete,保证 watchCache.resourceVersionstore 中对象版本一致。


三、Ready 门:启动同步与阻塞

Cacher 创建后不立即开放服务,而是先完成一次对 etcd 的全量 List(内部 reflector.ListAndWatch),将所有对象装入 watchCache.store,再打开 Ready 门。

type Cacher struct {
    ready *ready    // ready.wait() 用于阻塞未完成初始同步的请求
    // ...
}

ready 是一个 channel-based 信号量;ListWatch 请求在 Ready 打开前会在此阻塞(或超时返回错误)。这个机制防止客户端在 cache 还未同步完毕时得到空列表——List 风暴的一个触发点:apiserver 重启后大量 controller 同时 List 时,如果 Ready 门延迟打开,所有请求排队等待,一旦打开瞬间并发打 etcd。

生产上的表象:apiserver 进程刚启动后一段时间内,kubectl get pods 可能延迟明显,apiserver 日志里可见 cacher not ready 类似字样。


四、dispatch:事件的内存多路分发

etcd Watch 事件到达 Cacher 后,经 processEvent

  1. 写入 watchCache ring buffer;
  2. 更新 watchCache.store(全量快照);
  3. 调用 cacher.dispatchEvent,遍历已注册的 cacheWatcher,逐个投递匹配的事件。
flowchart LR
  etcdEvent["etcd Watch event"] --> processEvent["watchCache.processEvent"]
  processEvent --> ring["ring buffer append"]
  processEvent --> store["store.Update/Delete"]
  processEvent --> dispatch["cacher.dispatchEvent"]
  dispatch --> w1["cacheWatcher 1\n(client Watch)"]
  dispatch --> w2["cacheWatcher 2"]
  dispatch --> wn["cacheWatcher N"]

cacheWatcher 的背压:每个 cacheWatcher 有自己的 input channel(input chan *watchCacheEvent);dispatch 向 channel 非阻塞发送,若 channel 满则该 watcher 进入 nonblockingCh 的重试路径,最终可能被强制关闭并返回客户端一个错误(类似 etcd watchableStore 的 victims 机制,见 etcd/09 § 三)。


五、bookmark 与 progress notifications

BookmarkBOOKMARK 事件类型,API 资源版本标记)是 Watch 协议的进度通知:当 Watch 流上一段时间无新对象事件时,服务端向客户端发送一个 WatchEvent{Type: BOOKMARK, Object: ...},Object 只携带 resourceVersion,不含 object body。

Cacher 定期(bookmarkFrequency,默认约 60s,可通过 feature gate 调整)向满足条件的 cacheWatcher 推送 bookmark:

客户端(client-go Reflector)使用 bookmark 更新本地缓存的 ResourceVersion,使后续 Watch 重连可以从该 RV 续接,而不必从 RV=0 重新全量 List。bookmark 对于减少 List 风暴有关键作用:若 RV 能持续推进,客户端断线重连后 Cacher 可直接从 ring buffer 回放,不用打穿到 etcd。

bookmark 与 etcd/09RequestProgress 功能对应——etcd 层的 progress notification 传递到 cacher 后转发给客户端 bookmark;两者共同维持端到端的 resourceVersion 推进。


六、cache miss:穿透 etcd 的判据

Cacher 在 Watch 请求到来时,根据 resourceVersion(startRev)决定能否从 ring buffer 服务:

条件 处置
rv = 0 从当前 cache 快照启动 Watch,直接服务
rv 在 ring buffer 窗口内 回放 ring buffer + 后续增量
rv 早于 ring buffer 最旧 rev(已滑出窗口) 无法回放;若该 rv 还在 etcd compaction 范围内,向客户端返回 410 Gone;否则直接返回错误
rv 晚于当前 watchCache.resourceVersion(未来的 RV) 阻塞等待,直到 cache 追上或超时
cacher 尚未 Ready 等待 Ready 门,或超时

List 的 cache miss 路径稍有不同:List(rv="") 要求一致性读(当前 etcd 状态),Cacher 直接透传 etcd3 store 的 List;List(rv="0") 则可从 watchCache.store 服务,不需要打穿 etcd(第 6 篇详述)。

cache miss 打穿的成本:若大量 Watch 请求携带非零且已滑出窗口的 RV,每次都要退回 etcd 执行 Range 或新建 etcd Watch,造成 etcd 读压力突增——这是 Ring Buffer 容量、bookmark 频率与 compaction 间隔三者联动的 SLO 关键路径


七、List 风暴:两种典型触发场景

场景一:apiserver 重启后 Ready 门延迟

etcd 数据量大时,Cacher 初始全量 List 耗时较长,Ready 门晚开。所有 controller 堆积在 Ready 阻塞队列,Ready 打开后同时触发 List 请求,apiserver → etcd Range 瞬间并发高。

场景二:ring buffer 滑出窗口导致批量 resync

--watch-cache-sizes 容量设置过小(默认 100),高写入速率资源(如 Event)短时间内覆盖 ring buffer,部分 cacheWatcher 的 startRev 滑出窗口,被关闭并收到 410 Gone。client-go Reflector 收到 410 后执行重新 List,若多个控制器同时触发,形成风暴。

两种场景的共同出口:增大 --watch-cache-sizes(针对高写速率资源)、保证 bookmark 频率与 compaction 间隔匹配(使客户端持续推进 RV 而不依赖远古 RV)、以及 APF 流控(第 12 篇)对 List 操作单独限速。


八、谱系与开放问题

ZK 一次性 Watch(Hunt et al. ATC 2010)→ 重注册间隙丢事件
  → etcd v3:持久 Watch + Revision 回溯(见 etcd/09)
  → Kubernetes watch cache:多路分发 + 内存 ring buffer,减少 etcd Watch 压力
  → bookmark:近似 etcd RequestProgress,维持端到端 RV 推进

开放问题:watch cache SLO 与 compaction 间隔之间尚无与 K8s 官方联合建模的标准;ring buffer 容量、bookmark 频率、etcd auto-compaction 保留窗口三者构成一个隐式 SLO 关系——当 compaction 间隔小于客户端重连后能续接的 bookmark-RV 时,可能触发非预期的 List 风暴。etcd/12 §5.1 记录了 compaction 与 Watch SLO 的边界开放问题;apiserver 侧到第 16 篇收束。

本篇不写:未实测的 ring buffer 大小与 List 延迟关系;client-go Reflector 内部实现全书(第 7 篇边界);伪造 apiserver 指标截图。


参考资料

规范 / 源码(A)

论文 / 对照(A/B)

站内对照


上一篇resourceVersion 与 Revision 映射

下一篇List / Pagination 与一致性 List

读完这篇,下一步读什么

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


By .