bigywater.tech
Kubernetes · 13

watch 语义:从某个 resourceVersion 开始能收到什么事件

watch 是所有控制器的地基。从一个 resourceVersion 开始 watch 到底能收到什么?版本太旧时发生什么?这篇用真实的事件流把每种情形都跑出来,包括手动压缩 etcd 也没能让旧版本失效这个出乎意料的结果。

4 个 / 0 个
从 b 创建时的版本 watch 与从当前版本 watch 各自收到的事件数
410 Expired
too old resource version: 14929 (15739),写入累计 7200 次、过去 92 秒后出现
DELETED
对象标签改成 app=y 后,watch labelSelector=app=x 收到的事件

从某个版本开始

实测 · 本机执行输出
  watch?resourceVersion=14898(b 创建时的版本)收到 4 个事件:MODIFIED a rv=14899 | DELETED b rv=14900 | ADDED c rv=14901 | MODIFIED c rv=14902
  watch?resourceVersion=14902(当前版本)收到 0 个事件
  同样从 14902 开始,watch 期间创建再删除 live:ADDED live rv=14914 | DELETED live rv=14915

依次创建 a、b,修改 a,删除 b,创建 c,修改 c。从 b 创建时的版本(14898)开始 watch,恰好收到它之后的 4 个事件:MODIFIED a、DELETED b、ADDED c、MODIFIED c,每个事件里的 resourceVersion 就是那次修改的版本。从当前版本开始 watch 一个事件也没有,直到有新的变化才会收到(创建再删除 live 得到 ADDED、DELETED)。

不指定版本

实测 · 本机执行输出
  不带 resourceVersion:ADDED a rv=14899 | ADDED c rv=14902 | ADDED kube-root-ca.crt rv=14896
  resourceVersion=0:ADDED a rv=14899 | ADDED c rv=14902 | ADDED kube-root-ca.crt rv=14896
  sendInitialEvents=true(流式 list):ADDED a rv=14899 | ADDED c rv=14902 | ADDED kube-root-ca.crt rv=14896 | BOOKMARK  rv=14923
    BOOKMARK 的注解:{'k8s.io/initial-events-end': 'true'}
  (b 在 resourceVersion=0 的结果里不存在:被删除的对象不会以事件的形式“补发”)

不带 resourceVersion 或写 0:服务端先把现有对象当作 ADDED 发一遍,再接实时事件。已经删掉的 b 不会补发 DELETED——所以“从头 watch”拿不到历史,只能拿到当前状态。较新的 sendInitialEvents=true 把 list 和 watch 合成一个流,初始对象发完后有一个注解为 k8s.io/initial-events-end: true 的 BOOKMARK,告诉客户端“初始状态完整了”。

版本太旧

实测 · 本机执行输出
  记下 burst 的 resourceVersion=14929;再写 1200 次(12 秒);手动压缩 etcd 到 revision 16147(compacted revision 16147)
  压缩之后从 14929 开始 watch:收到 1199 个事件,第一个是 MODIFIED rv=14930——压缩没有让它失效,事件是从 apiserver 的 watch 缓存里补发的
  累计写入 2200 次(距 14929 已过去 30 秒):watch 收到 2199 个事件,第一个是 MODIFIED
  累计写入 3200 次(距 14929 已过去 42 秒):watch 收到 3199 个事件,第一个是 MODIFIED
  累计写入 4200 次(距 14929 已过去 55 秒):watch 收到 4199 个事件,第一个是 MODIFIED
  累计写入 5200 次(距 14929 已过去 67 秒):watch 收到 5199 个事件,第一个是 MODIFIED
  累计写入 6200 次(距 14929 已过去 80 秒):watch 收到 6199 个事件,第一个是 MODIFIED
  累计写入 7200 次(距 14929 已过去 92 秒):watch 收到 1 个事件,第一个是 ERROR
  出错时服务端发来的事件:{"type": "ERROR", "object": {"kind": "Status", "apiVersion": "v1", "metadata": {}, "status": "Failure", "message": "too old resource version: 14929 (15739)", "reason": "Expired", "code": 410}}
  恢复办法:重新 list 得到 resourceVersion=22275,再从它 watch:MODIFIED burst rv=22279

这一节的结果和直觉不一样:写 1200 次后手动把 etcd 压缩到当前 revision,从旧版本 watch 仍然收到全部 1199 个事件——旧事件是从 apiserver 自己的 watch 缓存里补发的,压缩没有让它失效。继续写,累计 6200 次(过去 80 秒)还行,累计 7200 次(92 秒)才得到一个 ERROR 事件:code: 410、reason: Expired、too old resource version: 14929 (15739),括号里是缓存里最老的版本。恢复办法是重新 list 拿到新的 resourceVersion,再从它 watch。

选择器下的事件

实测 · 本机执行输出
  watch?labelSelector=app=x 的事件序列(对象 sel 先后:标签改成 app=y → 改回 app=x → 改 data → 删除):
    ADDED     sel rv=22281
    DELETED   sel rv=22285
    ADDED     sel rv=22286
    MODIFIED  sel rv=22287
    DELETED   sel rv=22288

带 labelSelector 的 watch 看到的是“满足条件的对象集合”的变化:对象标签从 app=x 改成 app=y,对这个 watch 来说对象消失了,收到 DELETED;改回来又是 ADDED;条件内的修改才是 MODIFIED。客户端缓存收到 DELETED 并不代表对象真的被删除了。

分步演示

watch 事件
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / kind(Docker 里的单节点或多节点真实集群,节点是 Docker Desktop 虚拟机里的容器)/ kubectl;用空的 kubeconfig 之外的独立 kubeconfig,不碰用户自己的集群;计时用 date +%s%N / kubectl 事件时间戳,数字每次运行会有小幅抖动。三节点 kind 集群 kx-main(Kubernetes v1.37.0);用 kubectl get --raw "?watch=true&timeoutSeconds=N" 读事件流,watch 期间由脚本制造变化;1000~1200 次写入用 8 个线程并发的 kubectl patch 完成(约 85 次/秒);etcd 压缩用 etcd 静态 Pod 里的 etcdctl compact;版本号每次运行不同;每种情形各一次
    • 没有验证:410 出现的阈值由什么决定(缓存容量还是时间窗口,本机只观察到约 80~92 秒、6200~7200 次写入之间失效,没有区分两者);watch 缓存的大小调整参数;BOOKMARK 的周期;etcd 压缩后没有 watch 缓存的资源(缓存关闭时)的行为;HTTP/2 流的断开重连行为;很多 watcher 同时存在时的开销
    京ICP备2026067502号-1京公网安备11010502063487号