深色模式
Controller 原理:client-go informer、workqueue 与 controller-runtime
摘要:本文面向生产 SRE / 平台工程师,深入拆解 Controller 的底层运行机制:client-go 的 Reflector、DeltaFIFO、SharedInformer 缓存,rate-limited workqueue,以及 controller-runtime 如何用 Manager / Builder / Predicates 把这些原语封装成
Reconcile循环。覆盖版本:client-go 与 controller-runtime v0.18+(对应 Kubernetes v1.30+)。
适用版本与前提
- Kubernetes / client-go:v1.30+(本文以当前稳定 client-go 为准)。
- controller-runtime:v0.18+(随 Kubebuilder 4.x)。
- 前提:了解 Go 基本语法、Kubernetes 资源模型(Group/Version/Kind)、以及"期望状态 vs 实际状态"的声明式理念。
背景与问题
Operator 行为看起来很简单:用户改一个 CR,系统就自动跟随变化。但"跟随"背后是一整套事件驱动 + 水平驱动的管道。不理解这套管道,你就会在以下问题上栽跟头:
- 为什么改了 CR 但 controller 没反应(informer 未同步 / predicate 过滤)?
- 为什么
kubectl delete后子资源没被清理(finalizer / owner reference 缺失)? - 为什么 controller 重启后"补做"了大量操作(workqueue 重试 + 水平驱动)?
- 为什么 watch 连接频繁断开(watch 风暴 /
resourceVersion失效)?
核心组件全景
整套机制分为三层:client-go 缓存层(Reflector + DeltaFIFO + SharedInformer + Indexer)、队列层(workqueue)、业务逻辑层(Reconciler)。controller-runtime 在这之上是易用封装。
client-go 缓存层
Reflector:List-Watch 的入口
Reflector 通过 List 一次拉全量对象,再发起 Watch 长连接接收增量事件。它把事件以 Delta(Added/Updated/Deleted/Replaced/Sync)形式推入 DeltaFIFO。
关键点:resourceVersion 是 watch 的游标。Reflector 在 relist 时若发现本地缓存 resourceVersion 落后太多,会触发重新 List,避免"丢事件"。watch 因超时被断开后,会带上次最新的 resourceVersion 重新 Watch。
DeltaFIFO 与 SharedInformer
DeltaFIFO 是一个带去重的队列(同名对象的多条 delta 会合并 dedup),SharedInformer 从中 Pop 出 delta,做两件事:
- 把对象写入
Indexer(本地线程安全缓存,等价于"内存里的 etcd 视图")。 - 触发注册的
ResourceEventHandler(OnAdd/OnUpdate/OnDelete),通常把对象的namespace/name这个 key 投递到 workqueue。
"Shared" 之意:多个 controller 可共享同一个 informer 缓存,避免每个 controller 各起一个 watch 造成 watch 风暴。
Indexer 缓存
Indexer 提供按 namespace/name 或自定义索引快速查询对象的能力。controller-runtime 的 cache.Client 读对象时优先走本地缓存,而非每次打 API Server——这是 controller 高吞吐、低 API Server 压力的根本原因。
心智模型
Controller 的逻辑"视角"不是 API Server 那里的实时全量,而是自己本地 Indexer 缓存里的那个视图。缓存未同步完成前(HasSynced() == false),controller 不应开始 reconcile,否则会基于"空缓存"做出删除一切的错误决策。
controller-runtime 的 Manager 会保证 cache 同步完成后才启动 controller(来源:controller-runtime 文档与 Kubebuilder Book)。
workqueue:限速重试队列
client-go 的 workqueue 是 controller 健壮性的核心:它提供三种特性。
- 去重:同一 key 在队列里只存在一次(即使被多次 enqueue)。
- 限速(rate limiting):Reconcile 返回
error或Result{Requeue: true}后,key 会被重新入队,但不是立即、无限重试,而是按限速器退避(默认指数退避 + 整体上限)。 - 失败计数与最大重试:可配置超过 N 次后丢弃(避免永久卡死),或交给 metrics 暴露。
controller-runtime 默认使用 item 级指数退避限速器;在较新版本中([版本相关])默认是否启用 client-side ratelimiter 有过调整(controller-runtime v0.21 起默认关闭 client-side ratelimiter,需自行设置 rest.Config 的 QPS/Burst 来保留旧行为,来源:controller-runtime changelog)。生产部署务必确认你的限速与重试行为。
controller-runtime 的封装
controller-runtime 把上面所有原语封装为:Manager、Reconciler、Builder。
Manager
Manager 负责创建并持有 cache(informer 工厂)、client、recorder、leader election、metrics、健康检查,并启动所有注册的 controller。一个进程一个 Manager 即可。
Reconciler 接口
go
type Reconciler interface {
Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error)
}1
2
3
2
3
ctrl.Request 只含 Namespace/Name(一个 key)。Reconcile 的契约:
- 幂等:可能被多次、乱序调用,必须基于"当前实际状态"收敛,而非假设"只来一次"。
- 返回
err != nil→ 进 workqueue 重试(受限速器约束)。 - 返回
Result{RequeueAfter: d}→ 定时 d 后再次触发(用于周期性 reconcile,如检查备份到期)。 - 返回
Result{}且无 error → 本轮结束,等下次事件。
Builder:声明 watch 关系
go
func (r *DatabaseReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&appsv1.Database{}). // 主资源:watch Database
Owns(&appsv1.StatefulSet{}). // 拥有:StatefulSet 变化也触发 reconcile
Watches(&source.Kind{Type: &corev1.Secret{}}, // 额外 watch 其他类型
handler.EnqueueRequestsFromMapFunc(r.findDatabasesForSecret)).
WithOptions(controller.Options{MaxConcurrentReconciles: 2}).
Complete(r)
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
For:主资源,其变更直接 enqueue 自身 key。Owns:通过 owner reference 关联的子资源(如 StatefulSet、Pod)变更时,映射到 owner 的 key 触发 reconcile——这是"子资源变了要补调谐"的关键。Watches+handler:更灵活的映射,比如某个 Secret 变化要通知所有引用它的 Database。
水平驱动 vs 边缘驱动
官方与 Kubebuilder 文档反复强调:Reconcile 是水平驱动(level-based)的。即使 informer 漏掉某个事件,只要实际状态与期望不符,下一次(周期性或下次事件触发的)reconcile 仍会修复。这正是 controller 自愈能力的来源,也是它比"纯事件处理器"更鲁棒的原因。
常见误解
"事件来了才 reconcile"是错误模型。Reconcile 永远基于当前状态做 diff 与收敛;事件只是"唤醒"它。因此 Reconcile 函数里绝对不能假定收到了特定事件,而要重新读实际状态。
生产实践
- cache 同步门控:确保 controller 在 cache
HasSynced前不启动(controller-runtime Manager 默认保证)。 - 限制并发与速率:通过
MaxConcurrentReconciles与rest.Config的 QPS/Burst 控制对 API Server 的压力,避免大型集群里一个 controller 把 apiserver 打满。 - Finalizer 用于清理外部资源:当 CR 被删除时,要在
DeletionTimestamp非空且仍有 finalizer 时执行清理(删外部资源/子资源),再移除 finalizer 放行删除。没有 finalizer 的"外部资源"会成为孤儿。 - 调谐要只读缓存、写走 client:读用
r.Get(走本地缓存,快);创建/更新用r.Create/Update/Status().Update。注意Status().Update更新的是 status 子资源,需要对应 RBAC。
常见失败模式
- 缓存未同步就 reconcile:未等
HasSynced启动,基于空缓存删除了全部资源(灾难级)。Manager 默认已防,但自定义 controller 需自查。 - Reconcile 不幂等:依赖"事件顺序"导致重复创建对象(如重复建 Secret)。正确做法是"先 Get,若不存在再 Create"。
- watch 风暴:集群资源巨多且未共享 informer,每个 controller 各 watch 全量,压垮 apiserver。用 SharedInformer / controller-runtime cache 共享。
- 无限快速重试:Reconcile 一直返回 error 且无退避,打满 workqueue 与 apiserver。务必依赖限速器,并在无法自愈的错误上尽快 return(或加人工介入信号)。
- owner reference 缺失:用
controllerutil.SetControllerReference给子资源设置 owner,否则级联删除、Owns 触发都会失效。
故障排查
bash
# 看 controller 是否正常运行、有无频繁重启
kubectl get pods -n <operator-ns>
kubectl logs <operator-pod> --tail=200
# 观察 reconcile 指标(controller-runtime 暴露)
# controller_runtime_reconcile_total / controller_runtime_reconcile_errors_total
# workqueue 深度与重试:workqueue_adds_total / workqueue_depth
# informer 未同步可通过 cache 同步相关日志判断1
2
3
4
5
6
7
2
3
4
5
6
7
关键指标含义
controller_runtime_reconcile_total{result="error"}:错误 reconcile 计数,持续增长说明有结构性问题。workqueue_depth:积压深度,持续高位说明 reconcile 慢或频繁 requeue。workqueue_retries_total:重试计数,配合限速器观察是否陷入抖动。
回滚与清理
- Controller 自身无状态(状态在集群里),回滚即重新部署旧镜像版本(滚动更新,Kubernetes 原生支持)。
- 清理外部资源依赖 finalizer;如果 finalizer 逻辑有 bug 导致 CR 卡在
Terminating,需排查 controller 是否存活并能处理删除事件。
安全与合规
- Controller 的 ServiceAccount 权限过大(如
cluster-admin)会放大任何 Reconcile bug 的爆炸半径。遵循 RBAC 最小权限,仅授予它管理的资源类型。 - Leader election 保证多副本下只有一个 active controller,避免双写冲突;生产应部署 ≥2 副本 + PDB。
性能、容量与成本
- informer 会缓存所 watch 资源的全量对象到 controller 进程内存。watch 对象量级巨大(数十万 Pod)时,controller 内存与 apiserver 负载都需评估。
- 合理使用
Owns而非全局Watches,减少不必要的 reconcile 触发。
FAQ
Q:controller-runtime 和 client-go 是什么关系? A:controller-runtime 构建在 client-go 之上,封装了 informer/workqueue/manager,使开发者只需写 Reconcile。client-go 是底层依赖。
Q:Reconcile 返回 error 会怎样? A:key 被限速器重新入队并按退避重试;超过上限后丢弃并计数,不会无限阻塞(具体上限取决于配置)。
参考资料
- client-go 官方仓库与 informer 设计,访问日期:2026-10-08。
- Kubernetes 官方文档 - Controller 模式,访问日期:2026-10-08。
- controller-runtime 文档与 VERSIONING 说明,访问日期:2026-10-08。
- The Kubebuilder Book - 控制器原理章节,访问日期:2026-10-08。