Kubernetes Operator 开发实战 从一个 CRD 到稳定 reconcile
Operator 的价值是把运维知识编码进控制器:声明期望状态,控制器持续把现实推到期望。听起来简单,写出"不抖动、不打架、可回滚"的 Operator 有一整套纪律。本文按 controller-runtime 的实际开发顺序走一遍。
一、CRD 设计的三条原则
type CacheSpec struct {
Replicas int32 `json:"replicas"`
Image string `json:"image"`
Resources corev1.ResourceRequirements `json:"resources,omitempty"`
// 不要用 map[string]string 塞配置,字段要有语义
Eviction *EvictionPolicy `json:"eviction,omitempty"`
}
type CacheStatus struct {
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
Phase string `json:"phase"` // Progressing/Ready/Degraded
Conditions []metav1.Condition `json:"conditions,omitempty"`
ReadyReplicas int32 `json:"readyReplicas"`
}
三条原则:
- Spec 里只放用户意图,Status 里放观测结果,两者绝不互写;
- 必须带
observedGeneration,否则客户端无法判断 status 是否对应最新 spec; - Status 用
Conditions表达状态,别自造一堆 bool 字段。
kubebuilder 一键生成骨架:
kubebuilder init --domain okgoes.com --repo github.com/okgoes/cache-operator
kubebuilder create api --group cache --version v1alpha1 --kind RedisCache
make generate manifests
二、Reconcile 的幂等纪律
控制器可能被并发、重入、乱序调用。所有逻辑必须写成"期望态 vs 实际态"的收敛,而不是"执行一系列动作"。
func (r *RedisCacheReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := ctrl.LoggerFrom(ctx)
var cr cachev1.RedisCache
if err := r.Get(ctx, req.NamespacedName, &cr); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
if !cr.DeletionTimestamp.IsZero() {
return r.finalize(ctx, &cr)
}
if err := r.ensureConfigMap(ctx, &cr); err != nil {
return ctrl.Result{}, fmt.Errorf("ensure configmap: %w", err)
}
dep, err := r.ensureDeployment(ctx, &cr)
if err != nil {
return ctrl.Result{}, fmt.Errorf("ensure deployment: %w", err)
}
ready := dep.Status.ReadyReplicas
if err := r.patchStatus(ctx, &cr, ready); err != nil {
return ctrl.Result{}, err
}
if ready < *cr.Spec.Replicas {
log.Info("waiting for replicas", "ready", ready)
return ctrl.Result{RequeueAfter: 10 * time.Second}, nil
}
return ctrl.Result{}, nil
}
关键点:
- 用
CreateOrUpdate或controllerutil.CreateOrPatch,并在 mutation 函数里只改你拥有的字段; - 给从属资源设
OwnerReference,级联删除靠 GC; - Status 更新走
Status().Patch(),避免与 Spec 写冲突。
三、错误处理决定稳定性
永久错误与暂时错误的区分是 Operator 最容易做错的地方。返回 error 会触发指数退避重入队;返回 RequeueAfter 是"定时再来一次"。
if err := r.ensureIngress(ctx, cr); err != nil {
if isValidError(err) {
// 用户写错配置:不要疯狂重试刷日志,写进 Condition 等用户改
meta.SetStatusCondition(&cr.Status.Conditions, metav1.Condition{
Type: "Ready", Status: metav1.ConditionFalse, Reason: "InvalidSpec",
Message: err.Error(), ObservedGeneration: cr.Generation,
})
_ = r.Status().Update(ctx, cr)
return ctrl.Result{}, nil
}
return ctrl.Result{}, err // 暂时错误:apiserver 抖动、配额未满等,交给退避
}
反例是把配置校验错误也 return err,结果每几秒重入、事件刷屏、workqueue_depth 飙升。
四、Finalizer 与外部资源
Operator 常管外部资源(云数据库、DNS 记录)。K8s 删除 CR 时,必须靠 finalizer 保证外部资源同步清理。
const finalizer = "cache.okgoes.com/finalizer"
func (r *RedisCacheReconciler) finalize(ctx context.Context, cr *cachev1.RedisCache) (ctrl.Result, error) {
if !controllerutil.ContainsFinalizer(cr, finalizer) {
return ctrl.Result{}, nil
}
if err := r.cloud.DeleteInstance(ctx, cr.Spec.InstanceID); err != nil && !isNotFound(err) {
return ctrl.Result{}, err // 清理失败要能重试,别吞掉
}
patch := client.MergeFrom(cr.DeepCopy())
controllerutil.RemoveFinalizer(cr, finalizer)
return ctrl.Result{}, r.Patch(ctx, cr, patch)
}
两条铁律:外部删除必须幂等(已删除视作成功);finalizer 清理失败必须返回 error,否则资源泄漏。
五、控制并发、限流与租约
多副本部署时必须靠 leader election 保证只有一个实例在 reconcile。
mgr, err := ctrl.NewManager(cfg, ctrl.Options{
LeaderElection: true,
LeaderElectionID: "cache-operator.okgoes.com",
LeaderElectionNamespace: "okgoes-system",
})
单实例内也要限并发,避免一次大规模变更打爆 apiserver:
controller.New("rediscache", mgr, controller.Options{
Reconciler: r, MaxConcurrentReconciles: 4,
RateLimiter: workqueue.NewMaxOfRateLimiter(
workqueue.NewItemExponentialFailureRateLimiter(100*time.Millisecond, 2*time.Minute),
&workqueue.BucketRateLimiter{Limiter: rate.NewLimiter(rate.Limit(20), 50)},
),
})
再加事件过滤,别为无关字段变化空转:
For(&cachev1.RedisCache{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})).
Owns(&appsv1.Deployment{}, builder.WithPredicates(predicate.Or(
predicate.GenerationChangedPredicate{},
predicate.LabelChangedPredicate{},
)))
六、可观测性与灰度
必备指标:
controller_runtime_reconcile_total{result="error"} # 错误率
controller_runtime_reconcile_time_seconds_bucket # 单次耗时 P99
workqueue_depth / workqueue_adds_total # 队列积压
告警就两条:rate(reconcile_total{result="error"}[5m]) > 0.01 与 workqueue_depth > 100 持续 5 分钟。
发布上遵守"CRD 版本先行、控制器随后":CRD 可以提前发(新增可选字段不破坏既有对象),控制器升级要能同时处理新旧 spec,回滚时靠 observedGeneration 判断是否需要重算。
小结
Operator 的核心不是"操作 K8s API",而是三件事:CRD 契约设计(Spec/Status 分离 + Conditions + observedGeneration)、reconcile 幂等且区分错误类型、并发与限流保护 API Server。加上 finalizer 正确清理外部资源和四个关键指标,控制器就能长期稳定运行。先用 kubebuilder 跑通脚手架,再把这四条纪律逐条落实,比一开始追求复杂功能重要得多。
评论(0)
登录 后参与评论。