
执行 `kubectl delete pod` 后，终端已经打印 deleted，但 Pod 仍然显示 Terminating。再加上 `--force --grace-period=0`，有时对象仍然存在；有时对象消失了，节点上的容器却还在运行。

Finalizer 决定 API 对象是否还需要保留，kubelet 负责让节点上的容器退出。本文在 `zj` 的 kind 集群分别阻塞这两条路径，观察删除请求怎样继续推进。

<!--more-->

## deleted 不能单独证明进程已经停止

一次 Pod 删除会涉及 API Server、kubelet、容器运行时，以及可能承担清理责任的控制器。它们不是一次同步完成的函数调用。

API Server 接受删除请求后，Pod 可以带上 `deletionTimestamp`；kubelet 需要终止对应容器，其他组件也可能需要完成自己的清理。`kubectl get` 显示的 Terminating 是面向用户的状态摘要，不是 `status.phase` 的一个新枚举。

正常的优雅终止中，kubelet 发现删除请求后，按配置执行 `preStop`，再要求运行时向容器主进程发送终止信号。`preStop` 消耗的时间也计入终止宽限期；宽限期结束后，仍未退出的进程会被强制终止。应用应在这段时间内停止接收新工作并处理正在执行的请求。

Service 的 EndpointSlice 更新与节点终止异步推进。终止中的端点通常会标记为未就绪，数据面随后据此调整新连接的转发；这不保证所有已有连接已经排空。API 对象上的 Finalizer 又是另一项删除条件：即使容器已经停止，只要负责清理的控制器还没移除标识，对象仍可能保留。

排查时应分别获取删除时间、Finalizer 和运行时状态。前两项解释控制面在等什么，最后一项确认进程是否真的退出。优雅终止的顺序依据 [Pod 终止流程](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-termination)；下面两组实验分别检查 Finalizer 和 kubelet 中断后的行为。

## 删除为什么先变成一次更新

在 v1.35.1 的 [通用存储删除实现](https://github.com/kubernetes/kubernetes/blob/v1.35.1/staging/src/k8s.io/apiserver/pkg/registry/generic/registry/store.go#L1044)中，需要优雅删除或等待 Finalizer 的请求，会先经过 `updateForGracefulDeletionAndFinalizers`。它通过存储层的条件更新记录删除状态，再判断是否可以立即移除对象。宽限期已经为零，但仍有 Finalizer 时，函数仍然返回“不能立即删除”。这就是下面第一组实验中 `--force` 没有让对象消失的原因。

把删除意图留在对象上，有一个实际作用：负责清理的控制器重启后，仍然可以从当前对象读到“需要删除”，继续处理未完成的清理。它不必收到最初那一次 DELETE 请求，也不需要 API Server 一直挂着请求等待清理结束。Finalizer 字符串只标记责任；列表顺序不构成控制器执行顺序。

```mermaid
flowchart TB
    A[API Server 接受删除请求] --> B[对象记录删除意图]
    B --> C[kubelet 终止容器]
    B --> D[控制器完成清理并移除 Finalizer]
    D --> E{API 删除条件满足?}
    E -->|否| B
    E -->|是| F[API 对象移除]
    C --> G[节点上的进程退出]
```

图中的两条分支由不同组件推进。普通优雅终止需要 kubelet 参与；强制删除可以提前移除没有 Finalizer 阻塞的 API 对象，所以图中没有从“对象移除”指向“进程退出”的保证。第二组实验会直接检查这个差别。

## 实验一：没有控制器处理的 Finalizer

环境为 kind v0.33.0、Kubernetes v1.35.1，运行在 Linux 主机 `zj`。先创建普通 Pod `held`，加上实验用 Finalizer：

```yaml
# 这是故意设置的清理阻塞；实验没有为它启动控制器。
metadata:
  name: held
  finalizers:
    - lab.example.com/cleanup # 表示清理责任尚未解除。
```

Pod Ready 后，脚本发送普通删除请求，观察到 `deletionTimestamp` 已设置，但对象保留。随后执行强制删除：

```sh
kubectl --context kind-deep-dive -n dd-terminating delete pod held \
  --force --grace-period=0 --wait=false
kubectl --context kind-deep-dive -n dd-terminating get pod held -o yaml
```

实际对象仍然存在，`finalizers` 中仍有 `lab.example.com/cleanup`。这说明缩短终止宽限期并不会自动完成 Finalizer 的清理责任。

Finalizer 是对象上的标识，API Server 不会根据这个字符串自动执行某段清理代码。对应控制器需要发现删除请求，完成清理，再移除自己的标识。机制说明见 [Finalizers 文档](https://kubernetes.io/docs/concepts/overview/working-with-objects/finalizers/)。

本实验没有任何外部资源需要释放，所以脚本在确认这一点后移除了实验标识，对象随即删除。实际环境不能照搬“把 finalizers 清空”：如果它代表卸载存储、释放 IP 或删除外部资源，强行移除可能只让 API 对象消失，却留下真实资源。

## 实验二：对象消失了，容器还在运行

第二组创建不带 Finalizer 的 Pod `orphan-process`，指定在 `deep-dive-worker2` 运行。等它 Ready 后，记录真实的 container ID，然后只停止这个 kind 节点容器内部的 kubelet；containerd 继续运行。

接着强制删除 Pod。API 对象消失后，脚本通过节点容器中的 `crictl inspect` 检查先前记录的 container ID，实际得到：

```text
api_object_exists: false
container_state: CONTAINER_RUNNING
```

这个结果直接说明：API 对象删除不能证明 kubelet 已经停止旧进程。脚本恢复 kubelet 后继续等待，确认旧容器不再处于 Running，才结束实验。

这里没有关闭 `zj` 的 kubelet 或 Docker 服务，也没有让宿主机断电；故障注入的对象只是专用 kind worker 内的 kubelet。实验隔离了“控制面仍可访问，但节点代理不执行终止动作”这一条件。

## 为什么有状态应用尤其需要确认旧进程

如果控制器在别处创建了替代副本，而旧进程仍然能访问共享存储或外部服务，就可能出现两个实例同时操作同一份数据。是否安全，需要应用的成员协议、存储隔离或其他 fencing 手段保证，不能靠新 Pod 的 Ready 状态倒推旧进程已经退出。

StatefulSet 的稳定名称和身份也不能消除这个问题。强制删除只是缩短或绕过部分对象删除等待，不是对旧主机执行了物理隔离。Kubernetes 对强制删除的说明见 [Pod 终止流程](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-termination)。

## Terminating 应该从哪里查

先获取完整对象，确认删除时间、宽限期、Finalizer、节点和容器状态：

```sh
kubectl --context kind-deep-dive -n dd-terminating get pod held -o yaml
kubectl --context kind-deep-dive -n dd-terminating describe pod held
kubectl --context kind-deep-dive get nodes
```

如果 Finalizer 仍在，查负责该标识的控制器是否运行、是否在重试清理，以及它等待的外部条件。若节点不可达或 kubelet 未工作，API 中的容器状态也可能是旧报告，需要通过节点或运行时证据确认。

如果节点和 kubelet 正常，则继续核对应用退出、终止钩子、宽限期和挂载清理。不要把所有 Terminating 都归因于 Finalizer，也不要看到 Finalizer 就立即移除。本文的两组实验刻意各保留一个阻塞条件，生产现场可能同时有多个。

实验代码与使用说明见 [k8sdev/deep-dive-terminating](https://github.com/jimyag/k8sdev/tree/8f9bfbbd44ac061702310f332ee949fcdf0a213d/deep-dive-terminating)。

