Go 控制器限速:Reconcile 失败时,别把 API Server 打满
一、控制器不是越快重试越可靠
Go 写 Kubernetes 控制器时,Reconcile 失败后通常会重新入队。这个机制很方便,但如果错误来自外部依赖、权限配置或资源冲突,快速重试只会制造更多压力。严重时,控制器会不停访问 API Server 和下游服务,把小故障放大。
控制器限速的目标,是让系统有节奏地恢复。可恢复错误可以退避重试,不可恢复错误要记录事件并停止热循环。Reconcile 要做到可重复执行,但也要懂得慢下来。
二、错误类型决定入队策略
Reconcile 的错误不应全部返回err。可以区分临时错误、配置错误和等待状态。不同错误对应不同 requeue。
flowchart TD A[Reconcile] --> B[读取对象] B --> C{错误类型} C -->|对象不存在| D[结束] C -->|临时依赖失败| E[退避重试] C -->|配置非法| F[记录事件并停止热重试] C -->|等待资源| G[固定间隔检查] E --> H[Rate Limiter] G --> H这样控制器不会因为一个错误配置持续冲击集群。
三、实现时明确返回 Result 和 error 的语义
下面示例展示一个简化处理方式。临时错误返回 error,让队列限速器接管;配置错误记录状态后不再立即重试。
func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { obj := &aiv1.ModelService{} if err := r.Get(ctx, req.NamespacedName, obj); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } if err := validateSpec(obj.Spec); err != nil { r.recorder.Event(obj, corev1.EventTypeWarning, "InvalidSpec", err.Error()) return ctrl.Result{}, nil } if err := r.ensureDeployment(ctx, obj); err != nil { return ctrl.Result{}, fmt.Errorf("ensure deployment: %w", err) } return ctrl.Result{RequeueAfter: 30 * time.Second}, nil }这里对非法配置不返回 error,是为了避免热循环。用户修改 spec 后,watch 事件会再次触发 Reconcile。
四、限速还要看全局并发和外部依赖
controller-runtime 可以配置最大并发 Reconcile。这个值不是越大越好。控制器如果会调用云厂商 API、模型仓库或对象存储,就要按下游容量设置并发。
还要监控队列长度、Reconcile 延迟、错误率和 API Server 请求量。只看控制器进程是否运行,没有意义。队列堆积说明处理能力不足,错误率升高说明可能进入重试风暴。
最后,状态更新也要限频。频繁写 status 会增加 etcd 压力。只有状态真正变化时才更新,并使用 patch 减少冲突。
五、总结
Go 控制器限速要从错误分类开始。临时错误退避重试,非法配置记录事件后等待用户修改,等待状态用固定间隔检查。Reconcile 并发和状态更新都要受控。控制器的可靠性不是一直重试,而是在失败时不把集群拖下水。