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

【kube-apiserver】Watch 路径(服务端):长连接、410 Gone 与 timeout

文章导航

分类入口
kubernetesdistributed
标签入口
#kubernetes#apiserver#watch#410-gone#bookmark#timeout#informer#v1.30.3

目录

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.gowatchHandler)。Watch handler 主要做:

  1. 解析 ListOptions(resourceVersion、labelSelector、fieldSelector、timeoutSeconds);
  2. 调用 registry 的 Watch() → storage 的 Watch()
  3. 如果底层 storage 是 Cacher,走 cacher.Watch();如果 Cacher 不可用或配置直连,走 etcd3 store.Watch()(直接在 etcd 上建立 watcher);
  4. 把 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 的行为


三、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 循环:

边界说明:本篇(服务端)的 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)

站内对照


上一篇List、Pagination 与一致性 List

下一篇Admission 链概览:内置插件顺序与 webhook 边界

读完这篇,下一步读什么

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

2026-08-28 · kubernetes / distributed

【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 轴。


By .