
删除 Deployment 后，ReplicaSet 和 Pod 也陆续消失，但 Deployment Controller 并没有在一个事务里把它们全部删除。一次删除请求只作用于一个 API 对象；后续清理由垃圾回收控制器观察所有者关系后推进。

这也解释了两个看似矛盾的现象：父对象已经查不到，子对象仍然存在；父对象一直处于删除中，真正卡住的却是子对象的 Finalizer。理解它们需要把所有者关系、删除传播策略和外部资源清理分开。

<!--more-->

## 名字用来查找，UID 用来确认身份

`metadata.ownerReferences` 记录所有者的 API 版本、Kind、名字和 UID。Deployment 创建 ReplicaSet，ReplicaSet 创建 Pod 时，会在依赖对象上建立这样的引用。标签主要用于选择与组织资源，不能代替所有者关系；两个对象具有相同标签，不会自动产生级联删除关系。

UID 尤其重要。删除名为 `web` 的 Deployment 后，再创建一个同名 Deployment，新对象具有新的 UID，不会仅凭名称相同自动继承带旧 UID 的所有者引用。孤儿对象被控制器重新认领则是另一条需要检查选择器与控制器条件的路径。Kubernetes v1.35.1 的 [`isDangling`](https://github.com/kubernetes/kubernetes/blob/v1.35.1/pkg/controller/garbagecollector/garbagecollector.go#L434-L456)会读取引用指向的对象并核对 UID；名字相同而 UID 不同，仍按原所有者已经不存在处理。`attemptToDeleteItem` 还会核对待处理对象本身的 UID，避免队列里的旧记录误作用于同名新对象。

{{< github-file url="https://github.com/kubernetes/kubernetes/blob/v1.35.1/pkg/controller/garbagecollector/garbagecollector.go#L434-L456" >}}

引用有作用域限制：命名空间内的依赖对象可以引用同一命名空间的所有者或集群级所有者，不能用 ownerReference 建立跨命名空间的所有权。集群级依赖对象不能以命名空间级对象为所有者。需要跨作用域管理生命周期时，控制器必须自行设计清理协议，不能依赖这样一条无效引用。

无效引用还会产生不同后果。命名空间级依赖对象引用另一 Namespace 的对象时，GC 在依赖对象自己的 Namespace 查找所有者；找不到有效所有者就可能回收依赖对象。集群级依赖对象引用命名空间级 Kind 时，`isDangling` 返回专门的无效作用域错误，而不是把它直接当作普通的“不存在”，对象可能因此无法被正常回收。关系图发现不合法的 Namespace 关系时会产生 `OwnerRefInvalidNamespace` Warning，应结合对象事件检查，不能仅凭字段已被 API 保存就认定引用有效。

{{< github-file url="https://github.com/kubernetes/kubernetes/blob/v1.35.1/pkg/controller/garbagecollector/garbagecollector.go#L413-L450" >}}

{{< github-file url="https://github.com/kubernetes/kubernetes/blob/v1.35.1/pkg/controller/garbagecollector/graph_builder.go#L504-L534" >}}

## GC 如何从关系图走到删除请求

GC 通过 API Server 的 List/Watch 维护依赖图，把可能需要删除或孤儿化的对象放入队列。图是本地缓存，删除前仍需处理对象已经变化、引用已经变化和请求失败的情况。

[`classifyReferences`](https://github.com/kubernetes/kubernetes/blob/v1.35.1/pkg/controller/garbagecollector/garbagecollector.go#L464-L495)把所有者分成仍正常存在、已经不存在、正在等待依赖删除三类。有至少一个正常存在的所有者时，依赖对象不会仅因另一个所有者消失就被回收；GC 可以移除失效引用，再保留对象。这里的语义是“没有有效所有者后才考虑回收”，不能简化为“任意父对象删除就删除子对象”。

{{< github-file url="https://github.com/kubernetes/kubernetes/blob/v1.35.1/pkg/controller/garbagecollector/garbagecollector.go#L464-L495" >}}

```mermaid
flowchart TB
    A[从队列取得依赖对象] --> B[重新读取 API 对象并核对 UID]
    B --> C{当前还有所有者引用吗}
    C -->|没有| D[不因所有者关系回收]
    C -->|有| E[查询并分类所有者]
    E --> F{还有正常存在的所有者吗}
    F -->|有| G[保留对象 清理失效引用]
    F -->|没有| H[按传播策略推进删除]
    H --> I[通过 API Server 写入删除或引用更新]
```

图只展开回收判断，省略前台删除和环路处理。API 错误会返回 worker，由队列重新处理；GC 不是收到一次事件就必须一次完成所有后续操作。

## 三种传播策略改变的是父子对象的删除关系

后台删除允许所有者先从 API 中消失，GC 随后清理失去有效所有者的依赖对象。客户端已经收到删除成功，并不表示所有子对象都消失了。

对本文的 Deployment，未指定传播策略、对象又没有改变传播行为的已有 Finalizer 时，默认走后台级联清理。不要把这个默认值推广成所有资源和所有对象状态都相同：通用存储层优先考虑显式 DeleteOptions，再考虑已有 Finalizer 和资源的默认 GC 策略。Deployment 的 `DefaultGarbageCollectionPolicy` 返回 `DeleteDependents`，而前台删除标记是否添加，还要经过 `shouldDeleteDependents` 的判断。

{{< github-file url="https://github.com/kubernetes/kubernetes/blob/v1.35.1/pkg/registry/apps/deployment/strategy.go#L51-L55" >}}

{{< github-file url="https://github.com/kubernetes/kubernetes/blob/v1.35.1/staging/src/k8s.io/apiserver/pkg/registry/generic/registry/store.go#L930-L967" >}}

前台删除先给所有者设置 `deletionTimestamp` 和 `foregroundDeletion` Finalizer。GC 推进阻塞型依赖的删除，依赖满足清理条件后再移除所有者的 Finalizer。关键判断在 [`processDeletingDependentsItem`](https://github.com/kubernetes/kubernetes/blob/v1.35.1/pkg/controller/garbagecollector/garbagecollector.go#L654-L673)：阻塞型依赖列表为空，才移除前台删除标记；否则把依赖对象加入删除队列。

{{< github-file url="https://github.com/kubernetes/kubernetes/blob/v1.35.1/pkg/controller/garbagecollector/garbagecollector.go#L654-L673" >}}

这里要同时检查引用的 `blockOwnerDeletion`。前台删除所等待的是 GC 能观察到、并且被标为阻塞的依赖对象，不是任意业务关联对象，也不是集群中一切相关进程。设置这个字段还受到准入与权限检查约束，不能把它当作绕过所有者删除权限的方式。

孤儿化则保留依赖对象，移除它们对原所有者的引用，完成关系解除后删除所有者。保留 ReplicaSet 不会自动把它的副本数设为零；它仍可以继续管理自己拥有的 Pod。对运行中工作负载执行 orphan 前，需要明确接下来由谁管理。

## Finalizer 是清理协议中的阻塞标记

Finalizer 本身不会执行清理。控制器识别自己的 Finalizer，完成外部资源回收，再通过 API 移除标记。控制器不运行、权限不足或外部服务失败，都可能让对象保持删除中。

```mermaid
sequenceDiagram
    participant U as 用户
    participant A as API Server
    participant G as GC
    participant C as 清理控制器
    U->>A: 前台删除所有者
    A-->>G: Watch 观察到删除标记
    G->>A: 删除阻塞型依赖
    A-->>C: Watch 观察到依赖的 deletionTimestamp
    C->>C: 执行该 Finalizer 对应的清理
    C->>A: 清理完成 移除依赖 Finalizer
    A-->>G: Watch 观察到依赖消失
    G->>A: 无阻塞依赖 移除 foregroundDeletion
```

这不是全局同步屏障。图里的清理控制器只为自己负责的 Finalizer 作出完成承诺；GC 无法证明未被这个协议覆盖的外部资源已经安全释放。强行清空 Finalizer 会跳过承诺，可能留下云盘、负载均衡器或其他资源。

## 实验：让一个 Finalizer 把差异保留下来

实验在 `zj` 的独立 kind 集群 `article-control` 上运行，Kubernetes 和 kubectl 均为 v1.35.1。为排除 Pod 终止和业务控制器的干扰，每组只使用两个 ConfigMap：一个作为所有者，一个带 `article.example/hold` Finalizer 的依赖对象，引用设为 `blockOwnerDeletion=true`。

三组分别发起 foreground、background 和 orphan 删除，并以 API 状态断言结果：

```text
foreground: child terminating
foreground: owner retained by blocking child
background: owner absent
background: child terminating with finalizer
orphan: owner absent
orphan: child ownerReferences removed
```

前台组中，依赖对象已有删除时间戳，所有者仍存在，并带 `foregroundDeletion`。后台组中，所有者已经不存在，依赖对象仍被实验 Finalizer 留住。孤儿组中，依赖对象没有删除时间戳，所有者引用被移除。逐组清除实验 Finalizer 后，脚本检查预期删除的对象确实消失。

配置、脚本、证据和清理说明见 [k8sdev/article-control-gc](https://github.com/jimyag/k8sdev/tree/3cfa654905a750b1716de8c32aa42d7573220954/article-control-gc)。此实验验证 API 对象的 GC 行为，没有验证容器退出或外部资源释放。Pod 终止需要继续阅读 [Pod 删除流程](/posts/kubernetes-pod-terminating/)。先按该目录 README 的固定提交准备步骤创建独立工作目录，再运行实验。

三种传播策略分别检查所有者、依赖的删除状态和引用变化，避免只等待一个对象消失。

{{< github-file url="https://github.com/jimyag/k8sdev/blob/3cfa654905a750b1716de8c32aa42d7573220954/article-control-gc/run.py#L32-L53" >}}

## 删除卡住时沿阻塞关系检查

先看对象的 `deletionTimestamp` 和 `finalizers`，确认删除是否已受理。再根据 UID 找到真正的所有者和阻塞型依赖，区分对象自己的 Finalizer 卡住，还是前台删除正在等待子对象。仅按名字搜索可能把重建后的对象混进来。

如果依赖引用有效且清理控制器仍在运行，继续检查控制器权限、错误和外部清理结果。只有已经确认对应清理可以安全跳过，才考虑人工移除特定 Finalizer。把所有 Finalizer 一次清空，虽然能让 API 对象消失，却不能证明清理完成。

