apiserver 的 Watch 是 Kubernetes
控制面事件分发的主干。控制器、kubelet、kubectl
都依赖这条长连接。生产里最常见的混淆:410 Gone 是
apiserver 发给客户端的——它可能来自 cacher ring buffer
滑出窗口,也可能来自底层 etcd ErrCompacted
上浮,但两者的成因和应对完全不同;timeout 也分
--request-timeout 引起的服务端主动关闭与 APF
流控限速(第 12 篇)。
本篇钉 v1.30.3 服务端 Watch 路径:REST 层如何建立 Watch、cacher/etcd3 如何分工、resourceVersion=0 与具体 RV 的行为差、410 Gone 的来源分列、bookmark 与 timeout 对连接生命周期的影响,以及与 client-go Informer 的边界(不写 client-go 内核全书)。
本篇在系列中的位置
篇目 核心内容 第 5 篇 · Watch cache / cacher dispatch、bookmark、ring buffer 第 6 篇 · List / Pagination continue token、一致性语义 第 7 篇 · Watch 路径(服务端) 长连接、410 Gone、timeout 第 12 篇 · APF 与 max-in-flight 流控、排队、504 系列目录 五轴、阅读路径
版本锚定:Kubernetes v1.30.3(源码 tag
v1.30.3)。机制钉staging/src/k8s.io/apiserver/pkg/storage/cacher/、pkg/endpoints/handlers/watch.go(generic apiserver watch handler)@ kubernetes/kubernetes v1.30.3。etcd 侧 ErrCompacted 见 etcd/09(v3.5.33)。无真实集群则不粘贴伪造 Watch 事件流或 apiserver metrics 截图。
一、Watch 建立:从 REST 到 cacher
客户端发送
GET /api/v1/namespaces/default/pods?watch=true&resourceVersion=...,REST
handler 识别 watch=true 参数后进入 Watch
路径(pkg/endpoints/handlers/get.go →
watchHandler)。Watch handler 主要做:
- 解析
ListOptions(resourceVersion、labelSelector、fieldSelector、timeoutSeconds); - 调用 registry 的
Watch()→ storage 的Watch(); - 如果底层 storage 是 Cacher,走 cacher.Watch();如果 Cacher 不可用或配置直连,走 etcd3 store.Watch()(直接在 etcd 上建立 watcher);
- 把 Watch channel 对接成 HTTP 长连接(chunked transfer-encoding),逐事件序列化后 flush 给客户端。
flowchart TD
req["GET /pods?watch=true\n&resourceVersion=RV"]
handler["watchHandler\n(endpoints/handlers/get.go)"]
reg["registry.Watch()"]
cacher["Cacher.Watch()\n返回 cacheWatcher"]
etcd3watch["etcd3.Watch()\n(降级/直连)"]
eventloop["serveWatch 事件循环\n(flush JSON / protobuf)"]
req --> handler
handler --> reg
reg --> cacher
reg -.->|"Cacher 不可用时"| etcd3watch
cacher --> eventloop
etcd3watch --> eventloop
二、resourceVersion 语义:0 vs 具体 RV
Watch 请求中的 resourceVersion
决定事件窗口的起点:
resourceVersion |
含义 | cacher 行为 |
|---|---|---|
"" (未指定) |
从当前时刻起,不回放历史 | 从 watchCache.resourceVersion
起发送新事件 |
"0" |
从任意已知状态起(可从 cache 开始) | 先发送 cacher 内存 store 的全量 ADDED
事件,再转入增量 Watch |
具体 RV(如 "4200") |
从该 RV 之后的事件开始 | 查 ring buffer 是否包含该 RV 之后的事件 |
rv="0"
的特殊语义:Reflector 通常使用 rv="0"
发起初始 Watch——apiserver 会先发送一批 ADDED
事件(全量快照),再切换到增量推送。这与 rv=""
不同(rv=""
不发历史快照,直接推增量)。rv="0" 的快照发送在
cacher 侧通过遍历 watchCache.store 完成,与
etcd 无关。
具体 RV 的行为:
- 若该 RV 在 ring buffer 范围内:cacher 从 ring buffer
回放
[RV+1, current]区间的事件,再转增量。 - 若该 RV 早于 ring buffer 最旧事件(滑出窗口):
- 若该 RV 仍在 etcd 的 compaction 窗口内:cacher 可能降级到 etcd 路径重新建立 Watch,或直接返回 410 Gone(取决于版本与配置)。
- 若该 RV 已被 etcd compaction:etcd ErrCompacted 上浮为 apiserver 410 Gone(见第三节)。
- 若该 RV 大于 cacher 当前
watchCache.resourceVersion(未来 RV):cacher 将该 Watch 挂起等待,直到 cache 追上该 RV 或超时。
三、410 Gone 的来源分列
客户端收到
WatchEvent{Type: ERROR, Status: {Code: 410, Reason: "Expired"}}
时,有两条不同的成因链:
来源 A:cacher ring buffer 滑出窗口
cacher 无法从内存回放指定 RV 之后的事件(ring buffer 容量不足或写入速率过快),且无法安全降级:向客户端发送 ERROR 事件,HTTP status 410。
来源 B:etcd ErrCompacted 上浮
etcd 侧 compaction 裁掉了 cacher 内部 listerWatcher
依赖的 revision;cacher 自身 Watch 被 etcd 中断(收到
CompactRevision),cacher 进入重新 List + 重建
Watch 的流程(reflector.ListAndWatch
重启)。在此期间,已注册的 cacheWatcher 收到 ring buffer
无效信号,被关闭并向客户端推送 410。
分列方法(机制判断,非实测):
| 症状 | 倾向 A(cacher ring buffer) | 倾向 B(etcd ErrCompacted) |
|---|---|---|
| 集群写入速率突然升高 | 可能;ring buffer 被覆盖 | 不典型 |
| etcd compaction 刚执行 | 不典型 | 可能;etcd Watch 被裁 |
| 多资源类型 Watch 同时 410 | 不典型(各资源 cacher 独立) | 可能(共享 etcd compaction 事件) |
etcd_debugging_mvcc_slow_watcher_total
上升 |
不典型 | 不直接相关 |
客户端(client-go Reflector)对 410 的标准处理:重新
List(rv="0") 全量同步,再以当前 RV 续接
Watch。详见第 5 篇中 List 风暴场景。
四、timeout 与连接生命周期
Watch 连接的生命周期由以下因素决定:
1.
timeoutSeconds(客户端指定):Watch
请求可在 ListOptions.TimeoutSeconds
中指定超时;到期后 apiserver 主动关闭
Watch,客户端需要重新建连。不指定时使用 apiserver
默认值(--min-request-timeout,默认
1800s,具体行为见 pkg/server/config.go)。
2.
--request-timeout:apiserver
进程级全局请求超时;Watch 是长连接,此值对 Watch
不直接适用(Watch 使用 timeoutSeconds
语义),但影响其他请求类型。
3. 客户端主动关闭:HTTP 客户端断开 TCP 连接,apiserver 检测到后清理对应 cacheWatcher。
4. APF 流控(第 12 篇):Watch 建立时如果 APF 排队时间过长,在请求入队超时前会被拒绝(429 Too Many Requests),而非 504。建立后的 Watch 不受 APF 每次事件影响——流控只在请求建立阶段介入。
bookmark 对 timeout 的影响:bookmark 事件定期推送(默认约 60s),使服务端知道客户端仍活跃;某些负载均衡器(ELB/L7 代理)在长时间无数据时会断开连接,bookmark 的作用之一就是保持连接活跃。
sequenceDiagram
participant Client as client-go Reflector
participant apiserver
participant cacher
Client->>apiserver: GET /pods?watch=true&rv=RV
apiserver->>cacher: Watch(RV)
cacher-->>apiserver: cacheWatcher channel
loop 事件推送
cacher->>apiserver: WatchEvent(MODIFIED, ...)
apiserver->>Client: WatchEvent JSON chunk
end
Note over apiserver,Client: 60s 无事件
cacher->>apiserver: WatchEvent(BOOKMARK, rv=current)
apiserver->>Client: WatchEvent BOOKMARK
Note over Client: timeoutSeconds 到期
apiserver->>Client: 关闭连接
Client->>apiserver: 重新 GET /pods?watch=true&rv=current
五、与 client-go Informer 的边界
client-go Informer 在客户端侧维护 List+Watch 循环:
- List:Informer 启动时调用
List(rv="0")获取全量快照(走 cacher 内存路径); - Watch:使用 List 返回的 RV 发起 Watch,续接增量;
- 410 处理:收到 410 后重新 List,重建本地缓存。
边界说明:本篇(服务端)的 Watch 路径止于 apiserver 的 Watch handler + cacher 分发。client-go Informer 的 resync 机制、DeltaFIFO、EventHandler 注册、SharedInformerFactory 等不在本篇展开——这些是控制器框架内核,与 apiserver 的接口边界在 Watch 协议层(WatchEvent JSON/protobuf)。
APF 与 Watch 并发控制的交互见第 12 篇;排障五轴(Watch 轴口令)见第 15 篇。
六、开放问题
Watch 连接数与 cacher 内存压力:每个
cacheWatcher 有独立的 input channel(默认 buffer 100
事件)。大规模集群中,若同一资源类型有数千个 Watch
连接(多控制器 + 多 namespace),cacher 内存占用可观。K8s
社区在 1.25+ 引入 WatchList(Streaming
List,KEP-3157)作为替代方向,目的是减少 List
的全量数据传输,同时在服务端控制 Watch
连接生命周期——但该功能在 v1.30.3 仍处于 feature
gate(WatchList=true)阶段,生产稳定性尚待观察。
本篇不写:未实测的 Watch 延迟数字;client-go Informer 内核全书;伪造 Watch 输出。
参考资料
规范 / 源码(A)
- kubernetes/kubernetes
v1.30.3:staging/src/k8s.io/apiserver/pkg/endpoints/handlers/get.go(watchHandler);pkg/storage/cacher/cacher.go(Watch 实现);pkg/storage/etcd3/watcher.go - Kubernetes API Concepts · Efficient Watch(线索;以 v1.30.3 源码为准)
- KEP-3157: Watch List(v1.30.3 feature gate;未稳定)
- etcd/09 · Watch 机制(ErrCompacted 路径)
站内对照
- 第 5 篇 · Watch cache / cacher
- 第 6 篇 · List / Pagination
- 第 12 篇 · APF 与 max-in-flight
- etcd/12 · Compaction 与 SLO
下一篇:Admission 链概览:内置插件顺序与 webhook 边界
读完这篇,下一步读什么
优先读同系列或同问题的下一篇,把单篇消费变成主题集群。
【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。
【kube-apiserver】Watch cache / cacher:dispatch、bookmark 与穿透 etcd
钉 Kubernetes v1.30.3 cacher 对 storage.Interface 的包装:watchCache 滑动窗口与 Ready 门、bookmark 推送路径、cache miss 打穿 etcd 的触发条件,以及 List 风暴成因与 watch cache SLO 与 compaction 间隔之间的开放问题。
【kube-apiserver】排障五轴:Storage、Watch、Admission、Auth、APF
按 Storage/Watch/Admission/Auth/APF 五轴做症状否证;给出完整五轴命令表与症状→轴映射(504、410、401、403、webhook 超时、List 风暴、OOM);说明 apiserver_request_duration_seconds 等核心 metrics 语义;并提供决策树:何时穿透到 etcd/15,何时留在 apiserver 轴。
【kube-apiserver】控制面全景:缺口、五轴坐标系与 16 篇路线
相对 etcd/13、distributed/50、k8s-network 补齐 kube-apiserver 生产内核缺口;以 Storage/Watch/Admission/Auth/APF 五轴为坐标系定义 16 篇阅读路线;版本锚定 Kubernetes v1.30.3。