Kubernetes 编程 / Operator 专题【左扬精讲】—— k8s Finalizers 深度解析:对象的生命周期与删除控制
Kubernetes 编程 / Operator 专题【左扬精讲】—— k8s Finalizers 深度解析:对象的生命周期与删除控制
在 k8s 集群中,删除一个资源并不像数据库里 delete 一行记录那样"说没就没"。
当你执行 kubectl delete deployment my-app 时,背后其实发生了两件大事:
1. 先是"告诉集群这个东西要被删了,相关的组件请做好准备"(finalization 阶段)
2. 然后才是"从 etcd 里真正抹掉这条记录"(deletion 阶段)
Finalizers 就是连接这两个阶段的关键机制——它是一把锁,锁住了资源,直到所有依赖它的外部系统都清理干净,才放行删除。
本文基于 k8s v1.36.1 源码,全面解析 finalizers 的概念、原理、实操和避坑指南。
★ 理解 Finalizers 的本质:它是一把"删除前置条件锁",不是"删除钩子"
★★ 掌握 Finalization 和 Deletion 两个阶段的区别与联系
★★★ 理解 OwnerReference 和 GarbageCollector 与 Finalizers 的配合关系
★★ 理解 Foreground / Background / Orphan 三种删除策略的差异
★ 能够正确在 Operator 中使用 Finalizers 进行资源清理
📋 文章目录
- 一、概念认知:什么是 Finalizers
- 二、价值原理:为什么需要 Finalizers
- 三、实操落地:如何在 YAML 中使用 Finalizers
- 四、源码精讲:Finalizers 的实现机制
- 五、踩坑避坑:Finalizers 生产环境陷阱
- 六、高频答疑:20 个常见问题
一、概念认知:什么是 Finalizers
你可以把 Finalizers 想象成酒店退房的总台检查单。
当一位客人(k8s 对象)说"我要退房了"(kubectl delete),总台不会立刻把他的房间从系统里清除——而是先在这张检查单上逐项核对:行李托运了吗?房卡交还了吗?迷你吧账单结清了吗?
只有所有项目都打勾(finalizers 列表为空),酒店才会真正把房间释放给下一位客人(对象从 etcd 中删除)。
在 k8s 中,Finalizers 是一个字符串数组,存储在每个对象的 metadata.finalizers 字段内。
每当你发起资源删除请求时,apiserver(API 服务器) 会依次执行两个核心操作:
① 给资源写入 deletionTimestamp 并赋值当前时间,标记删除流程正式启动;
② 将对象持续保存在 etcd 中,禁止彻底清除,前提条件是 finalizers 列表未被全部清空。
核心特征
Finalizers 有以下几个关键特征,理解了它们你就掌握了 finalizers 的一半:
- 声明式而非代码式:Finalizers 只是一个字符串名字,不是直接执行的代码逻辑。真正执行清理工作的是控制器(Controller)或 Operator——它们通过监听带有 deletionTimestamp 的对象,主动做清理,然后再把 finalizer 从列表中移除。
- 非有序,不强制顺序处理:k8s 源码注释明确指出,finalizers 的处理顺序是不保证的,如果强制要求顺序反而可能引入死锁(deadlock)。这意味着控制器不应该依赖"我的 finalizer 一定在其他 finalizer 之前/之后被处理"这一假设。
- 所有有权限的 actor 都可以修改:finalizers 是一个共享字段,任何有权限修改对象的 actor(控制器、用户、webhook)都可以往里加或从里删。这既是灵活性,也是一个潜在的风险点。
- 跨版本/跨组件协作:Finalizers 是 k8s 对象生命周期管理的通用基础设施,不依赖特定资源类型。无论是内置资源(Deployment、StatefulSet)还是 CRD(CustomResource),都可以使用 Finalizers。
与 OwnerReference 的区别
新手极易混淆 OwnerReference(所有者引用)与 Finalizers。
一句话区分核心定位:OwnerReference 代表「这个对象归谁管理」,Finalizers 代表「删除前需要通知谁清理资源」。
OwnerReference 是 GarbageCollector(垃圾回收器) 的判断依据:父对象发起删除时,GC 会根据该字段自动决定同步删除或保留全部子对象;
而 Finalizers 相当于一道删除门卫锁,强制等待所有外部资源清理逻辑执行完毕,才允许对象真正从 etcd 中清除。
二者可协同配合使用:OwnerReference 负责解决父子资源级联依赖,Finalizers 负责解决外部资源异步清理等待。
二、价值原理:为什么需要 Finalizers
没有 Finalizers 之前,k8s 删除一个对象是"一删了之"——对象从 etcd 消失,但外部关联资源(如云上的持久卷、网络资源、第三方服务的订阅记录)可能根本不知道这件事。运维人员需要手动做大量清理工作,而且稍有不慎就会留下"孤儿资源"(orphaned resources)——既占用成本又无法管理。
Finalizers 的设计目的,就是让"对象被标记为删除"和"对象真正从存储中消失"这两个事件解耦。在这两个事件之间,控制器有足够的时间执行清理逻辑——取消云资源的订阅、删除关联数据、发送注销请求等。清理完成后,控制器主动把自己的 finalizer 从列表中移除,当列表为空时,k8s 才真正删除对象。
下面这张表对比了"无 Finalizers 传统方案"和"Finalizers 方案"的差异:
| 对比维度 | 传统方案(无 Finalizers) | Finalizers 方案 |
|---|---|---|
| 删除时机 | 对象立即从 etcd 消失,外部资源来不及清理 | 对象标记 deletionTimestamp 后保留,外部资源有清理窗口期 |
| 孤儿资源风险 | 高——对象没了,但云资源/数据库记录可能还留着 | 低——清理逻辑在 finalizer 被移除前必须执行完毕 |
| 运维负担 | 高——需要手动清理,或者依赖外部系统的 TTL 机制 | 低——控制器自动执行清理逻辑,无需人工介入 |
| 适用场景 | 临时对象、无外部依赖的简单资源 | 持有外部资源(PVC、Service、CRD 自定义资源)的对象 |
| 故障恢复能力 | 差——对象删除后无法追踪其关联资源 | 好——对象始终存在于 API 中,可被控制器持续监控直到清理完成 |
典型使用场景
- 持久卷(PVC)保护:删除一个 PVC 前,必须确保所有 Pod 都已不再使用它,否则会导致数据丢失。kubernetes.io/pv-protection finalizer 会阻止 PVC 在仍有使用者时被删除。
- 云资源解绑:删除一个 LoadBalancer 类型 Service 时,cloud provider 需要先通知云平台撤销负载均衡器——这个过程是异步的,需要等待。service.kubernetes.io/load-balancer-cleanup finalizer 就是干这件事的。
- CRD 自定义资源清理:当你删除一个自定义资源(如 MySQL 数据库实例)时,Operator 需要先清理底层数据库、备份、用户账号等资源,然后才能让 CR 真正消失。
- 数据删除确认:某些高风险操作(如删除集群)需要在 finalization 阶段向管理员发送确认通知,确保不是误操作。
三、实操落地:如何在 YAML 中使用 Finalizers
3.1 给 CRD 资源添加 Finalizers
最常见的实操场景是在编写 Kubernetes Operator(通常用 kubebuilder 或 controller-runtime 框架)时,给 CR(CustomResource)添加 finalizer。以下是一个最小可运行的 YAML 示例,演示如何在创建时自动注入 finalizer:
// my-database.yaml (k8s v1.36.1 验证通过)
apiVersion: myapp.io/v1
kind: Database
metadata:
name: production-db
namespace: default
finalizers:
- database.myapp.io/cleanup-pvcs # 清理关联的持久卷声明
- database.myapp.io/cleanup-backups # 清理云备份
- database.myapp.io/revoke-credentials # 吊销数据库凭证
spec:
engine: postgres
version: "15"
storage: 100Gi
上面这个 YAML 的关键字段解读:metadata.finalizers 是一个字符串数组,每个字符串是一个"标识符",通常用域名格式(逆向域名)命名以避免冲突,比如 database.myapp.io/cleanup-pvcs 表示"这个 finalizer 由 myapp.io 域下的 database 控制器处理,负责清理 PVC"。当你 apply 这个 YAML 后,对象被创建,同时 finalizers 列表被设置。
3.2 删除资源的三种策略
当你用 kubectl delete 删除一个带 OwnerReference 或 finalizers 的资源时,可以通过 propagationPolicy 控制子资源的删除行为。这个参数有三个取值:
// Foreground 级联删除(默认,前台删除)
kubectl delete deployment my-app --grace-period=30 --cascade=foreground # 或者 kubectl delete deployment my-app --grace-period=30 --propagation-policy=Foreground
Foreground 策略会同时给父对象加上 kubernetes.io/foregroundDeletion finalizer,使得父对象必须等所有子对象(ReplicaSet、Pod)都被删除后,才能真正从 etcd 消失。这是删除 Deployment 时的默认行为。
// Background 级联删除(后台删除)
kubectl delete deployment my-app --cascade=background # 或者 kubectl delete deployment my-app --propagation-policy=Background
Background 策略下,父对象会立即从 etcd 消失(deletionTimestamp 设置后立即删除),但 GarbageCollector 在后台异步删除子对象。这种方式更快,但子对象在后台删除期间会处于 Terminating 状态。
// Orphan 保留子对象(不级联删除)
kubectl delete deployment my-app --cascade=orphan # 或者 kubectl delete deployment my-app --propagation-policy=Orphan
Orphan 策略下,父对象被删除后,子对象会被保留下来,成为"孤儿"——它们不再有 OwnerReference,GarbageCollector 不会自动删除它们。如果你想保留某些工作负载(迁移场景常用),这个策略很有用。
3.3 查看和诊断 Finalizers
以下命令用于诊断 finalizers 相关问题:
$ kubectl get pvc my-pvc -o yaml | grep finalizers
# 输出示例:显示该 PVC 带有 pv-protection finalizer
metadata:
finalizers:
- kubernetes.io/pv-protection
deletionTimestamp: "2026-06-16T10:00:00Z"
deletionGracePeriodSeconds: 30
$ kubectl describe pvc my-pvc | grep -E "Finalizers|Deletion|Events"
# 输出示例 Finalizers: [kubernetes.io/pv-protection] Deletion Timestamp: 2026-06-16 10:00:00 Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal ExternalStorageProvider 2m persistentvolume-controller waiting for a volume to be created, either by external provisioner or manual creation. Warning VolumeMountConflict 5s kubelet mountVolume.volumeControllerOperation failed: ...
$ kubectl get pods --field-selector=status.phase=Terminating
# 查看所有处于 Terminating 状态的 Pod(这些 Pod 可能正在等待 finalizers 被清除) NAME READY STATUS RESTARTS AGE web-0 1/1 Terminating 0 5d
$ kubectl patch deployment my-app -p '{"metadata":{"finalizers":[{"key":"myapp.io/cleanup","value":null}]}}' --type=merge
# 手动移除 finalizer(生产环境慎用,等同于跳过清理逻辑) deployment.apps/my-app patched
3.4 Operator 中的 Finalizer 使用模式
在使用 controller-runtime 或 client-go 编写 Operator 时,finalizer 的标准使用模式如下:
- 创建时添加 finalizer:在 Reconcile 函数中检测到对象没有我的 finalizer 时,用 patch 或 update 将 finalizer 添加到 metadata.finalizers 列表中。这样即使后续有人删除对象,控制器也有机会做清理。
- 监听 deletionTimestamp:当 Reconcile 收到一个带有 deletionTimestamp 的对象时,说明对象正在被删除。此时执行所有必要的清理工作(如调用云 API 撤销资源)。
- 清理完成后移除 finalizer:所有清理工作完成后,用 patch 将自己的 finalizer 从列表中移除。当 finalizers 列表为空时,k8s 会自动将对象从 etcd 中删除。
- 幂等性处理:finalizer 的添加和移除操作必须是幂等的(执行多次和执行一次效果相同),因为 Reconcile 函数可能被多次调用。
四、源码精讲:Finalizers 的实现机制
本节深入 k8s v1.36.1 源码,从类型定义到删除处理链路,逐层解析 finalizers 的完整实现。下面的架构图展示了从用户执行 kubectl delete 到对象真正从 etcd 消失的完整数据流:
用户 kubectl delete deployment my-app │ ▼ kube-apiserver: DELETE handler (staging/src/k8s.io/apiserver/pkg/endpoints/handlers/delete.go) │ 解析 DeleteOptions(GracePeriodSeconds / PropagationPolicy) │ 调用 Store.Delete() ▼ generic registry Store.Delete() (staging/src/k8s.io/apiserver/pkg/registry/generic/registry/store.go) │ 检查对象是否已有 deletionTimestamp(pendingGraceful)? │ ├── YES → finalizeDelete()(走 finalization 路径) │ └── NO → updateForGracefulDeletionAndFinalizers() │ ├─ deletionFinalizersForGarbageCollection() 计算需要加/减哪些 finalizer │ ├─ 设置 deletionTimestamp(原子写入 etcd) │ └─ 如果有 pending finalizers → 返回,不删除 ▼ 对象写入 etcd (deletionTimestamp 已设置, finalizers 列表已更新) │ ▼ GarbageCollector 控制器 (pkg/controller/garbagecollector/garbagecollector.go) │ 监听被删除对象及其所有 dependent(子对象) │ 根据 PropagationPolicy 决定: │ Foreground → 先删除所有子对象,等全部消失后再删父对象 │ Background → 父对象立即消失,子对象后台异步删除 │ Orphan → 子对象变成孤儿(移除 OwnerReference) ▼ Controller / Operator 监听带 deletionTimestamp 的对象 │ Reconcile 检测到 DeletionTimestamp != nil │ 执行清理逻辑(取消云订阅、删除关联资源) │ 清理完成后 patch 对象,移除自己的 finalizer ▼ etcd: 当 finalizers 列表为空时,Store.Delete() 最终从存储中删除对象
从图中可以看到,整个删除流程分两条并行路径:一条是 GarbageCollector 控制父子资源的级联删除(由 propagationPolicy 驱动),另一条是控制器监听自己的 finalizer 并执行清理逻辑(由对象上的 finalizers 列表驱动)。这两条路径在对象真正从 etcd 消失时汇合。下面我们从源码层面逐层解析。
4.1 ObjectMeta 中的 Finalizers 定义
Finalizers 是 ObjectMeta 的一个普通字段,而不是某种特殊机制。理解这一点非常重要——finalizers 就是存在 etcd 里的一条普通数据,apiserver 和控制器都通过读写这个字段来协作。
// staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/types.go (k8s v1.36.1)
// 行 210-236:ObjectMeta 中与删除相关的字段 DeletionTimestamp *Time `json:"deletionTimestamp,omitempty" protobuf:"bytes,9,opt,name=deletionTimestamp"` // DeletionTimestamp 是 RFC 3339 格式的时间戳,当用户请求优雅删除时由 apiserver 设置。 // 当 finalizers 列表非空时,即使这个时间戳已设置,对象也不会从 etcd 中消失。 // 只有 finalizers 为空时,k8s 才允许对象真正被删除。 DeletionGracePeriodSeconds *int64 `json:"deletionGracePeriodSeconds,omitempty" protobuf:"varint,10,opt,name=deletionGracePeriodSeconds"` // 优雅删除的宽限期(秒数)。仅当 deletionTimestamp 也被设置时才生效。 // 如果为 nil,则使用资源类型的默认宽限期。设为 0 表示立即删除。
// staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/types.go (k8s v1.36.1)
// 行 263-279:ObjectMeta.Finalizers 字段定义 // Must be empty before the object is deleted from the registry. Each entry // is an identifier for the responsible component that will remove the entry // from the list. Finalizers []string `json:"finalizers,omitempty" patchStrategy:"merge" protobuf:"bytes,14,rep,name=finalizers"` // finalizers 是一个字符串切片,JSON 中的 key 为 "finalizers",支持 patchStrategy:merge(合并 patch)。 // patchStrategy:merge 意味着 patch 操作会合并而非替换整个列表——你可以只添加或只移除其中一个 finalizer。 // 注意:k8s 源码注释明确指出,finalizers 的处理顺序是不保证的(Order is NOT enforced), // 因为强制顺序可能导致死锁(deadlock)。任何有权限的 actor 都可以重新排列 finalizers 列表。
4.2 DeletionPropagation 与 DeleteOptions
// staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/types.go (行 531-547,k8s v1.36.1)
// DeletionPropagation 决定了删除对象时垃圾回收器如何处理其依赖项
type DeletionPropagation string
const (
// DeletePropagationOrphan = "Orphan"
// 垃圾回收器将依赖项设为孤儿——即移除它们的 OwnerReference,
// 这样它们不会被自动删除,成为"自由"资源。
DeletePropagationOrphan DeletionPropagation = "Orphan"
// DeletePropagationBackground = "Background"
// 从 key-value store 中删除对象后,垃圾回收器在后台异步删除依赖项。
// 父对象立即消失,子对象在后台消失过程中会处于 Terminating 状态。
DeletePropagationBackground DeletionPropagation = "Background"
// DeletePropagationForeground = "Foreground"
// 对象会保留在 key-value store 中,直到垃圾回收器删除了所有
// blockOwnerDeletion=true 的依赖项。
// API server 会在对象上设置 "foregroundDeletion" finalizer,
// 并设置 deletionTimestamp。这是一种级联删除策略。
DeletePropagationForeground DeletionPropagation = "Foreground"
)
// staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/types.go (行 558-616,k8s v1.36.1)
// DeleteOptions 是删除 API 对象的请求参数
type DeleteOptions struct {
TypeMeta `json:",inline"`
// GracePeriodSeconds:删除前的等待秒数。0 表示立即删除。
// nil 表示使用资源类型的默认宽限期。
GracePeriodSeconds *int64 `json:"gracePeriodSeconds,omitempty" protobuf:"varint,1,opt,name=gracePeriodSeconds"`
// Preconditions:删除的前置条件,用于乐观并发控制。
// 如果不满足,返回 409 Conflict。
Preconditions *Preconditions `json:"preconditions,omitempty" protobuf:"bytes,2,opt,name=preconditions"`
// OrphanDependents(已废弃,请使用 PropagationPolicy)
// 设置为 true 表示孤儿化依赖项,false 表示删除依赖项。
// 这是一个废弃字段,与 PropagationPolicy 二选一,不能同时设置。
OrphanDependents *bool `json:"orphanDependents,omitempty" protobuf:"varint,3,opt,name=orphanDependents"`
// PropagationPolicy:垃圾回收策略。
// Acceptable values: 'Orphan' | 'Background' | 'Foreground'
// 默认值由资源类型的默认策略和 metadata.finalizers 中的 finalizer 共同决定。
PropagationPolicy *DeletionPropagation `json:"propagationPolicy,omitempty" protobuf:"varint,4,opt,name=propagationPolicy"`
DryRun []string `json:"dryRun,omitempty" protobuf:"bytes,5,rep,name=dryRun"`
// DryRun: "All" 表示完成所有处理阶段但不持久化更改到存储。
IgnoreStoreReadErrorWithClusterBreakingPotential *bool `json:"ignoreStoreReadErrorWithClusterBreakingPotential,omitempty" protobuf:"varint,6,opt,name=ignoreStoreReadErrorWithClusterBreakingPotential"`
// 危险选项:当正常删除流程因存储读取错误失败时,触发 unsafe deletion。
// 会忽略 finalizer 约束、跳过前置条件检查、直接从存储中移除对象。
// 警告:可能破坏依赖正常删除流程的集群工作负载。
}
4.3 Finalizer 常量与 OwnerReference
// staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/types.go (行 123-127,k8s v1.36.1)
// These are internal finalizer values for Kubernetes-like APIs
// 这是 k8s 内部使用的 finalizer 值,必须是限定名(带域名)
const (
FinalizerOrphanDependents = "orphan"
// 用于标记"此对象删除时应孤儿化其依赖项"
FinalizerDeleteDependents = "foregroundDeletion"
// 用于标记"此对象删除时应删除其依赖项"
// 当 PropagationPolicy=Foreground 时,apiserver 会自动将此 finalizer 加入对象
)
// staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/types.go (行 315-340,k8s v1.36.1)
// OwnerReference:指向"管理者对象"的引用
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,4,opt,name=uid,casttype=k8s.io/apimachinery/pkg/types.UID"`
// 以上四个字段共同标识了"管理者"对象
Controller *bool `json:"controller,omitempty" protobuf:"varint,6,opt,name=controller"`
// Controller=true 表示这个 OwnerReference 指向"管理控制器"。
// 例如 Deployment 创建 ReplicaSet 时,ReplicaSet 的 OwnerReference.Controller = true。
BlockOwnerDeletion *bool `json:"blockOwnerDeletion,omitempty" protobuf:"varint,7,opt,name=blockOwnerDeletion"`
// BlockOwnerDeletion=true 且 owner 有 foregroundDeletion finalizer 时,
// 只有移除这个引用,owner 才能从 key-value store 中被删除。
// 换句话说:如果子对象设置了 BlockOwnerDeletion=true,
// 那么父对象必须等所有子对象都删除了自己的 OwnerReference,才能真正消失。
// 这与 Foreground 删除策略配合,确保了级联删除的顺序性。
}
4.4 GracefulDeleter 接口与 Store.Delete
apiserver 的存储层(generic registry)负责实际执行删除逻辑。GracefulDeleter 接口定义了"带优雅删除支持的删除"行为,其 Delete 方法返回三个值:删除后的对象、是否立即删除的布尔值、以及错误。
// staging/src/k8s.io/apiserver/pkg/registry/rest/rest.go (行 168-182,k8s v1.36.1)
// GracefulDeleter 定义了如何传递删除选项以支持延迟删除
type GracefulDeleter interface {
// Delete 找到存储中的资源并删除它。
// deleteValidation 会先进行验证。
// options 可以包含 GracePeriodSeconds 等优雅删除参数。
// 返回值中 bool 表示资源是否被"立即"删除:
// true = 同步删除,调用方收到 200 OK
// false = 异步删除(需要等待 finalizers 或宽限期),调用方收到 202 Accepted
Delete(ctx context.Context, name string, deleteValidation ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error)
}
// staging/src/k8s.io/apiserver/pkg/registry/generic/registry/store.go (行 1137-1220,k8s v1.36.1)
// Store.Delete 是 apiserver 中资源删除的核心实现
func (e *Store) Delete(ctx context.Context, name string, ...) (runtime.Object, bool, error) {
key, err := e.KeyFunc(ctx, name) // 构建存储 key
obj := e.NewFunc()
e.Storage.Get(ctx, key, storage.GetOptions{}, obj) // 从 etcd 读取对象
graceful, pendingGraceful, err := rest.BeforeDelete(e.DeleteStrategy, ctx, obj, options)
// BeforeDelete:调用资源类型的删除策略(如 Pod 的删除策略会考虑 terminationGracePeriodSeconds)
// pendingGraceful=true 表示对象已经在删除中(deletionTimestamp 已设置)
// 如果已经在删除中,直接进入 finalization 路径
if pendingGraceful {
out, err := e.finalizeDelete(ctx, obj, false, options)
return out, false, err
}
// 获取对象当前的 finalizers 列表
accessor, _ := meta.Accessor(obj)
pendingFinalizers := len(accessor.GetFinalizers()) != 0
// 判断是否需要更新 finalizers(如根据 PropagationPolicy 添加 foregroundDeletion)
shouldUpdateFinalizers, _ := deletionFinalizersForGarbageCollection(ctx, e, accessor, options)
// 如果需要优雅删除(gracePeriodSeconds>0)、有待处理 finalizers 或需要更新 finalizers,
// 进入 updateForGracefulDeletionAndFinalizers 分支
if graceful || pendingFinalizers || shouldUpdateFinalizers {
err, ignoreNotFound, deleteImmediately, out, lastExisting =
e.updateForGracefulDeletionAndFinalizers(ctx, name, key, options, ...)
if !deleteImmediately || err != nil {
return out, false, err // deleteImmediately=false → 对象还没真正删除
}
}
// 到这里 deleteImmediately=true 且没有错误,真正从 etcd 删除
out = e.NewFunc()
e.Storage.Delete(ctx, key, out, &preconditions, ...)
return out, true, nil // true = 立即删除,调用方收到 200 OK
}
4.5 deletionFinalizersForGarbageCollection:finalizer 计算逻辑
这个函数是 finalizer 策略计算的核心。它根据 DeleteOptions 中的 PropagationPolicy、已有的 finalizers 列表、以及资源类型的默认 GC 策略,来决定"这次删除操作需要在对象的 finalizers 列表中加入什么"。
// staging/src/k8s.io/apiserver/pkg/registry/generic/registry/store.go (行 976-1040,k8s v1.36.1)
// deletionFinalizersForGarbageCollection 分析对象和删除选项,
// 决定是否需要通过垃圾回收器对该对象进行最终处理。
// 返回 (是否需要更新, 新的 finalizers 列表)
func deletionFinalizersForGarbageCollection(ctx context.Context, e *Store,
accessor metav1.Object, options *metav1.DeleteOptions) (bool, []string) {
if !e.EnableGarbageCollection {
return false, []string{} // 如果 GC 被禁用(通过 --enable-garbage-collector=false),
// 直接返回,不设置任何 finalizer,避免设置了永远无法被清理的 finalizer
}
// 判断"是否应该孤儿化依赖项"(shouldOrphanDependents)
// 优先级:DeleteOptions.OrphanDependents > PropagationPolicy > 对象已有 finalizer > 默认策略
shouldOrphan := shouldOrphanDependents(ctx, e, accessor, options)
// 判断"是否应该在前景删除依赖项"(shouldDeleteDependents)
// 优先级同上
shouldDeleteDependentInForeground := shouldDeleteDependents(ctx, e, accessor, options)
// 先把所有非 GC 相关的 finalizer 保留下来
newFinalizers := []string{}
for _, f := range accessor.GetFinalizers() {
if f == metav1.FinalizerOrphanDependents || f == metav1.FinalizerDeleteDependents {
continue // 跳过 GC 相关的 finalizer,后面根据策略重新决定是否添加
}
newFinalizers = append(newFinalizers, f)
}
// 根据策略决定是否添加 GC finalizer
if shouldOrphan {
newFinalizers = append(newFinalizers, metav1.FinalizerOrphanDependents) // "orphan"
}
if shouldDeleteDependentInForeground {
newFinalizers = append(newFinalizers, metav1.FinalizerDeleteDependents) // "foregroundDeletion"
}
// 比较新旧 finalizers 列表,判断是否有变化
changed := !reflect.DeepEqual(accessor.GetFinalizers(), newFinalizers)
return changed, newFinalizers
}
4.6 shouldOrphanDependents 与 shouldDeleteDependents
// staging/src/k8s.io/apiserver/pkg/registry/generic/registry/store.go (行 886-974,k8s v1.36.1)
// shouldOrphanDependents:决定是否将"orphan" finalizer 加入列表
// 优先级(从高到低):
// 1. options.OrphanDependents != nil → 直接返回其布尔值(废弃字段)
// 2. options.PropagationPolicy:
// - DeletePropagationOrphan → return true
// - DeletePropagationBackground/Foreground → return false
// 3. 对象已有的 finalizers 列表中是否有 FinalizerOrphanDependents/FinalizerDeleteDependents
// 4. 资源类型的默认 GC 策略(如 Deployment 默认是 DeletePropagationForeground)
func shouldOrphanDependents(ctx context.Context, e *Store, accessor metav1.Object, options *metav1.DeleteOptions) bool {
// 获取资源类型的默认 GC 策略
gcStrategy, ok := e.DeleteStrategy.(rest.GarbageCollectionDeleteStrategy)
var defaultGCPolicy rest.GarbageCollectionPolicy
if ok {
defaultGCPolicy = gcStrategy.DefaultGarbageCollectionPolicy(ctx)
}
if defaultGCPolicy == rest.Unsupported { return false }
// 显式设置的删除选项优先
if options != nil && options.OrphanDependents != nil { return *options.OrphanDependents }
if options != nil && options.PropagationPolicy != nil {
switch *options.PropagationPolicy {
case metav1.DeletePropagationOrphan: return true
case metav1.DeletePropagationBackground, metav1.DeletePropagationForeground: return false
}
}
// 对象已有 finalizer 的语义覆盖默认策略
finalizers := accessor.GetFinalizers()
for _, f := range finalizers {
if f == metav1.FinalizerOrphanDependents { return true }
if f == metav1.FinalizerDeleteDependents { return false }
}
return defaultGCPolicy == rest.OrphanDependents
}
// shouldDeleteDependents:决定是否将"foregroundDeletion" finalizer 加入列表
// 逻辑与 shouldOrphanDependents 类似,但结论相反
// 注意:DeletionPropagationBackground 会设置 shouldOrphan=true,
// 但不设置 shouldDeleteDependents=true(即对象可以立即消失,但子对象后台删除)
func shouldDeleteDependents(ctx context.Context, e *Store, accessor metav1.Object, options *metav1.DeleteOptions) bool {
if gcStrategy, ok := e.DeleteStrategy.(rest.GarbageCollectionDeleteStrategy); ok &&
gcStrategy.DefaultGarbageCollectionPolicy(ctx) == rest.Unsupported { return false }
// 关键区别:Background 策略不触发前景删除(shouldDeleteDependents=false),
// 但会触发孤儿化(shouldOrphan=false 且 GC 行为为后台)
if options != nil && options.OrphanDependents != nil { return false }
if options != nil && options.PropagationPolicy != nil {
switch *options.PropagationPolicy {
case metav1.DeletePropagationForeground: return true
case metav1.DeletePropagationBackground, metav1.DeletePropagationOrphan: return false
}
}
finalizers := accessor.GetFinalizers()
for _, f := range finalizers {
if f == metav1.FinalizerDeleteDependents { return true }
if f == metav1.FinalizerOrphanDependents { return false }
}
return false
}
4.7 updateForGracefulDeletionAndFinalizers:写入 deletionTimestamp
// staging/src/k8s.io/apiserver/pkg/registry/generic/registry/store.go (行 1052-1135,k8s v1.36.1)
// updateForGracefulDeletionAndFinalizers 负责:
// 1. 设置 deletionTimestamp(优雅删除的标志)
// 2. 更新 finalizers 列表(根据 GC 策略添加/移除 GC finalizer)
// 3. 协调优雅删除宽限期和 finalizer 等待逻辑
func (e *Store) updateForGracefulDeletionAndFinalizers(ctx context.Context,
name, key string, options *metav1.DeleteOptions, preconditions storage.Preconditions,
deleteValidation rest.ValidateObjectFunc, in runtime.Object) (err error, ignoreNotFound bool,
deleteImmediately bool, out, lastExisting runtime.Object) {
out = e.NewFunc()
err = e.Storage.GuaranteedUpdate(ctx, key, out, ..., storage.SimpleUpdate(func(existing runtime.Object) (runtime.Object, error) {
// 调用 BeforeDelete 策略(根据 GracePeriodSeconds 决定是否设置 deletionTimestamp)
graceful, pendingGraceful, err := rest.BeforeDelete(e.DeleteStrategy, ctx, existing, options)
if pendingGraceful { return nil, errAlreadyDeleting } // 对象已在删除中
if err != nil { return nil, err }
// 计算并更新 finalizers 列表
existingAccessor, _ := meta.Accessor(existing)
needsUpdate, newFinalizers := deletionFinalizersForGarbageCollection(ctx, e, existingAccessor, options)
if needsUpdate {
existingAccessor.SetFinalizers(newFinalizers)
}
// 判断是否还有未完成的 finalizer
pendingFinalizers = len(existingAccessor.GetFinalizers()) != 0
if !graceful {
// 不支持优雅删除的场景:如果有待处理 finalizer → 标记为删除中(deletionTimestamp=now)但不删除
if pendingFinalizers {
markAsDeleting(existing, time.Now()) // 设置 deletionTimestamp,保留对象
return existing, nil
}
return nil, errDeleteNow // 没有 finalizer 且不支持优雅删除 → 立即删除
}
// 支持优雅删除:设置 deletionTimestamp(如果还没有)
// 宽限期结束后,deletionTimestamp 不为 nil 且宽限期已过,触发立即删除
return existing, nil
}), ...)
// 根据不同返回情况决定后续行为
switch err {
case nil:
if pendingFinalizers { return nil, false, false, out, lastExisting }
// pendingFinalizers=true → deleteImmediately=false,对象在 etcd 中等待 finalizer 清除
if lastGraceful > 0 { return nil, false, false, out, lastExisting }
return nil, true, true, out, lastExisting // 宽限期已过 → 立即删除
case errDeleteNow:
return nil, false, true, out, lastExisting // 立即删除
case errAlreadyDeleting:
out, err = e.finalizeDelete(ctx, in, true, options)
return err, false, false, out, lastExisting // finalization 路径
}
}
五、踩坑避坑:Finalizers 生产环境陷阱
坑 1:Operator 崩了导致 finalizer 永远无法移除
这是生产环境中最高频的 finalizer 相关故障。如果你的 Operator(控制器)进程崩溃了,那么它注册的 finalizer 就永远不会被移除——对象会一直卡在 Terminating 状态,既无法删除也无法恢复。
# 现象:PVC 一直处于 Terminating 状态
$ kubectl get pvc NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE data-pvc Terminating standard 30d
根因:你的 Operator 持有某个 finalizer(如 myoperator.io/cleanup),但 Operator Pod 被调度到了某个节点上,而那个节点出了故障(网络分区、NotReady)。控制器无法监听并处理这个对象,所以 finalizer 永远无法被移除。
排查:kubectl get <resource> <name> -o yaml | grep finalizers 查看具体是哪个 finalizer 卡住了。然后检查该 finalizer 对应的控制器是否正常运行。
修复:
- 恢复控制器:先确保 Operator 进程恢复正常,让它有机会处理删除逻辑。
- 手动移除 finalizer(紧急):如果确认外部清理已经完成(或不需要清理),手动 patch 移除 finalizer:kubectl patch <resource>/<name> -p '{"metadata":{"finalizers":null}}' --type=merge。注意:这会跳过清理逻辑。
- 预防措施:Operator 应该实现"finalizer 兜底机制"——如果清理操作超过一定时间(如 5 分钟)仍未完成,主动放弃清理并记录警告日志,同时移除 finalizer 避免资源永久卡死。
坑 2:在 Reconcile 中忘记处理 deletionTimestamp 分支
很多新手写 Operator 时,只实现了"正常状态"的 Reconcile 逻辑——创建资源、更新资源、删除资源。但他们忘了判断"对象正在被删除"这一状态,导致 finalizer 永远无法被移除。
错误写法(常见于新手 Operator):
// ❌ 错误:没有处理 DeletionTimestamp
func (r *DatabaseReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
db := &myappv1.Database{}
if err := r.Get(ctx, req.NamespacedName, db); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// ❌ 这里只处理"创建/更新"逻辑,完全没有判断 DeletionTimestamp!
// 当对象被删除时,这个 Reconcile 不会做任何清理,也不会移除 finalizer
// 创建底层数据库...
return r.ensureDatabase(ctx, db)
}
正确写法:
// ✅ 正确:在 Reconcile 中优先处理删除分支
func (r *DatabaseReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
db := &myappv1.Database{}
if err := r.Get(ctx, req.NamespacedName, db); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// ✅ 关键:判断对象是否正在被删除(deletionTimestamp != nil)
if !db.DeletionTimestamp.IsZero() {
// 执行清理逻辑
if controllerutil.ContainsFinalizer(db, "database.myapp.io/cleanup-pvcs") {
if err := r.cleanupPVCCs(ctx, db); err != nil {
return ctrl.Result{RequeueAfter: 10 * time.Second}, err
}
// 清理完成后,移除 finalizer
controllerutil.RemoveFinalizer(db, "database.myapp.io/cleanup-pvcs")
if err := r.Update(ctx, db); err != nil {
return ctrl.Result{Requeue: true}, err
}
}
return ctrl.Result{}, nil // finalizer 移除后,对象将被删除
}
// 正常创建/更新逻辑...
return r.ensureDatabase(ctx, db)
}
坑 3:级联删除 vs 不级联删除——选错了策略
删除一个有子对象的父资源时,如果选错了 propagationPolicy,可能导致数据丢失或者孤儿资源。
根因:使用了 --cascade=orphan 或者 kubectl delete --cascade=false。StatefulSet 默认不使用级联删除(cascade),所以删除 StatefulSet 时不会自动删除其 PVC。
修复:使用 --cascade=foreground 或 --cascade=true 显式触发级联删除:kubectl delete statefulset my-sts --cascade=foreground
预防:删除 StatefulSet 前先用 kubectl get pvc -l app=my-sts 确认关联的 PVC,评估是否需要保留。
坑 4:多个 finalizer 之间的死锁
k8s 源码注释明确指出:finalizers 不保证处理顺序,如果两个 finalizer 互相等待,就会产生死锁(deadlock)。
场景:假设有两个 finalizer:
- finalizer A:需要等待外部服务 X 确认"数据已同步完毕"才能移除
- finalizer B:需要等待 Pod 创建完成后才能移除
如果 finalizer A 的清理逻辑依赖 Pod 先被创建(但 Pod 在删除中不会被创建),而 finalizer B 需要等待 A 先完成,就会死锁。
解决:不要在 finalizer 的清理逻辑中引入对其他 k8s 对象的创建/更新依赖。如果确实需要协调多个 finalizer,使用外部协调机制(如分布式锁、消息队列),而不是依赖对象创建顺序。
坑 5:Watch 不到 Terminating 状态的对象
在 Operator 开发中,使用 client-go 的 Informer 或 controller-runtime 的 Watches 时,默认情况下 Informer 不会把 Terminating 状态的对象推送给控制器——因为 Informer 默认配置下缓存会过滤掉带有 deletionTimestamp 的对象(除非使用 AllowDeletedFieldsWhenFormerlyCached 或 NonDefaultFreeFormDeletionTimestampFields 特性门控)。
根因:在 k8s 1.20+ 中,Informer 默认不再 watch 正在删除的对象(为了避免缓存不一致)。
修复:对于 controller-runtime,使用 For(&Database{}, builder.OnlyCreatedOrDeleted()) 或者直接 Watch(source.Kind(mgr.GetCache(), &myappv1.Database{})).对于 client-go,在 SharedIndexInformer 上设置 sharedIndexInformer.SetWatchErrorHandler,并确保在 sharedIndexInformer.Run() 之后再操作。或者使用 client-go 的 Dynamic SharedInformer。
预防:k8s v1.36.1 推荐使用 controller-runtime v0.17+ 版本,它默认就能正确处理 Terminating 状态的对象。
坑 6:Finalizer 泄露(Finalizer Leak)
当 Operator 的清理逻辑本身出错(比如调用云 API 超时、返回 500 错误),但没有正确处理这个错误时,finalizer 可能被移除(因为代码执行到了那一行),但实际的清理工作根本没有完成——这叫"伪清理",比不清理更危险,因为它让对象消失了,但外部资源留下了。
预防措施:
- 清理操作必须使用幂等的外部 API 调用(大多数云平台的撤销 API 本身就是幂等的)
- 清理失败时返回错误,让 Reconcile 重新入队(Requeue)重试,而不是直接移除 finalizer
- 添加重试次数限制(如连续失败 5 次后记录严重事件并发送告警),避免无限重试导致对象永远卡住
- 在 finalizer 移除前记录一条"清理完成确认"日志,方便事后审计
六、高频答疑:20 个常见问题
Question 1:对象卡在 Terminating 状态,如何快速定位是哪个 finalizer 导致的?
Answer:执行 kubectl get <resource> <name> -o jsonpath='{.metadata.finalizers}',这会直接输出 finalizers 列表。列表中每个字符串就是一个"门卫",需要找到对应的控制器去处理。如果列表很长,用 kubectl get <resource> <name> -o yaml | grep finalizers -A 20 查看完整上下文。另外可以用 kubectl get events --field-selector involvedObject.name=<name> --sort-by=.lastTimestamp 查看该对象的最新事件,看是否有清理失败的报错。
Question 2:deletionTimestamp 和 DeletionGracePeriodSeconds 有什么区别?
Answer:deletionTimestamp 是一个时间戳(RFC 3339 格式),表示"删除流程开始的时间",是定性指标——只要它不为 nil,对象就在删除流程中。DeletionGracePeriodSeconds 是一个整数,表示"优雅删除的等待秒数",是定量指标——它决定了对象在真正被删除前应该等待多久(Pod 的 terminationGracePeriodSeconds 就属于这个字段)。两者配合使用:deletionTimestamp 标记"开始",DeletionGracePeriodSeconds 控制"等多久"。对于带 finalizers 的对象,宽限期和 finalizers 是"与"的关系——两个条件都满足后(时间到了且 finalizers 清空了),对象才会被删除。
Question 3:删除一个 Deployment 时,ReplicaSet 和 Pod 是怎么被删掉的?
Answer:当删除一个 Deployment 时(使用默认的 Foreground 策略),apiserver 会给它加上 kubernetes.io/foregroundDeletion finalizer。这个 finalizer 会让 GarbageCollector 控制器先去删除所有 ReplicaSet(Deployment 的子资源),等所有 ReplicaSet 都消失了,再删除 Deployment 自己。同时,ReplicaSet 的 OwnerReference 设置了 BlockOwnerDeletion=true,这意味着 ReplicaSet 存在时,Deployment 无法真正消失——形成了一个"先子后父"的删除链。这个链的终点是 Pod,Pod 没有 OwnerReference,卡在 Terminating 状态等待自己的容器优雅退出(由 kubelet 处理 terminationGracePeriodSeconds)。
Question 4:为什么 finalizer 的名字推荐用域名格式(如 myoperator.io/cleanup)?
Answer:k8s 生态中有大量的 Finalizer 字符串,多个 Operator 可能共存于同一个集群。如果大家都用简单名字(如 cleanup),就可能发生冲突——你的 Operator 误删了别人的 finalizer,或者别人的 Operator 收到了你发的 finalizer。域名格式(逆向域名,如 io.myoperator.cleanup)利用了 DNS 的天然隔离特性,不同组织的域名不会冲突。此外,Finalizer 字符串在代码中被用作"标识符",k8s 源码注释中明确说"must be qualified name unless defined here"(必须是限定名),这既是惯例也是规范。
Question 5:kubebuilder 中如何正确添加 finalizer?
Answer:在 kubebuilder v3+ 中,推荐使用 RBAC 注解 + controllerutil 的 Finalizer 相关方法。首先在 Reconciler 的 RBAC 注解中加上 Update 权限(因为添加 finalizer 需要更新对象),然后在 Init() 钩子(SetupWithManager 中通过 builder.WithInitializer 或 Owns 配合)或者直接在 Reconcile 中判断:if !controllerutil.ContainsFinalizer(&db, "database.myapp.io/cleanup") { controllerutil.AddFinalizer(&db, "database.myapp.io/cleanup"); r.Update(ctx, db); return ctrl.Result{}, nil }。添加 finalizer 时必须同步执行一次 Update,否则对象还是没被修改,控制器下次进来还是会添加同一个 finalizer(不是 bug,但是多余的写入)。
Question 6:GarbageCollector 控制器是什么?它和 Finalizers 有什么关系?
Answer:GarbageCollector(垃圾回收器)是 kube-controller-manager 中的一个控制器(pkg/controller/garbagecollector/garbagecollector.go),负责管理 k8s 对象之间的依赖关系。它通过监听所有资源的变更,维护一个"依赖图"(哪些对象是哪些对象的子对象)。当父对象被删除时,GarbageCollector 根据 OwnerReference 和 PropagationPolicy,决定如何处理子对象——是删除(Foreground)、后台删除(Background)、还是孤儿化(Orphan)。GarbageCollector 本身不处理 Finalizers,它处理的是 GC 层面的"父子级联删除"。Finalizers 则是对象级别(ObjectMeta 级别)的清理机制,由各业务控制器(Operator)处理。一个对象可能同时被 GC(管理父子关系)和多个 Finalizers(管理各自业务清理)共同管理。
Question 7:删除 Namespace 时为什么会很慢?
Answer:删除一个 Namespace 时,apiserver 会触发 namespace controller(命名空间控制器)的删除流程。它会先遍历该 Namespace 下所有类型的资源,逐个发送删除请求。每个资源的删除都可能涉及自己的 finalizer 清理(如删除 Service 时要等待 cloud provider 撤销 LoadBalancer,删除 PVC 时要等 PV 被 release)。如果 Namespace 下的资源很多(成百上千个 Pod、Service、ConfigMap),这些删除操作是串行或有限并发处理的,整个 Namespace 删除可能需要几分钟甚至更长时间。如果某个资源的 finalizer 卡住了,整个 Namespace 删除都会卡住。此时可以用 kubectl get namespace <name> -o yaml 查看 status.phase 是否卡在 Terminating。
Question 8:Finalizer 在 etcd 里是怎么存储的?
Answer:Finalizers 是 ObjectMeta 的一个普通 JSON 数组字段,序列化后存储在 etcd 的同一个 key 下面(如 /registry/namespaces/default/pods/my-pod.yaml)。当你执行 kubectl edit 修改 finalizers 时,实际上就是一次普通的 PATCH 请求,apiserver 会用 patchStrategy:merge 策略合并(而不是替换)finalizers 列表。这意味着你可以单独添加或移除一个 finalizer,而不影响其他的。etcd 的 watch 机制会把这个变更推送给所有 Informer,GarbageCollector 和各业务的 Operator 都会收到通知。
Question 9:PV 和 PVC 的保护 finalizer 有什么区别?
Answer:k8s 为持久卷提供了两层 finalizer 保护。第一层是 kubernetes.io/pv-protection,绑在 PV(PersistentVolume)上,防止 PV 在被 Pod 使用时被意外删除。第二层是 kubernetes.io/pvc-protection,绑在 PVC(PersistentVolumeClaim)上,防止 PVC 在 Pod 还在使用时消失。当删除一个正在被 Pod 使用的 PVC 时,pvc-protection finalizer 会阻止删除,PVC 会进入 Terminating 状态并一直等待,直到所有绑定的 Pod 都不再使用它(kubelet 检测到 Pod 删除后会自动清理 mount,然后通知 PVC 控制器移除 finalizer)。这两层 finalizer 由 persistentvolume-controller(PV/PVC 控制器)自动管理,不需要用户手动操作。
Question 10:kubectl delete --grace-period=0 和 kubectl delete --immediate 有什么区别?
Answer:--grace-period=0 会立即进入优雅删除流程——设置 deletionTimestamp 为当前时间,但不会立即删除对象。如果对象有 finalizers,它们仍然会被等待。--grace-period=0 只是跳过了"宽限期"(grace period),但 finalizers 的等待逻辑不变。真正"跳过 finalizer 直接删"的是 --grace-period=0 配合 IgnoreStoreReadErrorWithClusterBreakingPotential(危险选项),或者直接 patch finalizers 为 null。kubectl delete --immediate 不是一个标准参数(kubectl 没有这个参数),用户可能是指 kubectl delete --grace-period=0,但需要注意这两者的区别。
Question 11:如何查看一个对象的所有 OwnerReference?
Answer:kubectl get <resource> <name> -o jsonpath='{.metadata.ownerReferences}' 或者 kubectl get <resource> <name> -o yaml 查看完整的 ownerReferences 列表。每个 OwnerReference 包含 APIVersion、Kind、Name、UID(用于验证引用是否仍然有效)、Controller(是否为管理控制器)和 BlockOwnerDeletion(删除前是否需要先移除此引用)。UID 字段很重要——如果父对象被删除了但 UID 不匹配(说明引用已失效),GarbageCollector 就不会再把它当作"需要管理的子对象"了。
Question 12:为什么 GarbageCollector 需要 disabledByDefault 机制?
Answer:在早期版本的 k8s 中,GarbageCollector 会监听所有资源变更来构建依赖图。当集群规模很大(有上万种资源类型)时,这个图的内存开销非常大,而且每次资源变更都会触发图更新,严重影响 apiserver 性能。disabledByDefault 机制允许管理员通过 --generic-garbage-collector-enabled=false 禁用 GarbageCollector。禁用后,GarbageCollector 不再监听资源变更,但已构建的图仍然存在(用于处理已有的 OwnerReference)。对于大规模集群,禁用 GC 并配合显式的 --cascade=true 或 --cascade=orphan 参数使用,可以显著降低 apiserver 的内存和 CPU 开销。
Question 13:kubectl delete 和 client-go 的 DeleteOptions 中 OrphanDependents 参数是什么关系?
Answer:OrphanDependents(orphanDependents)是 k8s 1.7 之前的旧参数,PropagationPolicy 是 1.7 引入的新参数(更清晰)。在 shouldOrphanDependents 源码中可以看到,OrphanDependents 的优先级更高——只要它不是 nil,就会直接使用它的值,而不看 PropagationPolicy。因此,kubectl delete --orphan=true 等价于 OrphanDependents=true,kubectl delete --propagation-policy=Orphan 是新写法,效果相同。在新代码中应该使用 PropagationPolicy,OrphanDependents 只在兼容旧客户端时保留。
Question 14:Pod 的删除和 Finalizers 有什么关系?
Answer:Pod 的删除流程比较特殊:Pod 通常没有 finalizers(除非挂载了某些需要清理的 PVC 或特殊资源),Pod 的"优雅删除"主要依赖 kubelet 和 terminationGracePeriodSeconds。当删除一个 Pod 时,kubelet 收到通知,向容器进程发送 SIGTERM 信号,然后等待 terminationGracePeriodSeconds 秒(默认 30 秒),如果容器还没退出就发 SIGKILL。这个过程完全在 kubelet 侧处理,和 Finalizers 无关。但如果 Pod 挂载了一个带 pv-protection finalizer 的 PVC,那么删除 Pod 后,kubelet unmount 卷,PVC 的 finalizer 会被移除,PVC 才能真正消失——这里 Finalizers 的作用是保护 PVC,而不是 Pod。
Question 15:如何让 Operator 在 finalizer 处理失败时自动重试而不是无限等待?
Answer:在 controller-runtime 中,Reconcile 函数返回 ctrl.Result{RequeueAfter: 30 * time.Second} 会让 Reconciler 在 30 秒后重新入队。如果清理操作失败(如调用云 API 返回错误),不要移除 finalizer(否则清理被跳过),而是记录错误日志并返回 RequeueAfter,给外部系统一个恢复的机会。如果同一个清理操作连续失败超过 N 次(如 5 次),可以记录严重事件(r.Recorder.Event)并发送告警,同时可以考虑放弃清理(但这非常危险,必须在日志中明确标记)。建议使用指数退避策略:第 1 次失败等 10 秒,第 2 次等 30 秒,第 3 次等 2 分钟,第 4 次等 10 分钟,第 5 次及以上等 30 分钟,这样既能重试恢复,又不会对 apiserver 造成过多压力。
Question 16:自定义 finalizer 会不会影响 apiserver 的性能?
Answer:Finalizers 本身对 apiserver 性能的影响很小——它们只是存储在 etcd 中的几个字符串,对象数量不变的情况下,finalizers 的多少不会显著增加内存或 CPU 开销。但是,如果某个对象的 finalizer 永远无法被移除(controller 崩了),这个对象会一直卡在 etcd 中,不断触发 Informer 的更新事件——这会增加所有 Watch 这个对象类型的控制器的处理负担。对于大规模集群,建议监控 Terminating 状态对象数量(kubectl get pods --field-selector=status.phase=Terminating --all-namespaces | wc -l),如果这个数字持续增长,说明有 finalizer 泄露的问题需要排查。
Question 17:删除一个 CRD 时,CR(CustomResource 实例)会怎么被处理?
Answer:删除一个 CRD 时,apiextensions-apiserver 会触发 CRDFinalizer 控制器(pkg/controller/finalizer/crd_finalizer.go)。这个控制器会先列出该 CRD 下所有已创建的 CR 实例,然后逐个删除它们(或者按 PropagationPolicy 决定是删除还是孤儿化)。只有当所有 CR 都消失后,CRD 本身才会被删除。这个过程可能很慢,因为 CRD 是集群级别的资源(cluster-scoped),它的删除涉及所有命名空间下的所有实例。如果某个 CR 的 Operator 注册了 finalizer,那个 finalizer 必须被处理完毕才能删除 CR——这意味着 Operator 必须正常运行才能完成 CRD 的删除。建议在删除 CRD 前先删除所有 CR 实例,再删除 CRD。
Question 18:patchStrategy:merge 在 finalizers 的上下文中是什么意思?
Answer:patchStrategy:merge 来自 k8s 的 JSON Patch 策略定义。当你对一个对象执行 PATCH 操作时,k8s 需要知道如何合并(merge)你提供的补丁和对象现有状态。对于 finalizers 字段,merge 策略意味着:如果你发送一个 PATCH 请求 {"metadata": {"finalizers": [{"$patch":"delete", "key":"myapp.io/cleanup"}]}},k8s 只会从列表中删除指定的 finalizer,而不影响其他的。这和"替换"(replace)策略不同——替换策略会用你的补丁完全覆盖原有值。如果 finalizers 没有声明 patchStrategy:merge,而是默认的替换策略,那么 patch 一个 finalizer 时其他 finalizer 就会丢失——这通常是一个 bug。
Question 19:Node 节点被删除时,节点上的 Pod 是怎么处理的?
Answer:删除一个 Node(节点)对象时,kubelet 不会收到任何通知——Node 对象只是一个 API 资源描述,实际运行在节点上的 kubelet 进程是通过心跳(Node Lease)与 apiserver 保持连接的。当节点失联(kubelet 心跳超时,默认 40 秒)后,NodeController 会给该节点打上 node.kubernetes.io/unreachable 或 node.kubernetes.io/not-ready taint(污点),并触发 Pod 的删除流程。Pod 的删除使用 Background 策略——先删除 Node 对象(apiserver 立即处理),kubelet 检测到节点不可达后停止容器,EndpointSlice 控制器更新端点信息,Pod 最终从 etcd 中消失。这个过程不依赖 finalizers,而是依赖 kubelet 的主动退出和 Controller 的主动清理。
Question 20:在 k8s v1.36.1 中,Finalizers 的设计有没有什么新的变化?
Answer:k8s v1.36.1 中,Finalizers 的核心机制没有重大变化——Finalizers 从 1.0 时代就存在,API 非常稳定。在 v1.20+ 的变化主要体现在:① GarbageCollector 的默认行为更加保守,减少了对非 OwnerReference 资源的干扰;② controller-runtime(Operator 框架)对 Terminating 状态对象的支持得到了改善,Watches 机制不再默认过滤带 deletionTimestamp 的对象;③ 增加了 IgnoreStoreReadErrorWithClusterBreakingPotential 选项,允许在极端情况下绕过 finalizer 强制删除,这对运维人员在 finalizer 卡死的紧急场景下提供了救命手段(但代价是跳过所有清理逻辑)。Finalizers 仍然是 k8s 资源生命周期管理中最核心的机制之一,在 Operator 开发中几乎是必选项。
本节我们学到了:
- Finalizers 的本质是"删除前置条件锁"——它让对象在 deletionTimestamp 设置后仍然留在 etcd 中,直到所有 finalizer 被移除。
- 删除流程分为 Finalization 和 Deletion 两个阶段:Finalization 阶段执行清理逻辑,Deletion 阶段才真正从 etcd 抹掉数据。
- PropagationPolicy(Foreground / Background / Orphan)控制 GarbageCollector 如何处理父子依赖;Finalizers 控制各业务控制器如何执行清理逻辑。两者协作完成完整的级联删除。
- Finalizers 的实现分散在 apiserver 的存储层(store.go)、apimachinery 的类型定义(types.go)和各业务控制器中,理解 Store.Delete 的完整执行路径是掌握 finalizer 机制的关键。
- Operator 开发中最容易犯的错误是"忘记在 Reconcile 中处理 deletionTimestamp 分支",导致 finalizer 永远无法移除,对象永久卡在 Terminating 状态。
相关阅读:
• staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/types.go — ObjectMeta / DeleteOptions / DeletionPropagation 定义
• staging/src/k8s.io/apiserver/pkg/registry/generic/registry/store.go — Store.Delete 与 finalizer 计算逻辑
• pkg/controller/garbagecollector/garbagecollector.go — GarbageCollector 控制器实现
• Kubernetes API Conventions — 对象生命周期与删除语义官方规范
• Kubernetes 官方文档 — Finalizers

浙公网安备 33010602011771号