直接使用sharedinformer而非轮询,因其基于list-watch机制实现事件驱动、本地缓存与自动重连,避免轮询导致的限流、etcd压力及事件丢失;官方推荐且需显式run并等待cachesync。

为什么直接用 client-go 的 SharedInformer 而不是轮询 API?
轮询 /api/v1/pods 这类接口不仅浪费连接和服务器资源,还会错过事件(如 Pod 在两次轮询之间被创建又删除)。SharedInformer 基于 watch 机制 + 本地缓存,自动处理重连、增量更新和事件分发,是 client-go 官方推荐的控制器基础。
它默认每 10 小时强制 resync(可调),保证本地缓存与 apiserver 最终一致;同时支持添加多个 EventHandler,适合解耦业务逻辑。
- 初始化时必须传入
ResyncPeriod(哪怕设为 0,否则可能静默失败) - 不要在
OnAdd/OnUpdate中直接修改对象指针——它们来自共享缓存,修改会影响其他监听器;需用deepCopy()或构造新对象 -
SharedInformer不自动启动,必须显式调用informer.Run(stopCh),且stopCh应在程序退出前关闭
如何正确构造 Informer 并过滤特定 Namespace 和 Label?
原生 NewSharedIndexInformer 不支持开箱即用的 label/namespace 过滤。常见错误是把过滤逻辑写在 EventHandler 里,导致无意义事件仍被分发、缓存中塞满无关对象。
正确做法是在 Informer 构建阶段通过 cache.WithFieldIndexers 或更常用的 cache.Indexers + 自定义 ListOptions 实现服务端过滤:
// 只监听 default namespace 下带 app=nginx 的 Pod
listOptions := func(options *metav1.ListOptions) {
options.FieldSelector = "metadata.namespace=default"
options.LabelSelector = "app=nginx"
}
informer := corev1informers.NewPodInformer(
clientset,
cache.WithListOptions(listOptions),
)
-
FieldSelector支持metadata.namespace和metadata.name,但不支持 label —— label 过滤必须靠LabelSelector参数 - 若需跨 namespace 监听但只关心某几个 label,只能在 handler 中判断并 return,此时应确保 label key 存在(避免 panic:
obj.GetLabels()["app"]前先 check map 是否 nil) - 使用
cache.ByIndex做索引加速(如按 label 建索引)适用于高频查询场景,但会增加内存开销
控制器核心循环里怎么安全地更新资源而不触发无限 reconcile?
典型错误是:在 OnUpdate 中调用 clientset.CoreV1().Pods(ns).Update(ctx, pod, ...) 后,该更新又触发新一轮 OnUpdate,形成死循环。
根本解法是「区分事件来源」:给受控资源打 annotation(如 controller.kubernetes.io/managed-by: my-controller),并在 handler 中跳过已标记的对象;或更稳妥地比对 spec、status、annotations 差异再决定是否更新。
- 永远用
resourceVersion做乐观锁更新,避免覆盖他人变更;Update失败后要检查errors.IsConflict(err)并重试(client-go 提供retry.RetryOnConflict) - 不要在 handler 中阻塞操作(如 HTTP 请求、数据库写入)——应投递到 worker queue,用 goroutine 异步处理
- 若更新 status 字段,务必用
Status().Update而非Update,否则 apiserver 会拒绝(status 子资源权限独立)
本地调试时为什么 GetPod 返回 NotFound 却能正常 watch?
这是 client-go 缓存机制导致的认知偏差:Informer 启动后会先 list 全量数据填充缓存,但如果你在 informer.Start() 之前就调用 informer.Lister().Get(...),缓存还是空的,必然返回 NotFound。
调试时最容易忽略的是等待缓存同步完成:
informer := corev1informers.NewPodInformer(...)
informer.Informer().AddEventHandler(...)
go informer.Informer().Run(stopCh)
// ❌ 错误:立刻查缓存
pod, err := informer.Lister().Pods("default").Get("my-pod")
// ✅ 正确:等同步完成后再查
if !cache.WaitForCacheSync(stopCh, informer.Informer().HasSynced) {
log.Fatal("failed to sync cache")
}
pod, err := informer.Lister().Pods("default").Get("my-pod")
-
WaitForCacheSync必须在Run()启动后调用,否则永远超时 - 如果 informer 没有成功 sync(比如 RBAC 权限不足),
HasSynced()会一直返回 false,程序卡住——建议加 timeout context - 生产环境不应依赖
Lister().Get做关键路径查询;它只是缓存快照,实时性不如直接 clientset.Get
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











