jimyag's Blog

Kubernetes Pod 删除流程:进程终止、Finalizer 与强制删除

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

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

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

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

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

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

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

排查时应分别获取删除时间、Finalizer 和运行时状态。前两项解释控制面在等什么,最后一项确认进程是否真的退出。优雅终止的顺序依据 Pod 终止流程;下面两组实验分别检查 Finalizer 和 kubelet 中断后的行为。

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

在 v1.35.1 的 通用存储删除实现中,需要优雅删除或等待 Finalizer 的请求,会先经过 updateForGracefulDeletionAndFinalizers。它通过存储层的条件更新记录删除状态,再判断是否可以立即移除对象。宽限期已经为零,但仍有 Finalizer 时,函数仍然返回“不能立即删除”。这就是下面第一组实验中 --force 没有让对象消失的原因。

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

  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:

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

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

1
2
3
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 文档。

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

实验二:对象消失了,容器还在运行

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

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

1
2
api_object_exists: false
container_state: CONTAINER_RUNNING

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

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

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

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

StatefulSet 的稳定名称和身份也不能消除这个问题。强制删除只是缩短或绕过部分对象删除等待,不是对旧主机执行了物理隔离。Kubernetes 对强制删除的说明见 Pod 终止流程。

Terminating 应该从哪里查

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

1
2
3
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。

#Kubernetes #Kind #Troubleshooting