惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Help Net Security
Help Net Security
U
Unit 42
H
Help Net Security
酷 壳 – CoolShell
酷 壳 – CoolShell
云风的 BLOG
云风的 BLOG
宝玉的分享
宝玉的分享
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Vercel News
Vercel News
Jina AI
Jina AI
Apple Machine Learning Research
Apple Machine Learning Research
B
Blog RSS Feed
T
The Blog of Author Tim Ferriss
WordPress大学
WordPress大学
Recent Announcements
Recent Announcements
罗磊的独立博客
Google DeepMind News
Google DeepMind News
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Hacker News - Newest:
Hacker News - Newest: "LLM"
Recent Commits to openclaw:main
Recent Commits to openclaw:main
PCI Perspectives
PCI Perspectives
L
LangChain Blog
C
CXSECURITY Database RSS Feed - CXSecurity.com
S
SegmentFault 最新的问题
C
Cisco Blogs
T
The Exploit Database - CXSecurity.com
P
Proofpoint News Feed
B
Blog
V
Vulnerabilities – Threatpost
Scott Helme
Scott Helme
Google Online Security Blog
Google Online Security Blog
J
Java Code Geeks
E
Exploit-DB.com RSS Feed
The Cloudflare Blog
N
News and Events Feed by Topic
S
Schneier on Security
Cloudbric
Cloudbric
Forbes - Security
Forbes - Security
H
Hacker News: Front Page
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
The Hacker News
The Hacker News
博客园 - 【当耐特】
aimingoo的专栏
aimingoo的专栏
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
P
Palo Alto Networks Blog
GbyAI
GbyAI
AI
AI
T
Threat Research - Cisco Blogs
SecWiki News
SecWiki News
人人都是产品经理
人人都是产品经理

博客园 - 左扬

Kubernetes 编程 / Operator 专题【左扬精讲】—— OwnerReference 字段与级联删除机制 Kubernetes 编程 / Operator 专题【左扬精讲】—— 深入学习 Server-Side Apply:managedFields 替代 last-applied-configuration 的演进方向 Kubernetes 编程 / Operator 专题【左扬精讲】—— k8s Annotations 与元数据体系(Operator 专题) Kubernetes 编程 / Operator 专题【左扬精讲】—— RESTMapper:把 Group / Version / Kind / Resource 四元组翻译成 REST 路径的"查字典"大师 Kubernetes 编程 / Operator 专题【左扬精讲】—— Converter 资源版本转换器 Kubernetes 编程 / Operator 专题【左扬精讲】—— Application 业务扩展:从单 Deployment 到多 Workload 的复合 Operator 演进 Kubernetes 编程 / Operator 专题【左扬精讲】—— OwnerReference / Finalizer / 准入控制:k8s 资源生命周期的三大支柱 Kubernetes 编程 / Operator 专题【左扬精讲】—— controller-runtime 框架内幕:从 Manager 到 Reconcile 的全栈拆解 Kubernetes 编程 / Operator 专题【左扬精讲】—— 生产级 Operator 最佳实践:并发安全、资源清理与高可用设计 Kubernetes 编程 / Operator 专题【左扬精讲】—— application-operator Reconcile 循环源码精讲:从 client-go Informer 到 workqueue 的全链路解剖 Kubernetes 编程 / Operator 专题【左扬精讲】—— 从零搭建一个 application-operator 新项目:脚手架、API 设计与基于原生 DeploymentStatus/ServiceStatus 的状态建模 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:生产级 Controller 实践:并发安全、资源清理与高可用设计 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析: Controller 调试与诊断工具:从日志分析到问题定位 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:DynamicClient 操作 CRD:无需代码生成的动态操作 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:控制器与 APIServer 完整交互流程:从 Watch 到缓存同步 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:错误处理与重试机制:WorkQueue 限速器详解 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:Leader 选举机制:高可用控制器的必备技能 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:Controller 开发模式完整实战 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:SharedInformerFactory 与等待缓存同步 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:从认证配置到 Deployment 操作 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:版本对应、架构组件与组件关系 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:Informer 源码深度解析:从底层原理到实战应用 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:Reflector 源码深度解析 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:ListWatcher 源码深度解析 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:Indexer 与 ThreadSafeStore 核心原理与源码深度剖析 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:DeltaFIFO 核心原理与源码深度剖析 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:workqueue 核心原理与实战 Kubernetes 编程 / Operator 专题【左扬精讲】—— runtime.Codec 资源编解码:serializer 与 codec 差异、编解码数据结构、codec 核心调用链路 Kubernetes 编程 / Operator 专题【左扬精讲】—— Scheme 资源注册机制全解 Kubernetes 编程 / Operator 专题【左扬精讲】—— Kubernetes 自定义资源的内部版本与外部版本:从源码看版本定义机制 Kubernetes 编程 / Operator 专题【左扬精讲】—— Kubernetes 1.36.1 核心 API 数据结构全解 Kubernetes 编程 / Operator 专题【左扬精讲】—— Kubernetes 构建过程 【AIOPS】一文读懂LLM【左扬精讲】:从诞生到普及,解锁大语言模型的核心密码 【AIOPS】AI Agent 专题【左扬精讲】核心功能篇:MCP-VictoriaMetrics Hooks 源码精讲:Hooks 可观测性的无侵入式实现 【AIOPS】AI Agent 专题【左扬精讲】核心功能篇:MCP-VictoriaMetrics Golang 配置解析源码精讲 ——SRE 自定义 Agent 核心技巧 【AIOPS】AI Agent 专题【左扬精讲】核心功能篇:MCP-VictoriaMetrics Golang 并发模型解析 ——SRE 应对高并发采集的调优思路 【AIOPS】AI Agent 专题【左扬精讲】基础架构篇:MCP-VictoriaMetrics Golang 源码整体架构拆解 ——SRE 必懂的核心模块与数据流 OpenTelemetry 开发实战【左扬精讲】—— 云原生可观测体系构建与分布式追踪二次开发 Kubernetes 编程 / Operator 专题【左扬精讲】—— Operator 开发实战项目 7 —— 基于流量预测模型的智能弹性扩缩容 Operator 实战(AIOps 模型训练与智能扩容(下篇)—— 预测式弹性扩缩容 Operator 落地实现) Kubernetes 编程 / Operator 专题【左扬精讲】—— Operator 开发实战项目 7 —— 基于流量预测模型的智能弹性扩缩容 Operator 实战(AIOps 模型训练与智能扩容(上篇)—— 时序预测模型构建与离线训练) Kubernetes 编程 / Operator 专题【左扬精讲】—— Operator 开发实战项目 6 —— 基于运维专家知识库的智能故障诊断与排查 Operator 实战 Kubernetes 编程 / Operator 专题【左扬精讲】—— Operator 开发实战项目 5 —— 基于大语言模型(LLM)的实时日志流智能监测 Operator 实现 Kubernetes 编程 / Operator 专题【左扬精讲】—— Operator 开发实战项目 4 —— 基于 Operator 实现大模型私有化部署与管理 Kubernetes 编程 / Operator 专题【左扬精讲】—— Operator 开发实战项目 3(上篇)—— 面向 AI / 算力调度场景:GPU 竞价实例资源池统一调度管理 Operator 开发 Kubernetes编程 / Operator专题【左扬精讲】—— Operator 开发实战项目 2 —— 面向零售 / 电商潮汐流量难题:多云多集群数据中心级全链路弹性伸缩 DataCenter Scaler Operator 从 0 到 1 全链路开发 Kubernetes编程 / Operator专题【左扬精讲】—— 深入理解Kubebuilder注解:为什么Operator开发离不开这些特殊注释 Kubernetes编程 / Operator专题【左扬精讲】—— Operator 开发实战项目1 —— Applicaion Operator(通用应用生命周期管理 Operator 实战) Pod 镜像拉取失败?kubectl edit pods修改镜像地址的底层原理与实操 (该方法仅为临时应急方案,并非长期解决方案) Kubernetes编程/Operator专题精讲—— 理解控制器模式 —— 控制器模式的核心原理与实现逻辑(从原理到实践) 【AIOPS】AI Agent 专题【左扬精讲】模型微调实战:一站式平台 LLaMA-Factory 【AIOPS】AI Agent 专题【左扬精讲】基于 k8s+vLLM+Ray 分布式部署全指南:架构设计、资源调度与性能优化 【AIOPS】AI Agent专题【左扬精讲】非量化版DeepSeek分布式部署全指南:精度保障、显存规划与Ollama/vLLM选型 【AIOPS】AI Agent 专题【左扬精讲】零开发框架实现 ReAct Agent(Go SRE友好)
Kubernetes 编程 / Operator 专题【左扬精讲】—— k8s Finalizers 深度解析:对象的生命周期与删除控制
左扬 · 2026-06-16 · via 博客园 - 左扬

Kubernetes 编程 / Operator 专题【左扬精讲】—— k8s Finalizers 深度解析:对象的生命周期与删除控制

在 k8s 集群中,删除一个资源并不像数据库里 delete 一行记录那样"说没就没"。当你执行 kubectl delete deployment my-app 时,背后其实发生了两件大事:先是"告诉集群这个东西要被删了,相关的组件请做好准备"(finalization 阶段),然后才是"从 etcd 里真正抹掉这条记录"(deletion 阶段)。Finalizers 就是连接这两个阶段的关键机制——它是一把锁,锁住了资源,直到所有依赖它的外部系统都清理干净,才放行删除。本文基于 k8s v1.36.1 源码,全面解析 finalizers 的概念、原理、实操和避坑指南。

Kubernetes k8s v1.36.1 Operator Garbage Collection Go 源码级

学习重点:
  ★ 理解 Finalizers 的本质:它是一把"删除前置条件锁",不是"删除钩子"
  ★★ 掌握 Finalization 和 Deletion 两个阶段的区别与联系
  ★★★ 理解 OwnerReference 和 GarbageCollector 与 Finalizers 的配合关系
  ★★ 理解 Foreground / Background / Orphan 三种删除策略的差异
  ★ 能够正确在 Operator 中使用 Finalizers 进行资源清理


📋 文章目录

  1. 一、概念认知:什么是 Finalizers
  2. 二、价值原理:为什么需要 Finalizers
  3. 三、实操落地:如何在 YAML 中使用 Finalizers
  4. 四、源码精讲:Finalizers 的实现机制
  5. 五、踩坑避坑:Finalizers 生产环境陷阱
  6. 六、高频答疑: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(垃圾回收器)的依据——当父对象被删除时,GarbageCollector 会自动删除/保留子对象;而 Finalizers 是"删除门卫",确保清理逻辑执行完毕后才放行删除。两者可以配合使用:OwnerReference 解决"父子依赖"问题,Finalizers 解决"清理逻辑等待"问题。

二、价值原理:为什么需要 Finalizers

没有 Finalizers 之前,k8s 删除一个对象是"一删了之"——对象从 etcd 消失,但外部关联资源(如云上的持久卷、网络资源、第三方服务的订阅记录)可能根本不知道这件事。运维人员需要手动做大量清理工作,而且稍有不慎就会留下"孤儿资源"(orphaned resources)——既占用成本又无法管理。

Finalizers 的设计目的,就是让"对象被标记为删除"和"对象真正从存储中消失"这两个事件解耦。在这两个事件之间,控制器有足够的时间执行清理逻辑——取消云资源的订阅、删除关联数据、发送注销请求等。清理完成后,控制器主动把自己的 finalizer 从列表中移除,当列表为空时,k8s 才真正删除对象。

注意:Finalizers 解决的不是"对象本身的删除",而是"与对象关联的外部资源清理"问题。如果一个对象本身不持有任何外部资源(比如一个没有挂载 PVC 的 Pod),它可能根本不需要 Finalizers。

下面这张表对比了"无 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 不会自动删除它们。如果你想保留某些工作负载(迁移场景常用),这个策略很有用。

技巧:在 kubectl delete 时如果没有指定 --cascade 参数,默认行为取决于资源的类型和是否设置了 OwnerReference。对于 Deployment,默认就是 Foreground(级联删除 ReplicaSet 和 Pod)。可以用 kubectl delete --dry-run=server 查看默认策略。

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

高危操作:手动 patch 移除 finalizer 会导致清理逻辑被跳过,可能产生孤儿资源。除非你非常确定外部清理已经完成,否则不要执行此操作。

3.4 Operator 中的 Finalizer 使用模式

在使用 controller-runtime 或 client-go 编写 Operator 时,finalizer 的标准使用模式如下:

  1. 创建时添加 finalizer:在 Reconcile 函数中检测到对象没有我的 finalizer 时,用 patch 或 update 将 finalizer 添加到 metadata.finalizers 列表中。这样即使后续有人删除对象,控制器也有机会做清理。
  2. 监听 deletionTimestamp:当 Reconcile 收到一个带有 deletionTimestamp 的对象时,说明对象正在被删除。此时执行所有必要的清理工作(如调用云 API 撤销资源)。
  3. 清理完成后移除 finalizer:所有清理工作完成后,用 patch 将自己的 finalizer 从列表中移除。当 finalizers 列表为空时,k8s 会自动将对象从 etcd 中删除。
  4. 幂等性处理: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 路径
    }
}

源码要点总结:从 Store.Delete 的实现可以看出,finalizers 机制的本质是"修改 deleteImmediately 的返回值"。当对象的 finalizers 列表非空时,updateForGracefulDeletionAndFinalizers 返回 deleteImmediately=false,Store.Delete 不会执行 e.Storage.Delete(),对象就这样卡在 etcd 里等待控制器清理。控制器清理完成后 patch 移除 finalizer,再次触发 Store.Delete 时,pendingFinalizers=false,deleteImmediately=true,对象才真正消失。

五、踩坑避坑: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 对应的控制器是否正常运行。

修复

  1. 恢复控制器:先确保 Operator 进程恢复正常,让它有机会处理删除逻辑。
  2. 手动移除 finalizer(紧急):如果确认外部清理已经完成(或不需要清理),手动 patch 移除 finalizer:kubectl patch <resource>/<name> -p '{"metadata":{"finalizers":null}}' --type=merge。注意:这会跳过清理逻辑。
  3. 预防措施: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)
}

技巧:在 kubebuilder v3+ 中,使用 @kubebuilder.controllerfinalizer 注解可以自动生成 finalizer 相关代码(Init() 钩子中添加 finalizer,Reconcile() 中自动调用 cleanup 函数)。但你需要确保 cleanup 函数本身是幂等的,并且正确处理了 deletionTimestamp。

坑 3:级联删除 vs 不级联删除——选错了策略

删除一个有子对象的父资源时,如果选错了 propagationPolicy,可能导致数据丢失或者孤儿资源。

现象:删除了一个 StatefulSet,但它的 PVC 被意外保留了(孤儿资源)——PVC 还在占用云存储费用,但已经没有 Pod 使用它了。
根因:使用了 --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 特性门控)。

现象:Operator 的 Reconcile 函数没有收到被删除对象的通知,finalizer 无法被处理,对象一直卡在 Terminating 状态。
根因:在 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 个常见问题


Q1:对象卡在 Terminating 状态,如何快速定位是哪个 finalizer 导致的?

A:

执行 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 查看该对象的最新事件,看是否有清理失败的报错。


Q2:deletionTimestamp 和 DeletionGracePeriodSeconds 有什么区别?

A:

deletionTimestamp 是一个时间戳(RFC 3339 格式),表示"删除流程开始的时间",是定性指标——只要它不为 nil,对象就在删除流程中。DeletionGracePeriodSeconds 是一个整数,表示"优雅删除的等待秒数",是定量指标——它决定了对象在真正被删除前应该等待多久(Pod 的 terminationGracePeriodSeconds 就属于这个字段)。两者配合使用:deletionTimestamp 标记"开始",DeletionGracePeriodSeconds 控制"等多久"。对于带 finalizers 的对象,宽限期和 finalizers 是"与"的关系——两个条件都满足后(时间到了且 finalizers 清空了),对象才会被删除。


Q3:删除一个 Deployment 时,ReplicaSet 和 Pod 是怎么被删掉的?

A:

当删除一个 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)。


Q4:为什么 finalizer 的名字推荐用域名格式(如 myoperator.io/cleanup)?

A:

k8s 生态中有大量的 Finalizer 字符串,多个 Operator 可能共存于同一个集群。如果大家都用简单名字(如 cleanup),就可能发生冲突——你的 Operator 误删了别人的 finalizer,或者别人的 Operator 收到了你发的 finalizer。域名格式(逆向域名,如 io.myoperator.cleanup)利用了 DNS 的天然隔离特性,不同组织的域名不会冲突。此外,Finalizer 字符串在代码中被用作"标识符",k8s 源码注释中明确说"must be qualified name unless defined here"(必须是限定名),这既是惯例也是规范。


Q5:kubebuilder 中如何正确添加 finalizer?

A:

在 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,但是多余的写入)。


Q6:GarbageCollector 控制器是什么?它和 Finalizers 有什么关系?

A:

GarbageCollector(垃圾回收器)是 kube-controller-manager 中的一个控制器(pkg/controller/garbagecollector/garbagecollector.go),负责管理 k8s 对象之间的依赖关系。它通过监听所有资源的变更,维护一个"依赖图"(哪些对象是哪些对象的子对象)。当父对象被删除时,GarbageCollector 根据 OwnerReference 和 PropagationPolicy,决定如何处理子对象——是删除(Foreground)、后台删除(Background)、还是孤儿化(Orphan)。GarbageCollector 本身不处理 Finalizers,它处理的是 GC 层面的"父子级联删除"。Finalizers 则是对象级别(ObjectMeta 级别)的清理机制,由各业务控制器(Operator)处理。一个对象可能同时被 GC(管理父子关系)和多个 Finalizers(管理各自业务清理)共同管理。


Q7:删除 Namespace 时为什么会很慢?

A:

删除一个 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。


Q8:Finalizer 在 etcd 里是怎么存储的?

A:

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 都会收到通知。


Q9:PV 和 PVC 的保护 finalizer 有什么区别?

A:

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 控制器)自动管理,不需要用户手动操作。


Q10:kubectl delete --grace-period=0 和 kubectl delete --immediate 有什么区别?

A:

--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,但需要注意这两者的区别。


Q11:如何查看一个对象的所有 OwnerReference?

A:

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 就不会再把它当作"需要管理的子对象"了。


Q12:为什么 GarbageCollector 需要 disabledByDefault 机制?

A:

在早期版本的 k8s 中,GarbageCollector 会监听所有资源变更来构建依赖图。当集群规模很大(有上万种资源类型)时,这个图的内存开销非常大,而且每次资源变更都会触发图更新,严重影响 apiserver 性能。disabledByDefault 机制允许管理员通过 --generic-garbage-collector-enabled=false 禁用 GarbageCollector。禁用后,GarbageCollector 不再监听资源变更,但已构建的图仍然存在(用于处理已有的 OwnerReference)。对于大规模集群,禁用 GC 并配合显式的 --cascade=true 或 --cascade=orphan 参数使用,可以显著降低 apiserver 的内存和 CPU 开销。


Q13:kubectl delete 和 client-go 的 DeleteOptions 中 OrphanDependents 参数是什么关系?

A:

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 只在兼容旧客户端时保留。


Q14:Pod 的删除和 Finalizers 有什么关系?

A:

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。


Q15:如何让 Operator 在 finalizer 处理失败时自动重试而不是无限等待?

A:

在 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 造成过多压力。


Q16:自定义 finalizer 会不会影响 apiserver 的性能?

A:

Finalizers 本身对 apiserver 性能的影响很小——它们只是存储在 etcd 中的几个字符串,对象数量不变的情况下,finalizers 的多少不会显著增加内存或 CPU 开销。但是,如果某个对象的 finalizer 永远无法被移除(controller 崩了),这个对象会一直卡在 etcd 中,不断触发 Informer 的更新事件——这会增加所有 Watch 这个对象类型的控制器的处理负担。对于大规模集群,建议监控 Terminating 状态对象数量(kubectl get pods --field-selector=status.phase=Terminating --all-namespaces | wc -l),如果这个数字持续增长,说明有 finalizer 泄露的问题需要排查。


Q17:删除一个 CRD 时,CR(CustomResource 实例)会怎么被处理?

A:

删除一个 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。


Q18:patchStrategy:merge 在 finalizers 的上下文中是什么意思?

A:

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。


Q19:Node 节点被删除时,节点上的 Pod 是怎么处理的?

A:

删除一个 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 的主动清理。


Q20:在 k8s v1.36.1 中,Finalizers 的设计有没有什么新的变化?

A:

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