Skip to content

Pod 卡在 Terminating 删不掉

01 前置理论与核心机制

1. 强制删除(--force)的资源残留风险

  • 控制面假象:遇到 Pod 无法正常删除时,若直接执行 kubectl delete pod --force --grace-period=0,在底层仅是通知 API Server 强制从 ETCD 中抹除该 Pod 的元数据对象,使命令行列表看起来已清理完毕。
  • 数据面残留:强制删除并未解决宿主机(Node)上的实际阻塞问题。如果 Kubelet 是因存储插件(CSI)异常或文件句柄被占用而卡住,宿主机上的虚拟网卡、挂载点和数据卷依然处于占用状态。一旦后续有新 Pod 调度至该节点,极易因挂载点冲突或设备繁忙(Device or resource busy)而无法启动,引发二次故障。

2. Kubelet 停止 Pod 的清理生命周期

当 Kubelet 停止一个 Pod 时,会依次执行:下发 SIGTERM -> 超出宽限期下发 SIGKILL -> 卸载 Volume 数据卷 -> 拆除 Sandbox 网络 -> 清理容器运行时残留。 若 Pod 长时间停留在 Terminating 状态,通常是因为阻塞在了 卸载 Volume 数据卷 阶段。此时必须登录宿主机,沿着进程与文件系统挂载链路进行排查。

02 故障排查与恢复闭环 SOP

面对长时间处于 Terminating 状态且无法退出的 Pod,应避免直接强制删除,按以下宿主机排查链路处理:

步骤一:确认容器运行时的底层状态

登录该 Pod 所在的宿主机(Worker Node),绕过 K8s API,直接使用底层的容器运行时工具查看容器真实状态:

# 假设底层运行时为 containerd,使用 crictl 查找该 Pod 相关的容器残留
crictl ps -a | grep test-stuck-pod
  • 观测点:若无任何输出,说明业务容器进程本身已被清理完毕,阻塞点位于外层的文件系统卸载或网络拆除阶段。

步骤二:提取 Pod UID 作为追踪标识

在具备 kubectl 权限的终端上,获取该 Pod 的全局唯一标识符(UID),作为在宿主机文件系统中定位其挂载目录的标识:

kubectl get pod test-stuck-pod -o custom-columns=NAME:.metadata.name,UID:.metadata.uid
  • 观测点:记录返回的 UID 字符串(例如 b52cfaaa-e0ea-4ec5-b485-501a908e0e2f)。

步骤三:排查宿主机数据卷挂载残留

切回故障宿主机,利用获取到的 UID,检查该 Pod 对应的数据卷是否仍挂载在操作系统上:

# 查看系统挂载表中是否仍有该 UID 的残留记录
mount | grep b52cfaaa-e0ea-4ec5-b485-501a908e0e2f

  • 观测点:若挂载点依然存在(如 tmpfs on /var/lib/kubelet/pods//volumes/...),说明 Kubelet 在执行 umount 卸载时失败,通常是因为宿主机上有进程正在占用该目录。

步骤四:定位占用文件句柄的异常进程

使用 lsof(List Open Files)命令,结合 UID 检索正在访问这些挂载目录的进程:

# 屏蔽权限报错输出,精准匹配占用该 UID 目录的进程
lsof 2>/dev/null | grep b52cfaaa-e0ea-4ec5-b485-501a908e0e2f

  • 观测点:终端会输出正在占用该目录的进程列表。重点关注第二列的 PID(进程号)。常见的占用方包括宿主机上的日志采集 Agent、监控巡检脚本,或未正确退出的孤儿进程。

步骤五:终止占用进程,等待自然回收

获取占用进程的 PID 后,无需在 K8s 层面执行强制删除,直接在宿主机操作系统层面终止该异常进程:

# 强制终止占用挂载点的异常进程
kill -9

  • 验证闭环:异常进程终止后,文件句柄被立即释放。处于重试等待状态的 Kubelet 会自动继续执行后续流程,完成残留数据卷的卸载与目录清理。此时再次执行 kubectl get pods,处于 Terminating 状态的 Pod 会自动消失。

03 真实生产故障现象与案例库

案例 1:日志采集组件持续占用文件句柄

  • 故障现象:大量执行完批处理任务的 Job Pod 长时间卡在 Terminating 状态。
  • 排查结果:在宿主机通过 lsof | grep 排查发现,节点上以 DaemonSet 方式部署的 Filebeat 进程正在持续读取 Pod 数据卷中遗留的大体积日志文件。
  • 根因与处置:Kubelet 尝试卸载目录时,因 Filebeat 持有文件描述符未释放,导致 umount 报错 target is busy。处置方案为重启该节点的 Filebeat 实例以释放句柄,并调整 Filebeat 配置中的 close_removed 与 close_timeout 参数,使其及时关闭已删除容器的日志文件句柄。

案例 2:NFS 远端存储断连导致 D 状态阻塞

  • 故障现象:业务 Pod 挂载了外部 NFS 存储,因 NFS 服务端网络中断,该节点上所有关联 Pod 全部卡在 Terminating 状态。
  • 排查结果:crictl 确认容器已退出,mount 可见挂载残留。但执行 lsof 或 df -h 时,命令行终端直接卡死无响应。
  • 根因与处置:远端网络存储异常断开导致内核 I/O 调用陷入不可中断睡眠(Uninterruptible Sleep,即 D 状态)。任何触碰该挂载点的系统调用都会被内核挂起。此时无法通过 kill -9 终止相关进程,必须执行延迟卸载命令 umount -l /var/lib/kubelet/pods//volumes/...,使内核将该路径从挂载树中剥离。

案例 3:API 层面的 Finalizers 保护锁阻断

  • 故障现象:Pod 长期处于 Terminating 状态,但登录宿主机检查发现容器、网络及挂载点均已清理完毕,Kubelet 日志也无报错。
  • 排查结果:执行 kubectl get pod -o yaml 查看资源定义,发现 metadata.finalizers 字段下残留了自定义控制器的标识(如自研 Operator 注入的钩子)。
  • 根因与处置:此类情况并非宿主机资源阻塞,而是 K8s 控制面的逻辑保护锁。由于对应的外部控制器异常未能及时移除 Finalizer 标记,API Server 拒绝从 ETCD 中删除该对象。处置方案为确认底层资源已释放后,手动执行 Patch 移除该字段:kubectl patch pod -p '{"metadata":{"finalizers":null}}'。

04 生产排障速查表(Terminating 状态阻塞)

顺序排查层级执行命令 / 观测点异常定性处置动作01API 逻辑层检查 Pod YAML 的 finalizers 字段存在未清理的控制器保护标记确认安全后执行 kubectl patch 清空 finalizers02运行时层宿主机执行 crictl ps -a容器进程仍处于运行或停止失败状态检查 containerd/docker 状态,手动停止残留容器03挂载卷层获取 UID,执行 mount | grep <UID>宿主机存在未卸载的数据卷挂载点确认 Kubelet umount 操作受阻,转入句柄排查04句柄追踪lsof 2>/dev/null | grep <UID>检索出占用挂载目录的具体进程 PID确认外部进程占用了 Pod 的挂载数据卷05进程清理kill -9 <PID>(或 umount -l)释放文件句柄或剥离失效的远端挂载Kubelet 自动恢复清理流程,Pod 正常销毁

05 面试应答参考话术

面试官问:“线上发版时,偶尔会碰到几个 Pod 长时间停在 Terminating 状态删不掉。遇到这种情况你怎么处理?”

可以这样回答:遇到 Pod 卡在 Terminating 状态,我不会直接执行带 --force --grace-period=0 的强制删除命令。因为强制删除只是抹除了 API Server(ETCD)中的元数据对象,宿主机底层卡住的挂载点或网络资源并没有真正释放。如果直接强删,后续新 Pod 调度到该节点时,很容易因为挂载点冲突或设备繁忙而无法启动。标准的排查思路是顺着 Kubelet 销毁 Pod 的生命周期,到宿主机底层定位阻塞点:第一步:获取 Pod UID。

在控制面执行 kubectl get pod 提取该故障 Pod 的 UID,这是宿主机管理 Pod 目录的唯一标识。第二步:排查宿主机容器与挂载残留。

登录该 Pod 所在的宿主机,先用 crictl ps -a 确认容器进程本身是否已经退出。排除容器进程卡死后,执行 mount | grep <UID> 检查该 Pod 的 Volume 数据卷是否还挂载在宿主机上。绝大多数 Terminating 卡死都是因为 Kubelet 在卸载数据卷(umount)时失败了。第三步:定位并清理占用句柄的进程。

如果确认挂载点还在,直接执行 lsof | grep <UID>,排查是宿主机上的哪个外部进程(例如日志采集 Agent 或监控进程)正在读写该目录导致句柄无法释放。找到对应的进程 PID 后,在宿主机执行 kill -9 将其终止。文件句柄一旦释放,Kubelet 就会自动完成后续的卸载清理流程,此时无需执行强制删除,K8s 中的 Pod 就会正常消失。最后我也会补充:如果宿主机上没有任何挂载和进程残留,那问题通常出在控制面的 finalizers 逻辑锁上,此时再通过 kubectl patch 清空 finalizers 字段即可。

06 面试高频追问与应答预案

追问 1:如果 lsof 查不出任何占用进程,而且 mount | grep UID 也查不到任何残留,Pod 依然处于 Terminating 状态是怎么回事?

  • 回答要点:
  • 这说明宿主机数据面层面的资源已经被 Kubelet 彻底清理完毕,阻塞点位于 K8s 控制面(API Server)。
  • 原因通常是该 Pod 的 YAML 定义中包含 metadata.finalizers 字段(例如存储保护插件或自定义 Operator 注入了前置清理钩子,但控制器自身异常未能移除该标记)。确认宿主机无残留后,可执行 kubectl patch pod <name> -p '{"metadata":{"finalizers":null}}' 手动移除该字段,Pod 即可被立即清理。

追问 2:如果找到了占用挂载点的进程,但执行 kill -9 也无法终止该进程怎么办?

  • 回答要点:
  • kill -9 无法终止的进程通常处于内核态的 D 状态(Uninterruptible Sleep,不可中断睡眠)。在 K8s 环境中,最常见的原因是 Pod 挂载的远端网络存储(如 NFS、Ceph)发生了网络中断或服务端宕机,导致进程发起的底层 I/O 系统调用一直等待硬件响应而无法返回。
  • 处置方案:此时不能等待进程退出,可使用延迟卸载命令 umount -l <挂载点路径>(Lazy Unmount),直接将该挂载点从宿主机的文件系统命名空间中剥离,让 Kubelet 得以继续完成清理流程;若节点上已有大量进程陷入 D 状态导致负载异常,则需将该 Node 节点执行 cordon 隔离并安排重启恢复。

07 挂载阻塞自动诊断脚本(resolve_terminating_pod.sh)

该脚本用于在工作节点(Node)上执行,传入故障 Pod 的 UID 后,可自动检测容器残留、未卸载的数据卷挂载点,并输出占用该目录的进程 PID。

#!/usr/bin/env bash
# ==============================================================================
# 脚本名称: resolve_terminating_pod.sh
# 功能描述: 自动诊断 Node 节点上导致 Pod 卡在 Terminating 的挂载残留与占用进程 (纯只读)
# 使用方式: 在故障 Pod 所在的宿主机以 root 执行: ./resolve_terminating_pod.sh
# ==============================================================================

set -o pipefail

POD_UID=$1

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
PLAIN='\033[0m'

if [ -z "$POD_UID" ]; then
    echo -e "${RED}用法: $0 ${PLAIN}"
    echo -e "提示: 可通过 kubectl get pod  -o jsonpath='{.metadata.uid}' 获取 UID。"
    exit 1
fi

echo -e "${BLUE}================================================================${PLAIN}"
echo -e "${BLUE}           Pod Terminating 宿主机挂载阻塞自动诊断脚本           ${PLAIN}"
echo -e "${BLUE}================================================================${PLAIN}"
echo -e "${YELLOW}[INFO] 当前追踪的 Pod UID: ${POD_UID}${PLAIN}\n"

# 1. 检查 Kubelet Pod 目录状态
echo -e "${YELLOW}[1/3] 检查宿主机 Kubelet Pod 目录状态...${PLAIN}"
POD_DIR="/var/lib/kubelet/pods/${POD_UID}"

if [ -d "$POD_DIR" ]; then
    echo -e "  -> ${YELLOW}[INFO] 宿主机仍存在 Pod 数据目录: ${POD_DIR}${PLAIN}"
else
    echo -e "  -> ${GREEN}[OK] 宿主机已无该 Pod 目录残留 (/var/lib/kubelet/pods/${POD_UID})。${PLAIN}"
    echo -e "  -> ${GREEN}[结论] 宿主机数据面已清理完毕。若 Pod 仍处于 Terminating,请检查 API Server 中的 metadata.finalizers 字段。${PLAIN}"
    exit 0
fi

# 2. 探测挂载点残留
echo -e "\n${YELLOW}[2/3] 检索操作系统挂载表,排查未卸载的数据卷 (Volume)...${PLAIN}"
# 修复了这里的管道符乱码
MOUNT_RES=$(mount | grep "$POD_UID")

if [ -z "$MOUNT_RES" ]; then
    echo -e "  -> ${GREEN}[OK] 宿主机无该 UID 关联的活跃挂载点残留。${PLAIN}"
else
    echo -e "  -> ${RED}[ALERT] 检测到残留挂载点,Kubelet umount 卸载操作受阻:${PLAIN}"
    echo "$MOUNT_RES" | awk '{print "     " $3}'
fi

# 3. 定位占用句柄的进程
echo -e "\n${YELLOW}[3/3] 扫描文件句柄,定位占用挂载目录的进程...${PLAIN}"
# 修复了这里的管道符乱码
LSOF_RES=$(lsof 2>/dev/null | grep "$POD_UID")

if [ -z "$LSOF_RES" ]; then
    echo -e "  -> ${YELLOW}[WARN] lsof 未检测到常规用户态进程占用。${PLAIN}"
    echo -e "  -> ${YELLOW}[提示] 若上述残留挂载点为 NFS/Ceph 等网络存储,可能是存储断连导致的内核 D 状态阻塞,建议尝试执行 umount -l  进行延迟卸载。${PLAIN}"
else
    echo -e "  -> ${RED}[DANGER] 检测到正在占用挂载目录的进程:${PLAIN}"
    echo -e "     ${BLUE}进程名称     PID      用户     文件描述符${PLAIN}"
    echo "$LSOF_RES" | awk '{printf "     %-12s %-8s %-8s %s\n", $1, $2, $3, $4}' | sort | uniq

    PIDS=$(echo "$LSOF_RES" | awk '{print $2}' | sort -u | tr '\n' ' ')
    echo -e "\n  -> ${GREEN}[处置建议] 请核实上述进程用途,确认可终止后执行以下命令释放文件句柄:${PLAIN}"
    echo -e "     ${YELLOW}kill -9 $PIDS${PLAIN}"
fi

echo -e "\n${BLUE}================================================================${PLAIN}"