Kubernetes client-go Informer监听Deployment副本变更未触发事件
问题核心原因
你的代码存在4个致命问题,直接导致事件捕获失效、程序异常崩溃:
- 你使用
SharedInformerFactory创建informer,却绕过factory直接调用informer.Run()启动,factory上配置的namespace过滤、FieldSelector过滤规则完全不生效,informer会尝试拉取集群全量Deployment数据。如果你配置的RBAC权限没有集群级Deployment的list/watch权限,会直接静默list失败,本地缓存永远无法建立,自然收不到任何事件。 - 启动informer后没有等待本地缓存同步完成。informer的工作逻辑是先全量list目标对象推送Add事件,再从最新resourceVersion开始watch增量Update事件,缓存未同步完成时不会处理后续watch事件,甚至可能触发空指针导致无日志panic。
- 事件处理函数中直接写
panic逻辑,只要遇到一次类型断言异常(比如informer断连重连时会传入cache.DeletedFinalStateUnknown类型的墓碑对象),整个进程直接退出。如果程序部署在K8s集群中,Pod会被自动重启,表现就是无错误信息的崩溃循环。 - 逻辑漏洞:你在函数中写了
defer close(stopper),如果这段代码不是写在main函数的主协程中,函数返回时会直接关闭stopper通道,informer会立刻停止运行,根本等不到后续扩缩容事件。
修正后可直接运行的代码
package main import ( "time" "github.com/sirupsen/logrus" appsv1 "k8s.io/api/apps/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/informers" "k8s.io/client-go/tools/cache" ) func main() { // 前置变量:clientSet、targetName、targetNamespace、sendUpdates 提前初始化完成 labelOptions := informers.WithTweakListOptions(func(opts *v1.ListOptions) { opts.FieldSelector = "metadata.name=" + targetName }) factory := informers.NewSharedInformerFactoryWithOptions( clientSet, 5*time.Minute, // 重同步周期设为5分钟更合理,避免频繁请求API Server informers.WithNamespace(targetNamespace), labelOptions, ) deploymentInformer := factory.Apps().V1().Deployments().Informer() // 先注册事件处理器,再启动informer,避免漏掉启动窗口期的事件 _, err := deploymentInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { // 兼容处理墓碑对象 dep, ok := obj.(*appsv1.Deployment) if !ok { tombstone, ok := obj.(cache.DeletedFinalStateUnknown) if !ok { logrus.Errorf("AddFunc got unexpected object type: %T", obj) return } dep, ok = tombstone.Obj.(*appsv1.Deployment) if !ok { logrus.Errorf("AddFunc got invalid tombstone object type: %T", tombstone.Obj) return } } replicas := int(*dep.Spec.Replicas) logrus.Infof("Initial deployment synced, current replicas: %d", replicas) sendUpdates() }, UpdateFunc: func(oldObj, newObj interface{}) { // 兼容处理新旧对象的墓碑场景 oldDep, ok := oldObj.(*appsv1.Deployment) if !ok { tombstone, ok := oldObj.(cache.DeletedFinalStateUnknown) if !ok { logrus.Errorf("UpdateFunc got unexpected old object type: %T", oldObj) return } oldDep, ok = tombstone.Obj.(*appsv1.Deployment) if !ok { logrus.Errorf("UpdateFunc got invalid old tombstone object type: %T", tombstone.Obj) return } } newDep, ok := newObj.(*appsv1.Deployment) if !ok { tombstone, ok := newObj.(cache.DeletedFinalStateUnknown) if !ok { logrus.Errorf("UpdateFunc got unexpected new object type: %T", newObj) return } newDep, ok = tombstone.Obj.(*appsv1.Deployment) if !ok { logrus.Errorf("UpdateFunc got invalid new tombstone object type: %T", tombstone.Obj) return } } // 资源版本相同说明是informer重同步事件,直接跳过 if oldDep.ResourceVersion == newDep.ResourceVersion { return } // 跳过正在删除的Deployment事件 if newDep.DeletionTimestamp != nil { return } oldReplicas := int(*oldDep.Spec.Replicas) newReplicas := int(*newDep.Spec.Replicas) if oldReplicas != newReplicas { logrus.Infof("Deployment replicas changed from %d to %d", oldReplicas, newReplicas) sendUpdates() } }, DeleteFunc: func(obj interface{}) { logrus.Infof("Deployment %s/%s deleted", targetNamespace, targetName) }, }) if err != nil { logrus.Fatalf("Failed to add event handler: %v", err) } stopper := make(chan struct{}) defer close(stopper) // 统一通过factory启动所有informer,不要单独调用informer.Run factory.Start(stopper) // 必须等待缓存同步完成,这步不能省略 logrus.Info("Waiting for deployment informer cache sync...") if !cache.WaitForCacheSync(stopper, deploymentInformer.HasSynced) { logrus.Fatalf("Failed to sync deployment informer cache") } logrus.Info("Deployment informer cache synced, start listening events") // 阻塞运行直到收到退出信号 <-stopper }
关键注意事项
- 事件处理逻辑不要执行长耗时阻塞操作,否则会卡住整个informer的事件分发队列,导致事件延迟甚至丢失。如果有耗时逻辑,提前把数据丢到带缓冲的channel里,启动单独的worker协程处理。
- 确认你的ServiceAccount绑定的RBAC权限,拥有目标namespace下Deployment的list、watch权限,否则informer无法正常拉取数据。
- 不需要额外监听HPA资源,HPA触发扩缩容时一定会修改Deployment的
Spec.Replicas字段,只要informer缓存同步正常就不会丢事件。
内容的提问来源于stack exchange,提问作者Syd
相关产品推荐
相关产品推荐

