应使用 controller-runtime 而非手写 informer 循环,因其封装了资源版本冲突处理、重试退避、leader 选举等底层逻辑,仅需关注资源变更与处理逻辑;reconciler 天然幂等,支持延迟重入、命名空间隔离、标签过滤、dry-run 模式、finalizer 管理及本地调试适配。

为什么用 controller-runtime 而不是手写 Informer 循环
直接写 Informer + 事件循环容易漏掉资源版本冲突、重试退避、Leader 选举这些底层逻辑,controller-runtime 把它们封装成可组合的组件。你只关心“什么资源变了”和“怎么处理”,其他交给 Manager 和 Reconciler。
常见错误是自己起 goroutine 调 client.Delete 后不检查 errors.IsNotFound,结果日志刷满 404 not found;而 controller-runtime 的 Reconcile 方法天然支持幂等,删两次也没事。
-
Reconciler返回ctrl.Result{RequeueAfter: time.Minute}可延迟下次执行,适合清理前加个宽限期 - 用
client.List查Pod时务必加client.InNamespace,否则可能误删 default 命名空间里的资源 - 不要在
Reconcile里调time.Sleep,会阻塞整个控制器队列;要用RequeueAfter或者异步 job
如何识别“测试环境”资源并安全过滤
靠命名规则(比如 test-、-e2e)或标签(env=test、ci=auto)都行,但必须统一且可审计。别用注解做主判断依据——注解容易被人工修改,也不进 etcd watch 事件的 key 路径,导致控制器漏触发。
典型坑是:用 strings.Contains(pod.Name, "test") 匹配,结果把 postgres-test-db-7c89f 和 production-test-failover 全干掉了。
立即学习“go语言免费学习笔记(深入)”;
- 推荐用 label selector:
client.MatchingFields{"metadata.labels.env": "test"},配合 admission webhook 强制打标 - 对
Namespace级别清理,先查Namespace的labels,再 list 该 ns 下所有Pod/Job/PVC,避免跨 ns 误操作 - 加一个 dry-run 模式开关:通过环境变量
DRY_RUN=true控制是否跳过client.Delete,只打印将要删什么
Finalizer 和 OrphanDependents 怎么选才不卡住删除
测试资源一般没外部依赖,直接删就行。但如果你的控制器自己加了 Finalizer(比如想等 PVC 数据备份完再删),就必须在 Reconcile 里显式移除它,否则资源永远卡在 Terminating 状态。
另一个常见错误是用 client.Delete 时忘了传 &client.DeleteOptions{OrphanDependents: pointer.Bool(true)},结果 Deployment 删了,底下 ReplicaSet 和 Pod 还挂着,控制器以为“删完了”,其实没清干净。
- 删
Deployment前,先用client.List查它的ownerReferences,确认没有非测试环境的 owner - 删
PVC前检查spec.volumeMode和status.phase,Released状态的才能删,Bound的得先删对应PV或Pod - 对
Job,优先用ttlSecondsAfterFinished字段(K8s 1.12+),比控制器轮询更轻量
本地调试时为什么 manager.Start 一直卡住
因为默认用 in-cluster config 连 API server,本地没 kubeconfig 就 panic。必须显式加载 kubeconfig,并关掉 Leader 选举(本地不需要抢锁)。
错误现象:failed to acquire lease kube-system/my-controller-leader-lock 或 no route to host,其实是没读到 KUBECONFIG 环境变量或路径不对。
- 启动时加参数:
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{Scheme: scheme, MetricsBindAddress: "0", LeaderElection: false}) - 确保
~/.kube/config有对应集群权限,或者用KUBECONFIG=/path/to/kubeconfig go run main.go - 加
ctrl.Log = zap.New(zap.UseDevMode(true)),不然默认日志全丢进/dev/null,debug 时一脸懵
最麻烦的是 finalizer 清理时机——它必须在资源真正被删除前完成,否则 etcd 里残留对象会阻塞 namespace 删除。这个点不测到真实集群,光本地跑通没用。










