kubectl delete pod my-pod # 5 分钟过去了…… kubectl get pod my-pod # my-pod 0/1 Terminating 0 5m30s你盯着 Terminal,光标一闪一闪。Pod 还是 Terminating。
你加了--force --grace-period=0,还是删不掉。你又加了--force --grace-period=0,这次终于没了——但你不知道刚才那一刀下去,到底留下了什么后遗症。
🧠Pod 从"删"到"消失"——你以为很快,其实藏了六道门
大多数人脑子里 Pod 删除的过程:
kubectl delete pod → Terminating → 消失
真实的完整流程:
kubectl delete pod │ ▼ API Server 设 DeletionTimestamp + DeletionGracePeriodSeconds │ ▼ kubelet 收到 DELETE 事件 │ ├── 1. 执行 preStop hook(如果配了) ← 可能卡死 ├── 2. 等待 terminationGracePeriodSeconds ← 默认 30s ├── 3. 给容器发 SIGTERM ← 应用可能不响应 ├── 4. 等待 SIGKILL ← 又给了 2s ├── 5. 卸载 Volume(umount) ← 可能永远卡住 ├── 6. 通知 API Server 删 Pod │ ▼ Finalizer 检查 → 所有 Finalizer 移除 → Pod 真正删除 │ ▼ GC 回收
每一道门都可能卡死,而且卡在不同的门,排查方式完全不同。生产环境里最常卡的是三道门:finalizer 挡路、preStop 死等、Volume 卸载失败。
⚔️第一凶手:Finalizer——Pod 删不掉的罪魁祸首
Finalizer 是 Kubernetes 的一个保护机制:在 Pod 被真正删除前,必须先通过某种"外部审批"。
看一个卡住的 Pod:
kubectl get pod stuck-pod -o yaml | grep -A5 finalizers
metadata: deletionTimestamp: "2026-05-27T14:00:00Z" # 删了很久了 finalizers: - kubernetes.io/pvc-protection # ← 就是它在挡路
kubernetes.io/pvc-protection这个 finalizer 说:这个 Pod 还引用着 PVC,在 PVC 安全解除绑定前,不准删 Pod。
为什么 PVC 还没解除绑定?因为 Volume 还没 umount。为什么没 umount?因为 kubelet 卸载的时候发现文件系统还在被占用(device is busy)。
死循环:
Pod 等 finalizer 移除 → finalizer 等 PVC 释放 → PVC 等 umount 完成 → umount 等 Pod 停止容器 → Pod 在等 finalizer 移除
这就是经典的"卡死 Terminating"现场。
怎么破?手动拆断链条:
# 1. 先看是哪个 finalizer 在挡路 kubectl get pod stuck-pod -o jsonpath='{.metadata.finalizers}' # 2. 直接干掉它(危险操作,确保你知道后果) kubectl patch pod stuck-pod -p '{"metadata":{"finalizers":null}}' --type=merge
⚠️ 但这样做的后果是什么?PVC 没有被正确 detach→ 下一个使用同一 PVC 的 Pod 可能挂载失败 → 数据不一致。
👉这不是一条随意执行的命令。你在绕过一道安全门。
除了 PVC finalizer,常见的挡路鬼还有:
foregroundDeletion— 级联删除模式- 自定义 CRD finalizer(Operator 加的)
- Service Account Token 的 finalizer
🧩第二凶手:preStop Hook——你亲手写的死循环
lifecycle: preStop: exec: command: - /bin/sh - -c - "echo 'shutting down' && sleep 60 && curl http://localhost:8080/notify"
问题在哪?如果localhost:8080已经在 Nginx 退出后不可达了,curl会一直卡着(默认超时没有设置)。
preStop 启动 → 尝试执行 curl → 连不上 → 超时等待(可能很久) → kubelet 在等 preStop 完成 → Pod 在 Terminating 状态
只要 preStop 没返回,kubelet 就不会给容器发 SIGTERM。Pod 就在 Terminating 状态无限等待。
👉 最狠的场景:preStop 里写了一个永远不会返回的命令,Pod 永远删不掉——直到你--force。
preStop 的正确写法:
lifecycle: preStop: exec: command: - /bin/sh - -c - | # 1. 设超时 timeout 10 curl http://localhost:8080/notify || true # 2. 等 in-flight 请求完成(必须设上限) sleep 5 # 3. 明确退出 exit 0
三条铁律:
- 所有网络调用必须有
timeout— curl、wget、grpc_health_probe 统统加超时 - 所有 sleep 不能超过
terminationGracePeriodSeconds - 5s— 否则没等到 SIGTERM 就被 SIGKILL 了 - 必须
exit 0— 返回值不为 0 不代表"重试",kubelet 不会重试,它只会等
💥第三凶手:Volume 卸载失败——"device is busy"
这是最难排查的,因为日志很少。
kubelet 在 Pod Terminating 时会依次卸载所有 Volume。但在容器还没完全停止时,Volume 可能还在被进程使用。
nginx 容器在写访问日志 → 对 Volume 持有文件句柄 kubelet 尝试 umount → 返回 "device is busy" kubelet 重试 umount → 还是 busy 重试 → 还是 busy → 重试……
在 kubelet 日志里看不到明确的错误——它会一直重试,直到天荒地老。
源码在pkg/kubelet/volumemanager/:
func (dsw *desiredStateOfWorldPopulator) checkVolumeUnmount() { // 重试逻辑不会停止,没有最大重试次数 // 所以 Pod 就会永远 Terminating }
👉 这个场景最常发生在:
- NFS/CephFS 挂载— 网络文件系统,umount 失败率远高于本地存储
- hostPath— 容器内进程 touch 了一个文件,忘了关,文件句柄一直活着
- subPath 挂载— 比普通 Mount 多一层文件系统绑定,出错概率更高
怎么确认是 Volume 的问题?
# 在节点上查看挂载点 mount | grep <pod-uid> # 如果挂载点还在 → Volume 没卸载 # 如果能找到 → 检查是否有进程在用这个挂载点 lsof | grep <mount-point>
如果有进程还在用——kill 掉那个进程,umount 就走得通了。
怎么预防?
spec: terminationGracePeriodSeconds: 60 # 给足时间让应用优雅关闭文件
如果你知道你的应用需要 10 秒来关闭文件和连接——terminationGracePeriodSeconds至少设 30 秒。默认也是 30 秒,但给了就是给了。
⚔️SIGTERM 被无视了——你的应用是个"聋子"
kubelet 发给容器的第一个信号是 SIGTERM(相当于kill -15)。这意味着:应用可以捕获这个信号,做一些清理工作。
但如果应用没有正确处理 SIGTERM 呢?常见的防坑:
Node.js 没有处理 signal:
// ❌ 这样写,SIGTERM 直接杀进程——不等任何 in-flight 请求 const express = require('express'); const app = express(); app.listen(3000); // 没有 process.on('SIGTERM', ...) = 暴力退出
// ✅ 正确写法 process.on('SIGTERM', () => { console.log('Received SIGTERM, closing server...'); server.close(() => { console.log('Server closed, exiting'); process.exit(0); }); // 设一个超时以防万一 setTimeout(() => process.exit(1), 5000); });
Java 同理——JVM 不会自动处理 SIGTERM,你得用Runtime.getRuntime().addShutdownHook()。
Go 同理——signal.Notify()是必须的。
👉如果进程忽略了 SIGTERM,kubelet 会等terminationGracePeriodSeconds秒,然后发 SIGKILL。SIGKILL 是不可捕获的——进程被直接杀掉,没有清理机会。
这解释了为什么有时候 Pod 删得很快(进程收到 SIGTERM 正确退出),有时候超慢(进程忽略 SIGTERM,硬生生等到 grace period 结束才被 SIGKILL 爆头)。
💥终极杀招:--force --grace-period=0——你知道你在干什么吗?
当一切卡住,--force --grace-period=0就是你的最后手段:
kubectl delete pod stuck-pod --force --grace-period=0
这是用DeleteOptions里的GracePeriodSeconds: 0直接从 etcd 删 Pod。API Server 不会等 kubelet 确认容器已停止——直接删记录。
⚠️后果清单:
1️⃣容器可能还在运行— kubelet 最终会发现 Pod 已经不存在了,然后清理容器。但这个过程有延迟。同一时间可能有"僵尸容器"在消耗节点资源。
2️⃣Volume 没被正确卸载— 这是最危险的。如果 Pod 用的是 RWO(ReadWriteOnce)模式的 PVC,下一个绑定同一 PVC 的 Pod 到不同节点时,Volume 因为还没 umount 而挂载失败。
3️⃣StatefulSet 的"有序删除"被打破— StatefulSet Controller 认为 Pod 还在、绑定也还在。force delete会在它背后抽掉基石,导致 PVC 和 Pod 的绑定关系破裂。
4️⃣preStop hook 没执行— 日志没被优雅 flush、连接没被优雅关闭。下游看到的是连接突然断开——不是一个优雅的 FIN,而是断崖式的 RST。
👉--force --grace-period=0不是"解决问题",是"跳过问题"。跳过之后出的新问题,往往比卡住 Terminating 更麻烦。
🎯排查流程图:Pod 卡在 Terminating 时按什么顺序查?
Pod 卡在 Terminating? → 第 1 步:kubectl describe pod 看 Events | ├─ 有 "unmount failed" 或 "device busy" │ → 凶手:Volume 卸载失败 │ → 解法:上节点 lsof + fuser 查谁在用挂载点 │ ├─ Events 为空 + finalizers 非空 │ → 凶手:finalizer 在挡路 │ → 解法:kubectl get pod -o yaml | grep finalizers │ 确认后可手动 patch 移除(评估后果) │ └─ Events 为空 + finalizers 为空 + 时间还没到 30s → 凶手:preStop hook 在执行中 或 进程没响应 SIGTERM → 解法:等 terminationGracePeriodSeconds 超时 如果配得太长 → 改短后再删
💡最后一击
Pod 卡在 Terminating 不是"Kubernetes 坏了",恰恰是"Kubernetes 在遵守你设定的规则"。
Finalizer 在等你确认 PVC 安全脱钩——是你写的规则。preStop hook 在执行你写的脚本——是你让它等的。terminationGracePeriodSeconds 在给你留优雅关闭的时间——是你设的。
这些机制都在保护你——但它们保护得太认真了。认真到 Pod 真的删不掉。
下次卡住,别急着--force --grace-period=0。打开kubectl describe pod,看看 Events、查查 finalizers、上节点lsof看一眼挂载点。知道凶手在哪,一刀毙命——比暴力删 Pod 优雅一万倍