Watch本质是HTTP长连接+JSON流,非轮询;client-go自动重连但事件不全量重放,需用Informer封装或手动处理410错误、resourceVersion起点;Delete事件可能因Finalizer延迟触发,需检查DeletionTimestamp。

Watch 本质是 HTTP 长连接 + JSON 流,不是轮询
client-go 的 Watch 不是定时调用 List 再比对,而是复用一个底层 HTTP 连接,持续接收服务端推送的 WatchEvent 流。这意味着:连接一旦断开(如网络抖动、apiserver 重启),必须靠 client-go 自动重连;而重连后拿到的事件是“从断点继续”,不是全量重放——所以你不能假设 Watch 流里一定包含某个资源的完整生命周期。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 永远用
cache.NewInformer或cache.NewSharedIndexInformer封装 Watch,别手写Watch()循环——它自动处理重连、事件去重、本地缓存同步 - 如果真要裸用
Watch(),必须检查err和resp.StatusCode,遇到410 Gone表示资源版本过旧,需先List获取新resourceVersion再重试 - Watch 请求默认带
?watch=true&resourceVersion=0,但resourceVersion=0会从当前时刻开始,错过之前已发生的变更;生产环境应从List结果中取最新metadata.resourceVersion作为起点
为什么 Informer 的 EventHandler 有时收不到 Delete 事件
不是 client-go 漏了,而是 K8s 的 GC 机制和 Finalizer 导致的“延迟删除”:资源进入 Deleting 状态后,会先触发 Delete 事件;但若对象有 finalizers,它不会被真正移除,而是卡在该状态,直到所有 finalizer 被清理。此时你再 Get 它,仍能拿到对象(只是 deletionTimestamp 非空)。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 在
DeleteFunc中,先检查obj.(*corev1.Pod).DeletionTimestamp是否非空——非空说明是逻辑删除,对象还在 etcd 里;为空才是物理删除完成 - 需要“真正消失后才执行”的逻辑(比如清理宿主机文件),得额外起一个 goroutine 定期
Get,直到返回404 NotFound - 避免在
DeleteFunc里直接调用阻塞操作(如 HTTP 请求、数据库写入),否则会拖慢整个 Informer 的事件队列
Watch 多个命名空间 or 全局资源?看 GroupVersionResource 和 Scope
Watch 范围由 schema.GroupVersionResource 和命名空间参数共同决定。比如 corev1.SchemeGroupVersion.WithResource("pods") 是集群级资源定义,但传 namespace="default" 就只 Watch default 下的 Pod;传 namespace=""(空字符串)且该资源是 namespaced 类型,则报错 namespaces is forbidden;只有 Cluster-scoped 资源(如 Node、ClusterRole)才能不指定 namespace。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- Watch 所有命名空间的 Pod:用
clientset.CoreV1().Pods("").Watch(ctx, opts)——注意是"",不是metav1.NamespaceAll(后者是常量,值为".",仅用于 List 语义) - Watch 自定义资源(CRD)时,必须确保 clientset 已注册对应
Scheme,否则Watch()会 panic:“no kind is registered for the type” - 同时 Watch 多个 namespace?别拼多个 Watch——用
SharedInformerFactory启多个 informer,共享同一套 Reflector 和 DeltaFIFO,更省内存
Watch 内存暴涨或 Goroutine 泄漏的常见原因
Watch 本身不耗内存,但事件处理不当会快速堆积:比如在 AddFunc 里启动一个没控制的 goroutine 去处理,又没加 context 取消或限速,这个 goroutine 挂了之后还占着对象引用,GC 不掉。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 永远给事件处理函数加超时控制:
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second),并在 defer 里cancel() - 避免在 EventHandler 里直接操作 clientset 的 write 方法(如
Patch、UpdateStatus),它们可能失败重试,导致 goroutine 卡住;改用 workqueue + retrying informer 模式 - 用
pprof查 goroutine 数量时,重点看是否大量停在runtime.gopark或net/http.(*persistConn).readLoop——前者说明 handler 卡死,后者说明 Watch 连接没关干净
Watch 的难点不在怎么写,而在怎么让它稳定跑三个月不出问题:重连逻辑、事件语义理解、资源生命周期绑定、错误传播路径——这些地方一漏,就是凌晨三点的告警。

















