第 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.Interface(staging/src/k8s.io/apiserver/pkg/storage/interfaces.go)定义
Create、Get、List、Watch、GuaranteedUpdate
等契约。etcd3
store(pkg/storage/etcd3/)是底层实现;Cacher(pkg/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:滑动窗口与容量
watchCache(cacher/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.resourceVersion 与
store 中对象版本一致。
三、Ready 门:启动同步与阻塞
Cacher 创建后不立即开放服务,而是先完成一次对 etcd 的全量
List(内部
reflector.ListAndWatch),将所有对象装入
watchCache.store,再打开 Ready
门。
type Cacher struct {
ready *ready // ready.wait() 用于阻塞未完成初始同步的请求
// ...
}ready 是一个 channel-based
信号量;List、Watch 请求在
Ready
打开前会在此阻塞(或超时返回错误)。这个机制防止客户端在
cache 还未同步完毕时得到空列表——List
风暴的一个触发点:apiserver 重启后大量 controller
同时 List 时,如果 Ready
门延迟打开,所有请求排队等待,一旦打开瞬间并发打 etcd。
生产上的表象:apiserver
进程刚启动后一段时间内,kubectl get pods
可能延迟明显,apiserver 日志里可见
cacher not ready 类似字样。
四、dispatch:事件的内存多路分发
etcd Watch 事件到达 Cacher 后,经
processEvent:
- 写入
watchCachering buffer; - 更新
watchCache.store(全量快照); - 调用
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
Bookmark(BOOKMARK
事件类型,API 资源版本标记)是 Watch 协议的进度通知:当
Watch 流上一段时间无新对象事件时,服务端向客户端发送一个
WatchEvent{Type: BOOKMARK, Object: ...},Object
只携带 resourceVersion,不含 object body。
Cacher 定期(bookmarkFrequency,默认约
60s,可通过 feature gate 调整)向满足条件的 cacheWatcher
推送 bookmark:
- watcher 已追上当前
watchCache.resourceVersion(synced 状态); - 距上次 bookmark 超过
bookmarkFrequency。
客户端(client-go Reflector)使用 bookmark 更新本地缓存的
ResourceVersion,使后续 Watch 重连可以从该 RV
续接,而不必从 RV=0 重新全量 List。bookmark 对于减少
List 风暴有关键作用:若 RV
能持续推进,客户端断线重连后 Cacher 可直接从 ring buffer
回放,不用打穿到 etcd。
bookmark 与 etcd/09 的
RequestProgress 功能对应——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)
- kubernetes/kubernetes
v1.30.3:staging/src/k8s.io/apiserver/pkg/storage/cacher/cacher.go;cacher/watch_cache.go;storage/interfaces.go;storage/etcd3/ - Kubernetes API Concepts · Efficient Watch(线索;以 v1.30.3 源码为准)
- etcd/09 · Watch 机制(watchableStore 侧;v3.5.33)
- etcd/12 · Compaction 与 SLO
论文 / 对照(A/B)
- Hunt, P., et al., ZooKeeper: Wait-free coordination for Internet-scale systems, USENIX ATC 2010(一次性 Watch 对照)
站内对照
- 第 4 篇 · resourceVersion 映射
- 第 6 篇 · List / Pagination
- 第 7 篇 · Watch 路径(服务端)
- 第 12 篇 · APF 与 max-in-flight
上一篇:resourceVersion 与 Revision 映射
下一篇:List / Pagination 与一致性 List
读完这篇,下一步读什么
优先读同系列或同问题的下一篇,把单篇消费变成主题集群。
kube-apiserver / Kubernetes 控制面内核:storage、cacher、Admission 与 APF
补齐 etcd 系列 K8s 耦合篇之上的 kube-apiserver 生产内核:storage.Interface、watch cache、resourceVersion、Admission/Webhook、APF,并以排障与相对 etcd 的分层收束。
【kube-apiserver】控制面全景:缺口、五轴坐标系与 16 篇路线
相对 etcd/13、distributed/50、k8s-network 补齐 kube-apiserver 生产内核缺口;以 Storage/Watch/Admission/Auth/APF 五轴为坐标系定义 16 篇阅读路线;版本锚定 Kubernetes v1.30.3。
【kube-apiserver】storage.Interface 与 etcd3:codec、prefix 与 CRUD 路径
拆解 kube-apiserver 的 storage.Interface 契约与 etcd3 实现:codec 序列化、pathPrefix/resourcePrefix、value.Transformer 加密边界、GuaranteedUpdate 乐观并发,以及 Watch 到 etcd3 的完整路径。版本锚定 Kubernetes v1.30.3 / etcd v3.5.33。
【kube-apiserver】resourceVersion 与 Revision 映射:mod revision、continue 与一致性读期望
钉 Kubernetes resourceVersion 字段与 etcd mod revision 的对应关系;分析 continue token 的分页语义与成本;说明不同 List 路径的一致性期望差异;以及 410 Gone 与 ErrCompacted 的分列。版本锚定 Kubernetes v1.30.3 / etcd v3.5.33。