用client-go写controller因其实现了informer机制等自动处理能力,避免手动实现watch重连、资源版本对齐等;新项目推荐controller-runtime以减少样板代码和错误。

为什么用 client-go 写 Controller 而不是直接调 API?
因为 client-go 封装了 Informer 机制、Reflector、DeltaFIFO 和 SharedIndexInformer,能自动处理连接断开、资源版本(resourceVersion)对齐、本地缓存和事件去重——手动轮询或长连接容易漏事件、重复处理、丢状态。
不用它的话,你得自己实现:watch 重连逻辑、410 Gone 后全量 list、对象 diff、并发安全的本地存储……这些在 client-go 里早就是成熟路径。
实操建议:
- 始终基于
SharedIndexInformer(而非原始Watch)构建控制器主循环 - 不要自己 new
http.Client去调/apis/xxx/v1/namespaces/.../watch—— 那是退化回“半成品” -
client-gov0.28+ 强制要求使用Controller接口(来自k8s.io/client-go/tools/cache),别再手写 for-select-loop
controller-runtime 和原生 client-go 选哪个?
绝大多数新项目该用 controller-runtime(CR),它是基于 client-go 的封装层,屏蔽了 Informer 注册、Scheme 构建、Reconcile 并发控制等样板代码。不是“更高级”,而是“少写错”。
立即学习“go语言免费学习笔记(深入)”;
常见错误现象:no kind "MyResource" is registered for version "mygroup.example.com/v1" —— 这是因为没把 CRD 类型注册进 scheme,而 controller-runtime 的 Builder 会帮你做这事;原生 client-go 需手动调 AddToScheme。
实操建议:
- 新 Controller 直接用
ctrl.NewControllerManagedBy(mgr)启动,别从cache.NewSharedIndexInformer开始造轮子 - 若必须用原生
client-go(比如嵌入已有 client-go 工程),确保调用myv1.AddToScheme(scheme),且scheme被传给cache.NewListWatchFromClient -
controller-runtime默认启用 leader election 和 healthz 端点,生产环境省掉自己补的 200 行胶水代码
Reconcile 函数里不能干哪些事?
Reconcile 是同步函数,阻塞就卡住整个队列;它不是 goroutine,框架不会帮你并发调度多个实例——同一对象的多次事件会被排队执行。
典型翻车场景:time.Sleep(5 * time.Second) 卡死队列;http.Get("https://external-api.com") 没设 timeout 导致所有后续事件堆积;在 Reconcile 里启动无限 go func() 导致 goroutine 泄漏。
实操建议:
- 所有外部 I/O 必须带 context 超时:
ctx, cancel := context.WithTimeout(r.ctx, 10*time.Second) - 禁止在 Reconcile 中做任何非幂等写操作(比如重复创建 ConfigMap)——要靠
Get + Compare + Patch/Update或者Apply(需 k8s 1.22+)保证 - 需要异步轮询外部系统?用
EnqueueAfter或独立 worker,别塞进 Reconcile - 日志打
req.NamespacedName.String(),别只打req.Name——namespace 错了根本查不到是谁触发的
怎么让 Controller 只监听指定 namespace?
默认情况下,Informer 监听集群范围资源(Scope: Cluster)。如果 CRD 是 namespaced 类型,但你的 Controller 只想管 production 这个 namespace,不能靠 if 判断跳过——那只是浪费 CPU 和 etcd 带宽。
正确做法是在 Informer 初始化时加 namespace 限定。用 controller-runtime 很简单;原生 client-go 需换 ListWatch 构造方式。
实操建议:
-
controller-runtime:用Watches(&source.Kind{Type: &myv1.MyResource{}}, &handler.EnqueueRequestForObject{}, builder.WithPredicates(predicate.NamespacePredicate{Namespaces: []string{"production"}})) - 原生
client-go:构造cache.NewListWatchFromClient时,client 必须是 namespaced(如clientset.MyGroupV1().MyResources("production")),而不是 cluster-scopedclientset.MyGroupV1().MyResources("") - 注意:namespace 限定对 CustomResourceDefinition 对象本身无效(它永远是 cluster-scoped),只能用于其下实例(如 MyResource)
最常被忽略的一点:测试时用 kubectl apply -f cr.yaml 创建资源,却忘了检查 metadata.namespace 字段——空 namespace 在 namespaced CRD 下等于创建失败,但错误藏在 events 里,Reconcile 根本收不到事件。










