4. 控制器运行时与协调循环

Operator 的核心不是“部署一次就完事”,而是持续把集群的实际状态拉回到期望状态。这个持续拉回的过程由控制器运行时(Controller Runtime)承载,其中心是 Reconcile 协调循环。理解 Reconcile 的调用时机、事件如何驱动它、以及为什么必须写成幂等函数,是写出可靠 Operator 的分水岭。

Reconcile 的本质:一次“期望 vs 实际”的对账

在 controller-runtime 中,每个控制器都实现一个 Reconcile(ctx, req) 方法。它接收的 req 只是一个 NamespacedName(命名空间 + 名称),而不是完整对象。这意味着 Reconcile 必须自己通过 client 去读取当前对象的最新状态。框架不保证你拿到的是“变更前”还是“变更后”的版本,只保证“这个对象可能需要处理”。

Reconcile 的返回值是 (Result, error),它决定了后续行为:

  • 返回 Result{}, nil:本次协调成功,不重新入队,等待下一次事件触发。
  • 返回 Result{RequeueAfter: d}, nil:d 时间后重新入队,用于周期性巡检或等待外部系统就绪。
  • 返回 Result{Requeue: true}, nil:立即重新入队,通常用于依赖对象尚未就绪、需要快速重试的场景。
  • 返回 error:框架按指数退避(默认 5ms 起,上限约 1000s)重新入队,直到成功或达到最大重试。

一个常见的误区是把 Reconcile 当成“事件处理器”,在里面写“收到创建事件就创建 Deployment”。正确的心智模型是:Reconcile 不关心发生了什么事件,只关心“现在这个对象应该长什么样,实际长什么样,差在哪”。事件只是触发对账的闹钟。

事件驱动机制:谁在敲响 Reconcile 的门

controller-runtime 通过 Informer 监听 API Server 的 watch 流,把对象变更转换为事件,再经 WorkQueue 投递给 Reconcile。事件来源主要有三类:

  • 主资源事件:通过 For(&MyApp{}) 注册,MyApp 的增删改都会触发。
  • 从属资源事件:通过 Owns(&appsv1.Deployment{}) 注册,被 MyApp 拥有的 Deployment 变更时,会反向映射到其 owner(MyApp)并触发 Reconcile。
  • 任意关联资源事件:通过 Watches(&corev1.ConfigMap{}, handler.EnqueueRequestsFromMapFunc(...)) 注册,用自定义映射函数把 ConfigMap 变更映射到需要处理的 MyApp 列表。

WorkQueue 有两个关键特性:去重和限速。同一对象在短时间内多次变更只会保留一个待处理项;失败重试则走 RateLimiter。这解释了为什么 Reconcile 可能被“合并触发”——你不需要为每次变更都做全量处理,只需保证最终状态正确。

Owns 的 ownerReference 是双向联动的关键:Deployment 被删除时,Kubernetes 的垃圾回收会级联删除其 owner,而 Owns 监听则让 MyApp 的 Reconcile 被触发,从而有机会重建被误删的从属资源。

幂等性:Reconcile 必须能安全地重复执行

由于事件可能重复、重试可能发生、控制器可能重启后重新入队,Reconcile 会被执行很多次。幂等性要求:无论执行多少次,只要输入(集群状态)相同,最终结果一致,且不产生副作用累积。

实现幂等的常用手法:

  1. 先读后写,用 CreateOrUpdate 语义:先 Get 目标对象,不存在则 Create,存在则比较并 Update/Patch。不要盲目 Create。
  2. 用 Patch 代替 Update:client.Patch 配合 client.MergeFrom 只提交差异,减少冲突和覆盖他人修改的风险。
  3. 把“已存在”当作成功:如果目标资源已存在且符合期望,直接返回成功,不要报错。
  4. 状态写入与规格写入分离:Status 子资源用 Status().Update(),避免与 Spec 更新互相干扰。
  5. 避免在 Reconcile 中做不可逆操作:如发送邮件、扣款、删除外部资源,应通过状态机或 finalizer 控制,确保重试安全。

下面这段示例展示了一个幂等的 Reconcile 骨架:它先读取 MyApp,再确保 Deployment 存在且副本数正确,最后更新 Status。注意所有写操作都基于“当前实际状态”判断,而不是基于事件类型。

func (r *MyAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    var app myv1.MyApp
    if err := r.Get(ctx, req.NamespacedName, &app); err != nil {
        return ctrl.Result{}, client.IgnoreNotFound(err)
    }

    // 期望状态:副本数来自 Spec
    desiredReplicas := app.Spec.Replicas

    // 实际状态:读取同名 Deployment
    var dep appsv1.Deployment
    err := r.Get(ctx, types.NamespacedName{Name: app.Name, Namespace: app.Namespace}, &dep)
    if apierrors.IsNotFound(err) {
        dep = buildDeployment(&app, desiredReplicas)
        if err := r.Create(ctx, &dep); err != nil {
            return ctrl.Result{}, err
        }
    } else if err != nil {
        return ctrl.Result{}, err
    } else if *dep.Spec.Replicas != desiredReplicas {
        dep.Spec.Replicas = &desiredReplicas
        if err := r.Update(ctx, &dep); err != nil {
            return ctrl.Result{}, err
        }
    }

    // 更新 Status,反映当前观察到的副本数
    app.Status.ReadyReplicas = dep.Status.ReadyReplicas
    if err := r.Status().Update(ctx, &app); err != nil {
        return ctrl.Result{}, err
    }
    return ctrl.Result{}, nil
}

这段代码即使被连续调用十次,结果也一致:Deployment 不会被重复创建,副本数不会被反复改写,Status 只是被刷新为当前值。这就是幂等。

协调循环中的状态流转与结果判读

把 Reconcile 的输入、判断、输出串起来看,可以形成一条清晰的状态流转链。下图展示了从事件触发到结果判读的完整路径,以及幂等性在其中的作用点。

事件源(Informer Watch) MyApp 增 / 删 / 改 Owned Deployment 变更 关联 ConfigMap 变更 WorkQueue 去重 · 限速 · 重试 NamespacedName 指数退避 5ms→1000s Reconcile(ctx, req) ① Get 当前对象(读最新状态) ② 计算期望状态 vs 实际状态 ③ Create / Update / Patch 差异 ④ Status().Update() 写回状态 幂等:重复执行结果一致 返回值决定后续行为 Result{}, nil → 等待下次事件 RequeueAfter → 周期巡检 error → 退避重试 Requeue:true → 立即重入队 重新入队 幂等性设计原则(Reconcile 可安全重复执行) 先读后写 Get 不存在则 Create 存在则比较后更新 Patch 优先 MergeFrom 只提交差异 降低冲突与覆盖风险 状态分离 Spec 与 Status 分开写 Status().Update() 副作用受控 finalizer 保护删除 外部操作可重试

图中左侧是事件源,中间是 WorkQueue 的去重与限速,右侧是 Reconcile 的四步对账逻辑,下方是返回值分支与幂等原则。虚线回环表示“未达成期望时重新入队”,这是协调循环持续运转的动力来源。

实际排障中值得观察的信号

当 Operator 行为异常时,可以从几个方向判断问题出在事件驱动还是幂等实现上:

  • Reconcile 被频繁触发:检查是否在 Reconcile 中更新了被 Owns 监听的对象,导致“写→事件→再写”的自激循环。常见于 Status 更新触发了主资源事件。
  • Reconcile 从不触发:检查 RBAC 是否允许 watch,Informer 是否成功启动,以及 Owns 的 ownerReference 是否正确设置。
  • 重复创建资源报 AlreadyExists:说明没有先 Get 再判断,违反了幂等原则。
  • 资源版本冲突(Conflict):说明用了 Update 全量覆盖,应改用 Patch 或重新 Get 后重试。
Reconcile 中不要缓存跨调用的可变状态(如把上次处理结果存在结构体字段里)。控制器可能并行处理不同对象,也可能重启,缓存会导致状态错乱。所有判断都应基于当次从 API Server 读取的最新对象。

把 Reconcile 理解为“对账函数”而非“事件回调”,把幂等当作硬性约束而非优化项,控制器运行时才能真正稳定地驱动集群向期望状态收敛。事件驱动负责“什么时候对账”,Reconcile 负责“怎么对账”,幂等性保证“对多少次账都不会出错”——三者缺一不可。