不能直接用 client-go Watch 监听 CRD,因其缺乏自动重连、resourceVersion 断点续传、本地缓存及 workqueue 去重限速,易导致事件丢失、API 压力大、重复处理;Reconcile 由对象生命周期事件、Informer 首次同步、Status 子资源更新三类信号触发;字段级响应需在 EnqueueRequestsFromMapFunc 中比对新旧对象并前置过滤;必须确保 CRD 设置 spec.preserveUnknownFields: false 且 RBAC 包含 watch 权限,否则监听链路在前端即中断。

用 Go 写 Kubernetes Operator 监听资源变化,核心不是“监听某个字段”,而是靠 controller-runtime 的 Reconcile 函数响应整个对象的生命周期事件(Create/Update/Delete)+ Informer 缓存同步完成 + OwnerReference 关联变更。直接调 client-go 的 Watch 或轮询会丢事件、绕过缓存、引发状态不一致。
为什么不能直接用 client-go Watch 监听 CRD?
CRD 资源注册后,其变更必须经由 controller-runtime 的 Informer 体系分发,否则会遇到三类硬伤:
-
Watch连接断开后无法自动恢复,且缺失resourceVersion断点续传逻辑 - 未经过
SharedInformer的对象没有本地缓存,Get操作全走 API Server,压垮控制平面 - 多个 Operator 副本会重复处理同一事件,因为没走
workqueue的去重与限速
Reconcile 函数触发条件有哪些?
Reconcile 不是靠“监听 spec.xxx 字段”触发的,而是被以下三类信号驱动:
- CR 对象本身发生
Create/Update/Delete(哪怕只改了metadata.annotations) - Informer 缓存完成首次同步(
Sync),此时会触发一次全量调谐 - 你显式在
Watches中监听了该 CR 的Status子资源,且其他组件(如 webhook)更新了Status
注意:Reconcile 接收的是 reconcile.Request(含 NamespacedName),不是原始事件对象;每次调用都需重新 Get 当前状态,不能依赖上一次缓存。
立即学习“go语言免费学习笔记(深入)”;
如何只响应特定字段变更(比如 spec.replicas)?
controller-runtime 不提供原生字段级监听,但可用 EnqueueRequestsFromMapFunc 实现轻量过滤:
- 在
Builder中用.Watches(&source.Kind{Type: &myappv1.MyApp{}}, handler.EnqueueRequestsFromMapFunc(...)) - 在
MapFunc中:先从cache获取旧对象(用r.Get或mgr.GetCache().Get),再对比old.Spec.Replicas != new.Spec.Replicas,仅在此时返回非空reconcile.Request - 切勿把比对逻辑放在
Reconcile内部——那样仍会进队列、占 worker、消耗锁
这个逻辑必须在入队前做,否则就失去了过滤意义。
两个最容易漏掉的硬性前提
很多团队写了 Reconcile 却收不到事件,根本原因常是:
- CRD YAML 中
spec.preserveUnknownFields: false没设为false,导致controller-runtime无法校验结构,Informer 启动失败且无明确错误日志 - RBACK 中
rules缺少watch权限,例如只给了get和list,结果 Informer 根本没注册,Reconcile永远不会被调用
这两项不满足,整个监听链路就断在最前端,后续所有逻辑都不会执行,排查时容易陷入“代码没错但就是不触发”的死循环。


















