Pod 一直 Terminating,死活删不掉?三个凶手,每一个你都意想不到

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 优雅一万倍。

相关推荐
飞翔沫沫情2 小时前
K8s Alloy 采集 Pod 控制台日志
云原生·容器·kubernetes
KaifuZeng4 小时前
通信与接口协议面试十、PCIe
单片机·嵌入式硬件·面试·通信与接口协议
三月不知肉味y4 小时前
2026-07-05-JAVA面试场景题训练
java·开发语言·面试
qzhqbb7 小时前
字节面试分享复盘
面试·职场和发展
潮起鲸落入海8 小时前
Kubernetes Metric Server, Quota and Limits
容器·kubernetes
未秃头的程序猿8 小时前
虚拟线程用了一年,我真香了——10倍吞吐量是真实存在的
java·后端·面试
帅次9 小时前
Android 高级工程师面试:Flow 与状态流 近1年高频追问 22 题
android·面试·职场和发展
摇滚侠9 小时前
云原生 Java 架构师的第一课 K8s+Docker+KubeSphere+DevOps 19-25
java·云原生·kubernetes
贺国亚9 小时前
Harness-架构原理与工程实践
面试