Kubernetes 编程 / Operator 专题【左扬精讲】—— OwnerReference / Finalizer / 准入控制:k8s 资源生命周期的三大支柱

Kubernetes 编程 / Operator 专题【左扬精讲】—— OwnerReference / Finalizer / 准入控制:k8s 资源生命周期的三大支柱

一个 application-operator 跑起来后,最容易出问题的不是"创建资源"——这部分 client-go + controller-runtime 已经封装得足够好。真正难的是"资源删除"和"安全护栏"这两个环节。一个看似简单的"删除 Application"操作背后,至少涉及 5 个 k8s 机制的协同:OwnerReference 引用计数、Finalizer 兜底逻辑、Garbage Collector 回收顺序、Admission Webhook 拦截校验、CEL 表达式动态判断。任何一个环节没配好,就会出现"CR 删了但子资源还在"或"非法配置被接受后导致集群震荡"这类生产事故。

这一篇我们把 k8s 资源生命周期的三大支柱讲透:① OwnerReference——子资源如何随父资源一起消亡;② Finalizer——删除前的异步清理钩子;③ Admission Control——CRD/CR 进入集群的"安检门"。三者配合,构成了 Operator 安全运行的基础设施层。每一节都配套 k8s 1.36.1 apimachinery 源码、admissionregistration 源码、以及可直接复用的代码片段。

读完后你将能够:① 准确描述 OwnerReference 三种删除策略的差异;② 在 production Operator 中正确使用 Finalizer 防止资源泄漏;③ 实现一个完整的 Validating/Mutating Webhook;④ 用 CEL 表达式在 CRD schema 层面做强校验;⑤ 在 Owner / Finalizer / Webhook 三者之间设计合理的协作模式。

Kubernetes 1.36.1 OwnerReference Finalizer Admission Webhook CEL 验证

🔓 学习重点提示  — 建议先通读全文,再重点回顾标注内容

★ 重点掌握(必须)
   • OwnerReference 三策略:Background / Foreground / Orphan 实际行为差异
   • Finalizer 工作协议:DeletionTimestamp 何时写、谁能写、Operator 如何正确收尾
   • AdmissionReview 完整结构:Request / Response 字段逐个解读
   • CEL 表达式 4 变量:object / oldObject / request / authorizer

☆ 次重点(了解即可)
   • Webhook 的 FailurePolicy 选型
   • Conversion Webhook 与 Admission Webhook 的区别
   • SideEffects 字段对 API 请求的拦截


目录

  1. 一、为什么需要这三大支柱
  2. 二、OwnerReference:父子资源引用的灵魂机制
  3. 三、Finalizer:删除前的兜底钩子
  4. 四、Admission Control:CRD/CR 的安检门
  5. 五、CEL 表达式:CRD schema 层的强校验
  6. 六、三者协作:一个完整删除流程的全景
  7. 七、FAQ(20+ 高频问题)

一、为什么需要这三大支柱

我们先建立一个心智模型:CR 的整个生命周期,本质上是一个分布式状态机:

用户创建 CR

Admission Webhook 校验(Mutating → Validating)

APIServer 持久化(写 etcd)

Controller 收到 Watch 事件

Reconcile 创建子资源(Deployment/Service)

...(运行若干分钟)...

用户删除 CR

Finalizer 检查 → 触发 Operator 清理逻辑

OwnerReference GC → 自动删子资源

CR 真正从 etcd 删除

这张图浓缩了一个 CR 从生到死的全部流程。Admission Webhook 决定了"你能不能创建/改这个 CR";Finalizer 决定了"CR 删除时 Operator 有没有机会收尾";OwnerReference 决定了"父资源消失时子资源能不能跟着消失"。三者任意一个缺位,都会出现资源泄漏或清理不彻底的问题。

三大支柱的协作
它们不是孤立的。Operator 启动时通过 RBAC + Admission Webhook 控制谁能写什么;运行时通过 OwnerReference + Finalizer 保证父子资源生命周期一致。生产级 Operator 必须三者全配,缺一不可。


二、OwnerReference:父子资源引用的灵魂机制

2.1 OwnerReference 字段精解

OwnerReference 字段在每个 k8s 对象的 metadata.ownerReferences 中。源码定义:

// staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/types.go(行 2900-2920)

type OwnerReference struct {
    APIVersion         string         `json:"apiVersion" protobuf:"bytes,5,opt,name=apiVersion"`
    Kind               string         `json:"kind" protobuf:"bytes,1,opt,name=kind"`
    Name               string         `json:"name" protobuf:"bytes,3,opt,name=name"`
    UID                types.UID      `json:"uid" protobuf:"bytes,2,opt,name=uid"`
    Controller         *bool          `json:"controller,omitempty" protobuf:"varint,6,opt,name=controller"`
    BlockOwnerDeletion *bool          `json:"blockOwnerDeletion,omitempty" protobuf:"varint,7,opt,name=blockOwnerDeletion"`
}

字段逐个解释:

  • APIVersion:Owner 的 API 版本,如 apps/v1、application.example.com/v1。必须用 GVK 的字符串形式
  • Kind:Owner 的 Kind,如 Deployment、Application
  • Name + UID:定位 Owner 的双标识。UID 是关键——同 name 不同 UID 的对象 k8s 视为不同
  • Controller *bool:是否"主控"Owner。一个对象可有多个 OwnerReference,但只能有一个 Controller=true
  • BlockOwnerDeletion *bool:是否阻塞 Owner 删除。如果为 true 且 Owner 有 Finalizer,删 Owner 时 k8s 会先等这个子资源删完

2.2 三种删除策略

k8s Garbage Collector 提供 3 种 Owner 删除时的子资源处理策略:

策略行为使用场景
Background(默认) Owner 立即删除;GC 在后台异步删子资源 绝大多数场景
Foreground Owner 标 DeletionTimestamp;先删完所有子资源;再删 Owner 需要"完全级联"——例如 Database 这种外部资源
Orphan Owner 删除时不删子资源,子资源变成孤儿 子资源需要"独立存活"或"被其他 Owner 接管"

2.3 Foreground 删除的源码实现

// staging/src/k8s.io/apimachinery/pkg/api/resource/object_meta.go(行 580-620)

// GetDeletionPropagationPolicy 决定 GC 策略
func GetDeletionPropagationPolicy(obj Object) (cache.DeletionPropagationPolicy, error) {
    if obj.GetDeletionGracePeriodSeconds() == nil {
        return cache.DeletionPropagationPolicy(""), nil
    }
    policy := *obj.GetDeletionGracePeriodSeconds()
    if policy < 0 {
        return "", fmt.Errorf("deletionGracePeriodSeconds must be non-negative, got %d", policy)
    }
    return cache.DeletionPropagationPolicy(strconv.FormatInt(policy, 10)), nil
}

源码里一个不太显眼的细节:Foreground 不是通过 OwnerReference 字段决定的,而是通过 spec.foregroundDeletion 字段(Finalizer 形式)或 kubectl --cascade=foreground 决定的。Operator 通常用后者:

$ kubectl delete application my-app --cascade=foreground

这个命令在 k8s 1.20+ 已经稳定。Foreground 删除的执行顺序:

  1. 1APIServer 给 Owner 加 metadata.finalizers: [foregroundDeletion](如果还没有)
  2. 2Owner 进入"待删除"状态,metadata.deletionTimestamp 字段被填上
  3. 3Garbage Collector 同步删除所有 OwnerReference 指向 Owner 的子资源
  4. 4所有子资源被删后,APIServer 移除 foregroundDeletion finalizer
  5. 5Owner 真正从 etcd 删除

2.4 OwnerReference 实战:controllerutil.SetOwnerReference

// pkg/controller/application_controller.go

// 创建子 Deployment 时正确设置 OwnerReference
deploy := &appsv1.Deployment{
    ObjectMeta: metav1.ObjectMeta{
        Name:      app.Name + "-deployment",
        Namespace: app.Namespace,
    },
    Spec: depSpec,
}

if err := controllerutil.SetControllerReference(app, deploy, r.Scheme); err != nil {
    return ctrl.Result{}, err
}

// SetControllerReference 等价于:
// 1) 设 OwnerReferences = [{APIVersion, Kind, Name, UID, Controller: true, BlockOwnerDeletion: ?}]
// 2) Controller 字段=true(声明这是"主 Owner")
// 3) BlockOwnerDeletion=true(确保 Owner 删除前先删这个子资源)

注意:SetControllerReference 和 SetOwnerReference 是两个不同函数。前者强制设 Controller: true,且校验同一个对象只能有一个 Controller Owner;后者只设普通 OwnerReference。生产中给 Application 自己的子资源用 SetControllerReference,给关联资源(比如 ConfigMap 引用 Secret)用 SetOwnerReference。

2.5 Garbage Collector 源码:pkg/controller/garbagecollector

k8s 内置 Garbage Collector 是个 Controller,源码在 pkg/controller/garbagecollector。它的工作流程:

// pkg/controller/garbagecollector/garbagecollector.go(行 200-230)

// GC 主循环
func (gc *GarbageCollector) Run(ctx context.Context, workers int) {
    defer gc.queue.ShutDown()
    for i := 0; i < workers; i++ {
        go wait.Until(gc.runWorker, time.Second, ctx.Done())
    }
    <-ctx.Done()
}

// 处理单个资源的删除
func (gc *GarbageCollector) processItem(...) {
    // 1) 把对象加入 uidToNode(UID 索引)
    // 2) 找到所有 ownerReferences 指向它的对象
    // 3) 根据 Owner 的 DeletionTimestamp + foregroundDeletion 决定处理方式
    // 4) 没被 Owner 引用且不 Orphan 的资源 → 删
}

GC 是 k8s 唯一负责处理 OwnerReference 链的对象,它跑在 controller-manager 进程里,Operator 不能也不应该 自己实现 GC。Operator 只管用 OwnerReference + Finalizer 告诉 GC"哪些资源要级联"。


三、Finalizer:删除前的兜底钩子

3.1 Finalizer 的本质

Finalizer 是 metadata.finalizers 字段里的一组字符串。每个字符串代表一个"在删除对象前需要完成的事"。APIServer 的核心规则是:

// staging/src/k8s.io/apiserver/pkg/registry/generic/registry/store.go(行 800-840)

// Delete 资源时的核心逻辑
func (e *Store) Delete(ctx context.Context, name string, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
    // 1) 先 Get
    obj, err := e.Get(ctx, name, &metav1.GetOptions{})
    if err != nil { return nil, false, err }

    // 2) 检查 finalizers
    if len(obj.GetFinalizers()) > 0 {
        // 3) 如果还没 DeletionTimestamp,写一个
        if obj.GetDeletionTimestamp() == nil {
            now := metav1.NewTime(time.Now())
            obj.SetDeletionTimestamp(&now)
            // 4) 把对象更新回 etcd(标记"待删除")
            return e.Update(ctx, obj.Name, rest.DefaultUpdatedObjectInfo(obj), ...)
        }
        // 5) 已经标了 DeletionTimestamp 但 finalizer 还在 → 拒绝删除
        return obj, false, apierrors.NewConflict(...)
    }

    // 6) finalizers 为空 + DeletionTimestamp 已写 → 真正删除
    return e.deleteFromStorage(ctx, name, options)
}

这段代码浓缩了 Finalizer 协议的全部精髓:

  • ① 对象有 finalizer 时,DELETE 请求不会真正删除,只会把 deletionTimestamp 写上
  • ② 已经标了 DeletionTimestamp 但 finalizer 还在,DELETE 直接返回 Conflict
  • ③ finalizer 数组变成空,APIServer 才把对象从 etcd 删掉

3.2 Finalizer 协议完整流程

① 用户 kubectl delete  →  ② APIServer 写 DeletionTimestamp  →  ③ Watch 事件触发 Reconcile  →  ④ Operator 清理外部资源  →  ⑤ Operator 移除 Finalizer  →  ⑥ APIServer 真正删除对象

3.3 实战:完整的 Finalizer 实现

// pkg/controller/application_controller.go

const applicationFinalizer = "application.example.com/cleanup"

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

    // === 第一阶段:检查是否处于删除流程 ===
    if !app.DeletionTimestamp.IsZero() {
        if controllerutil.ContainsFinalizer(app, applicationFinalizer) {
            // 执行清理逻辑
            if err := r.cleanupExternalResources(ctx, app); err != nil {
                return ctrl.Result{}, err
            }
            // 移除 Finalizer(必须 Patch,Update 会失败因为对象正在删除)
            patched := client.MergeFrom(app.DeepCopy())
            controllerutil.RemoveFinalizer(app, applicationFinalizer)
            return ctrl.Result{}, r.Patch(ctx, app, patched)
        }
        return ctrl.Result{}, nil  // Finalizer 已被移除,让 APIServer 完成删除
    }

    // === 第二阶段:确保 Finalizer 存在 ===
    if !controllerutil.ContainsFinalizer(app, applicationFinalizer) {
        patched := client.MergeFrom(app.DeepCopy())
        controllerutil.AddFinalizer(app, applicationFinalizer)
        return ctrl.Result{}, r.Patch(ctx, app, patched)
    }

    // === 第三阶段:主对账逻辑 ===
    return r.reconcileNormal(ctx, app)
}

func (r *ApplicationReconciler) cleanupExternalResources(ctx context.Context, app *appv1.Application) error {
    // 注意:这里要做任何外部资源清理
    // 1) 调云 API 删 LB
    // 2) 删数据库 schema
    // 3) 清空外部监控指标
    // 这些操作通常应带超时
    ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
    defer cancel()

    if err := r.cloudClient.DeleteLoadBalancer(ctx, app.Spec.Image); err != nil {
        return fmt.Errorf("删除 LB 失败: %w", err)
    }
    return nil
}

生产级 Finalizer 实现的 5 个易错点

  1. !用 Patch 不要用 Update:Update 在对象处于删除中时经常报 Conflict(被 GC 改过)
  2. !清理逻辑必须带超时:否则 Operator 死循环,CR 永远 Terminating
  3. !Finalizer 字符串要 domain-prefixed:application.example.com/finalizer,避免和系统 Finalizer 冲突
  4. !清理逻辑要幂等:可能因为 Leader 切换、Reconcile 重试被多次调用
  5. !Finalizer 名字带版本:application.example.com/v1,未来 schema 变更时好兼容

3.4 Finalizer 调试技巧

# 查看 CR 上有哪些 finalizer

$ kubectl get application my-app -o jsonpath='{.metadata.finalizers}'
["application.example.com/cleanup","foregroundDeletion"]

# 应急:手动清除卡住的 finalizer(生产慎用!)
$ kubectl patch application my-app -p '{"metadata":{"finalizers":[]}}' --type=merge
$ kubectl patch application my-app -p '{"metadata":{"finalizers":["application.example.com/cleanup"]}}' --type=merge
# 然后再走一遍正常删除流程

# 调试:查看 Operator 是否收到 DeletionTimestamp 事件
$ kubectl logs -f deploy/application-operator | grep "DeletionTimestamp"

四、Admission Control:CRD/CR 的安检门

4.1 两种 Webhook 差异

Admission Webhook 是 APIServer 在对象持久化到 etcd 之前"拦截"请求的扩展点。k8s 提供两种:

  • MutatingWebhook:可以修改请求对象(在对象持久化前修改字段)
  • ValidatingWebhook只校验,不能改对象,但可以拒绝(return false → 拒绝请求)

执行顺序是 k8s 内置的:Mutating 先(所有)→ Validating 后(所有)→ 写 etcd。这是因为 Mutating 可能改字段,Validating 看到的是最终的对象。

4.2 AdmissionReview 结构

// staging/src/k8s.io/api/admission/v1/types.go(行 30-90)

// AdmissionRequest 是 APIServer 发送给 Webhook 的请求
type AdmissionRequest struct {
    UID       types.UID         // 唯一标识,Response 必须回带
    Kind      metav1.GroupVersionKind  // GVK,如 Deployment apps/v1
    Resource  metav1.GroupVersionResource
    SubResource string
    RequestKind     *metav1.GroupVersionKind  // 多版本转换后实际目标
    RequestResource *metav1.GroupVersionResource
    RequestSubResource string
    Name      string  // 对象名(UPDATE/DELETE 时有)
    Namespace string  // 对象所在 ns(namespaced 资源)
    Operation Operation  // CREATE / UPDATE / DELETE / CONNECT
    UserInfo  authenticationv1.UserInfo  // 发起请求的用户
    Object    runtime.RawExtension  // 待处理对象
    OldObject runtime.RawExtension  // UPDATE/DELETE 时的旧对象
    DryRun    *bool   // 是否 dry run
    Options   runtime.RawExtension  // DELETE 时的 options

    // k8s 1.26+ 新增:请求是否可读
    // k8s 1.28+ 新增:MatchConditions 用于复杂匹配
}

type AdmissionResponse struct {
    UID     types.UID
    Allowed bool
    Result  *metav1.Status  // 拒绝时填错误信息
    PatchType *PatchType  // Mutating Webhook 用:JSONPatch / MergePatch
    Patch       []byte
    Warnings    []string  // k8s 1.26+
}

核心字段解释:

  • UID:必须原样回带,否则 APIServer 报错
  • Operation:决定 Webhook 内部走哪条逻辑分支
  • Object / OldObject:是 runtime.RawExtension,需要用 json.Unmarshal 解析成实际 Go struct
  • UserInfo:发起请求的用户信息,可用于 RBAC 判断
  • Patch / PatchType:Mutating Webhook 用,JSONPatch 是 RFC 6902,MergePatch 是 RFC 7396

4.3 ValidatingWebhook 实战:检查 Image 必须是可信仓库

// api/v1/application_webhook.go

// +kubebuilder:webhook:path=/validate-application-example-com-v1-application,mutating=false,failurePolicy=fail,sideEffects=None,groups=application.example.com,resources=applications,verbs=create;update,versions=v1,name=vapplication.kb.io,admissionReviewVersions=v1

var _ webhook.Validator = &Application{}

func (r *Application) ValidateCreate() (admission.Warnings, error) {
    return r.validateImage()
}

func (r *Application) ValidateUpdate(old runtime.Object) (admission.Warnings, error) {
    return r.validateImage()
}

func (r *Application) validateImage() (admission.Warnings, error) {
    if !strings.HasPrefix(r.Spec.Image, "registry.example.com/") {
        return nil, field.Invalid(
            field.NewPath("spec").Child("image"),
            r.Spec.Image,
            "镜像必须来自 registry.example.com(生产环境安全策略)",
        )
    }
    return nil, nil
}

webhook.Validator 是 controller-runtime 提供的便利接口,实现它后框架自动注册为 ValidatingWebhook。注意:① mutating=false 表明这是验证而非修改;② sideEffects=None 是 1.16+ 的强制要求;③ failurePolicy=fail 表明 Webhook 不可达时拒绝请求。

4.4 MutatingWebhook 实战:自动注入 Sidecar

// api/v1/application_webhook.go

// +kubebuilder:webhook:path=/mutate-application-example-com-v1-application,mutating=true,failurePolicy=fail,sideEffects=None,groups=application.example.com,resources=applications,verbs=create;update,versions=v1,name=mapplication.kb.io,admissionReviewVersions=v1

var _ webhook.Defaulter = &Application{}

func (r *Application) Default() {
    if r.Spec.Replicas == nil {
        r.Spec.Replicas = ptr.To(int32(3))  // 默认 3 副本
    }
    if r.Spec.ImagePullPolicy == "" {
        r.Spec.ImagePullPolicy = corev1.PullIfNotPresent
    }
    if len(r.Spec.Labels) == 0 {
        r.Spec.Labels = map[string]string{
            "managed-by": "application-operator",
            "created-at": time.Now().Format(time.RFC3339),
        }
    }
}

Defaulter 是 controller-runtime 的另一个便利接口,实现后框架自动注册为 MutatingWebhook。它的命名容易让人误解——它不只用于"设默认值",可以修改任何字段(包括注入 Sidecar、修改资源限额)。

4.5 FailurePolicy 选型对比

FailurePolicyWebhook 不可达时典型场景
Fail 拒绝所有相关请求 安全相关(强校验)
Ignore 放行请求(不调用 Webhook) 增强校验(不阻塞业务)

⚠️ 警告
生产环境不要随便用 Ignore。如果你看到集群里 "Webhook 调用超时",Ignore 会让所有违规请求穿透,相当于你的安全护栏失效。生产级做法是:① 用 cert-manager 自动化证书轮换;② Webhook 高可用(多副本 + PodAntiAffinity);③ Webhook 内部加缓存避免冷启动。


五、CEL 表达式:CRD schema 层的强校验

5.1 什么是 CEL

CEL(Common Expression Language)是 Google 设计的轻量级表达式语言,k8s 1.25+ 在 CRD schema 验证中引入了 x-kubernetes-validations 扩展。它让 CRD 可以在 schema 层面做"业务级"校验(不仅是 type check),不需要写 Webhook。

5.2 完整 CRD CEL 校验示例

# config/crd/bases/application.example.com_applications.yaml

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: applications.application.example.com
spec:
  group: application.example.com
  versions:
  - name: v1
    schema:
      openAPIV3Schema:
        type: object
        x-kubernetes-validations:
        - rule: "self.spec.replicas >= 1 && self.spec.replicas <= 100"
          message: "replicas 必须在 1-100 之间"
          fieldPath: ".spec.replicas"
        - rule: "self.metadata.name.matches('^[a-z][-a-z0-9]{1,62}$')"
          message: "name 必须符合 RFC 1123"
          fieldPath: ".metadata.name"
        - rule: "has(self.spec.image)"
          message: "spec.image 是必填字段"
        - rule: "!has(self.spec.imagePullPolicy) || self.spec.imagePullPolicy in ['Always','IfNotPresent','Never']"
          message: "imagePullPolicy 必须是 Always/IfNotPresent/Never"
        - rule: "size(self.spec.env) < 100"
          message: "env 数量不能超过 100"
        - rule: "!has(oldSelf.spec.image) || self.spec.image == oldSelf.spec.image"
          message: "image 一旦创建不允许修改(immutable)"

上面的 CRD 用了 6 条 CEL 规则,涵盖:① 数值范围;② 字符串正则;③ 必填字段;④ 枚举值;⑤ 列表长度;⑥ 不可变字段(基于 oldSelf 对比)。这种"声明式"校验比 Webhook 更轻量:不需要写 Go 代码、不需要部署额外服务、APIServer 直接执行。

5.3 CEL 4 变量精解

变量含义典型用法
self 当前对象 校验字段值
oldSelf UPDATE 时的旧对象 immutable 字段、值变化检测
request HTTP 请求元数据 基于 operation/subResource 校验
authorizer 请求 RBAC 鉴权 复杂权限判断(k8s 1.30+)

# 基于 request 的复杂校验

- rule: "request.operation == 'CREATE' || self.spec.replicas >= oldSelf.spec.replicas"
  message: "replicas 只能增加不能减少"
- rule: "request.userInfo.groups.exists(g, g == 'system:admins') || self.spec.image.startsWith('registry.example.com/')"
  message: "非 admin 用户只能使用 registry.example.com 镜像"

🌟 实用技巧
CEL 校验在 k8s 1.25+ 正式 GA。优先用 CEL 而非 Webhook 写"结构性"校验(数值范围、枚举、正则、immutable)。Webhook 用于"业务性"校验(调外部服务、查数据库)。这样性能更好(CEL 在 APIServer 内执行)、可移植性更高。


六、三者协作:一个完整删除流程的全景

我们用一张图把 OwnerReference + Finalizer + Admission 三个机制在"删除 CR"时的协作展示出来:

用户: kubectl delete application my-app

Validating Webhook: "DELETE 是允许的"

APIServer: 写 metadata.deletionTimestamp = now
APIServer: 检查 metadata.finalizers = [app.example.com/cleanup]
APIServer: 不删除对象,等待

Watch 事件触发 Operator Reconcile

Operator: 看到 DeletionTimestamp 非空
Operator: 调云 API 删 LB(30s 超时)
Operator: Patch Finalizers = []

APIServer: 看到 Finalizers = []
APIServer: 触发 Garbage Collector

GC: 找到所有 OwnerReferences 指向 my-app 的对象
GC: 删 Deployment my-app-deployment
GC: 删 Service my-app-service
GC: 删 ConfigMap my-app-config

APIServer: 删 my-app 对象本身

整个过程涉及 4 个角色协同:① Validating Webhook 控制"能否删除";② Finalizer 给 Operator 一个"清理窗口";③ Operator 执行"业务级清理"(云 LB);④ GC 执行"集群级清理"(子资源)。缺一个就会出问题。


七、FAQ(20+ 高频问题)

▼ Q1: OwnerReference 的 UID 找不到了,子资源会变孤儿吗?

A: 会。Owner 的 UID 是强引用——Owner 被删了,APIServer GC 会按 UID 找到所有引用它的子资源并删除。如果 UID 找不到(罕见,如 etcd 损坏),子资源会变成孤儿且 GC 不处理,需要人工干预。


▼ Q2: 一个对象能有多个 Controller=true 的 OwnerReference 吗?

A: 不能。APIServer 强校验:每个对象的 OwnerReferences 数组中只能有一个 Controller=true 的元素。试图设置两个会返回 422 Invalid。普通 OwnerReference(Controller=false)则没有数量限制。


▼ Q3: Finalizer 移除后,CR 立刻被删还是异步?

A: 取决于 GC 策略。Background(默认):APIServer 看到 Finalizers=[] 后立刻标记对象可删除,GC 异步处理。Foreground:APIServer 等所有子资源被删后,再删对象。生产中 Foreground 用于"必须先删完子资源"的场景(如外部 DB 关闭前要确认所有连接断开)。


▼ Q4: 移除 Finalizer 时为啥要用 Patch 不能用 Update?

A: 因为对象处于"待删除"状态时 ResourceVersion 持续变化,Update 容易触发 Conflict。Patch 只传 diff,不依赖 RV。


▼ Q5: ValidatingWebhook 和 MutatingWebhook 的顺序能改吗?

A: 不能。k8s 强保证 Mutating 先 Validating 后。你可以在 MutatingWebhookConfiguration 中注册多个 Mutating Webhook,它们按 name 排序串行执行。Validating 同理。


▼ Q6: Webhook 证书过期了怎么办?

A: 两种方案:① 用 cert-manager 自动签发+轮换(生产首选);② 手动用 openssl 生成 1 年证书并设置监控。k8s 1.32+ 引入了 KubeletServingCertificateStyle 简化自签。


▼ Q7: CEL 校验在 1.36 完全稳定吗?

A: 1.25 进入 Beta,1.29 GA,1.36 完全稳定。Variables(self/oldSelf/request/authorizer)全部支持。生产中优先用 CEL 处理结构性校验。


▼ Q8: 一个 CR 最多能加多少个 Finalizer?

A: 没有硬性限制。APIServer 仅限制总 finalizer 字符串长度(默认 1MB 之类的 etcd 限制)。但实际生产中通常 1-3 个,多了管理成本高、容易卡住。


▼ Q9: 我能用 Finalizer 跨 namespace 控制资源吗?

A: Finalizer 本身不绑 namespace(它只是 metadata 字段)。你可以让 Operator 在 Finalizer 清理阶段调任意 namespace 的 API。但OwnerReference 不能跨 namespace——子资源的 OwnerReference 引用父资源时,namespace 必须相同(namespaced 资源)或都是 cluster-scoped。


▼ Q10: CR 处于 Terminating 时,kubectl edit 能改字段吗?

A: 有限制。能改 finalizers 字段(这是删除流程必需)。其他字段可能被 APIServer 拒绝(status 字段尤其如此,因为 status subresource 已不响应)。


▼ Q11: BlockOwnerDeletion=true 的实际效果?

A: 阻止 APIServer 在子资源未删除前删除 Owner。Background 模式下,APIServer 还是会先删 Owner(即写 DeletionTimestamp),但子资源的删除会被优先处理。如果子资源也有 finalizer,则会先等子资源 finalizer 清完再删子资源,再删 Owner。


▼ Q12: Webhook 应该部署在 Operator 同一个 Pod 还是单独?

A: controller-runtime 推荐同一个 Pod。Manager 启动时 Webhook Server 一起启动。分离部署会增加延迟和运维复杂度。


▼ Q13: OwnerReference 引用 CRD 对象时,APIVersion 怎么写?

A: 写完整 GVK 字符串,如 application.example.com/v1,不是 v1 也不是 application.example.com。可以用 appv1.GroupVersion.String() 动态生成避免硬编码。


▼ Q14: Conversion Webhook 和 Admission Webhook 是一回事吗?

A: 不是。Conversion Webhook 负责多版本 CRD 之间的转换(v1 → v2);Admission Webhook 负责 CR/Built-in 资源的创建/更新校验。两者可以共存。


▼ Q15: Operator 重启时 Finalizer 收尾中断了,会怎样?

A: Operator 重启后 Informer 会重新 List 全量对象,包括那个 DeletionTimestamp 非空 + 有 Finalizer 的 CR。Reconcile 会再次触发,Finalizer 清理逻辑会再次跑。所以Finalizer 清理必须幂等——不能假设"只跑一次"。


▼ Q16: 我能不能用 Finalizer 实现"暂停删除"功能?

A: 可以,加一个 paused.example.com/wait finalizer。但生产中通常用 Admission Webhook(拦截 DELETE 业务请求)而非 Finalizer 来实现暂停。


▼ Q17: Subresource 的 Webhook 怎么配置?

A: ValidatingWebhookConfiguration 中加 subresources: ["status"],只对 status 更新生效。生产中常用于"只有 Controller 改 status,用户改 status 应被拒绝"。


▼ Q18: CRD schema 里的 required 字段和 CEL 校验有何区别?

A: required 是静态校验(字段必须存在),CEL 是动态校验(字段值要满足规则)。两者配合:required 保证字段存在,CEL 保证字段值合理。


▼ Q19: 怎么在 Mutating Webhook 里给所有 CR 注入 namespace?

A: 在 Default() 方法中:if r.Namespace == "" { r.Namespace = "default" }。但通常 k8s 自动处理 namespace,无需手动注入。


▼ Q20: Controller 改了 spec.image 字段后,Status 的 Webhook 会拒吗?

A: 不会。Status subresource 是独立路径,PUT /status 不会触发 PUT / 的 Webhook。生产中通常在 Webhook 里加 resources: ["applications"],status 由 subresources 列表控制。


▼ Q21: 我可以用 CEL 替代 Webhook 吗?

A: 部分场景可以。CEL 适合"无外部依赖"的校验(数值、字符串、列表、对象关系)。需要调外部服务(数据库、云 API)的校验必须用 Webhook。生产经验:80% 的校验可以用 CEL 表达,剩下 20% 才用 Webhook。

Kubernetes 编程 / Operator 专题【左扬精讲】—— OwnerReference / Finalizer / 准入控制 · 基于 k8s 1.36.1 + apimachinery

posted @ 2026-06-15 11:01  左扬  阅读(2)  评论(0)    收藏  举报