bash
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 删除的过程:
arduino
kubectl delete pod → Terminating → 消失
真实的完整流程:
arduino
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:
bash
kubectl get pod stuck-pod -o yaml | grep -A5 finalizers
yaml
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"现场。
怎么破?手动拆断链条:
bash
# 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------你亲手写的死循环
yaml
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 的正确写法:
yaml
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 可能还在被进程使用。
arduino
nginx 容器在写访问日志 → 对 Volume 持有文件句柄
kubelet 尝试 umount → 返回 "device is busy"
kubelet 重试 umount → 还是 busy
重试 → 还是 busy → 重试......
在 kubelet 日志里看不到明确的错误------它会一直重试,直到天荒地老。
源码在 pkg/kubelet/volumemanager/:
go
func (dsw *desiredStateOfWorldPopulator) checkVolumeUnmount() {
// 重试逻辑不会停止,没有最大重试次数
// 所以 Pod 就会永远 Terminating
}
👉 这个场景最常发生在:
- NFS/CephFS 挂载 --- 网络文件系统,umount 失败率远高于本地存储
- hostPath --- 容器内进程 touch 了一个文件,忘了关,文件句柄一直活着
- subPath 挂载 --- 比普通 Mount 多一层文件系统绑定,出错概率更高
怎么确认是 Volume 的问题?
bash
# 在节点上查看挂载点
mount | grep <pod-uid>
# 如果挂载点还在 → Volume 没卸载
# 如果能找到 → 检查是否有进程在用这个挂载点
lsof | grep <mount-point>
如果有进程还在用------kill 掉那个进程,umount 就走得通了。
怎么预防?
yaml
spec:
terminationGracePeriodSeconds: 60 # 给足时间让应用优雅关闭文件
如果你知道你的应用需要 10 秒来关闭文件和连接------terminationGracePeriodSeconds 至少设 30 秒。默认也是 30 秒,但给了就是给了。
⚔️ SIGTERM 被无视了------你的应用是个"聋子"
kubelet 发给容器的第一个信号是 SIGTERM(相当于 kill -15)。这意味着:应用可以捕获这个信号,做一些清理工作。
但如果应用没有正确处理 SIGTERM 呢?常见的防坑:
Node.js 没有处理 signal:
javascript
// ❌ 这样写,SIGTERM 直接杀进程------不等任何 in-flight 请求
const express = require('express');
const app = express();
app.listen(3000);
// 没有 process.on('SIGTERM', ...) = 暴力退出
javascript
// ✅ 正确写法
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 就是你的最后手段:
bash
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 时按什么顺序查?
sql
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 优雅一万倍。