Kubernetes 疑难杂症:Pod 始终处于 Terminating 状态的真实原因与安全修复方案

摘要 :在 Kubernetes 集群运维中,Pod 卡在 Terminating 状态是一个常见的问题。通常情况下,这极有可能是因为容器运行时(如历史版本中的 Docker 或 containerd)在宿主机或其他命名空间中发生了挂载泄漏(mount leak)。这会导致 kubelet 在清理资源时触发 "device or resource busy" 错误,进而使得该 Pod 的 API 对象永远滞留。本文将带你从底层逻辑拆解该问题,并提供安全、彻底的修复步骤。


现象描述

当删除一个包含多个 Pod 的资源对象(如 ReplicationController 或 Deployment)时,大部分 Pod 会被删除,但总有几个顽固的 Pod 停留在 Terminating 状态:

bash 复制代码
NAME        READY   STATUS        RESTARTS   AGE
pod-186o2   1/1     Terminating   0          2h
pod-4b6qc   1/1     Terminating   0          2h
pod-8xl86   1/1     Terminating   0          1h

此时,无论是反复执行 kubectl delete 还是等待,都无济于事。要彻底解决这个问题,我们需要先理解其背后的工作机制。

为什么 Pod 会卡在 "Terminating"?

当 Kubernetes 删除一个 Pod 时,kubelet 会执行一个严谨的关闭流程:发送 SIGTERM 信号 -> 等待 terminationGracePeriodSeconds -> 停止容器 -> 卸载(unmount)卷和网络命名空间 -> 最后从节点和 API Server 中移除该 Pod。

如果卸载(unmounting)过程失败 (通常是因为挂载点处于 "busy" 状态),清理流程就会中断,导致 API 对象陷入 Terminating 的边缘状态。

这种 "device or resource busy" 的错误在 Kubernetes 和 Docker/Moby 的 issue 跟踪器中由来已久。核心原因通常分为两类:

  1. 挂载泄漏 / 运行时占用(Host-level) :容器运行时泄露了挂载点,或者有进程在这些挂载点内持有了文件描述符。常见症状包括 kubelet 无法移除 Pod 的 volume 路径;umount 报告 "not mounted" 但 /proc/mounts 中依然存在记录。
  2. Finalizers 阻塞(API-level):例如 PVC/CSI finalizers 为了确保资源被正确清理,会有意拦截删除操作。如果负责清理的控制器发生故障,对象就会卡住。(警告:在未完全评估后果前,切勿暴力移除 finalizers)。

快速排查决策树

遇到该问题时,按照以下逻辑进行排查,切勿盲目使用 --force:

第一步:检查是否被 Finalizer 阻塞

bash 复制代码
kubectl get pod <POD_NAME> -o jsonpath='{.metadata.finalizers}'
  • 如果输出包含 finalizers:问题出在存储/CSI 的清理流程上。请修复底层的存储依赖,而不是强行删除 Pod。仅在底层资源已手动确认处理完毕后,作为最后手段移除 finalizer。
  • 如果没有 finalizers:高度怀疑是挂载泄漏或运行时持有。进入宿主机级别的深度排查。

节点级深度排查与修复(针对 Docker & containerd)

通过 kubectl describe pod <POD_NAME> 找到 Pod 所在的节点,并通过 SSH 登录到该节点。

1. 定位 Pod 的 UID 和挂载路径

首先获取目标 Pod 的 UID:

bash 复制代码
POD=pod-186o2
NS=default
UID=$(kubectl get pod $POD -n $NS -o jsonpath='{.metadata.uid}')
echo $UID

在节点上检查 kubelet 下的挂载情况:

bash 复制代码
# 查找属于该 UID 的挂载记录
grep -F "$UID" /proc/mounts || true
findmnt | grep "$UID" || true

如果看到类似 /var/lib/kubelet/pods/<UID>/volumes/... 的路径拒绝被卸载,这就是罪魁祸首。

2. 识别占用挂载点的进程

bash 复制代码
# 查找哪个进程打开了挂载点下的文件或目录
lsof +D /var/lib/kubelet/pods/$UID 2>/dev/null | head

如果某个容器运行时或 sidecar 进程仍持有打开的文件,这里会暴露其行踪。

3. 容器运行时清理

根据你集群使用的运行时,执行相应的清理操作。

对于 Docker(传统集群):

bash 复制代码
docker ps --no-trunc | grep $POD
# 如果找到容器,尝试优雅停止:
docker stop <containerID>

# 逃生舱操作(高危:会重启节点上所有容器):
# systemctl restart docker

注:docker rm 时的 "device or resource busy" 是个历史悠久的已知问题(涉及 aufs/overlay2)。重启 Docker daemon 通常能释放泄漏的引用,但在生产节点需极其谨慎。

对于 containerd(现代 Kubernetes 默认):

bash 复制代码
crictl ps | grep $POD
crictl stopp <containerID>
crictl rmp <containerID>

# 逃生舱操作(高危):
# systemctl restart containerd

4. 强制卸载残留路径 (Lazy Umount)

如果运行时已不再持有该路径,但挂载依然存在:

bash 复制代码
umount -l /var/lib/kubelet/pods/$UID/volumes/<driver>/<vol> || true

(注意:仅当确认使用该路径的进程已彻底退出后,才能使用 -l 参数。Lazy umount 会立即断开连接,并在 busy 引用消失后清理系统资源。)


安全删除 Pod(仅在解除占用后)

一旦 kubelet 能够完成节点的清理工作,普通的删除命令即可生效:

bash 复制代码
kubectl delete pod $POD -n $NS

如果普通的 delete 依然无效,且你已经 100% 确认节点上的真实工作负载和挂载均已清理干净,方可使用强制删除来清理 API 对象:

bash 复制代码
kubectl delete pod $POD -n $NS --grace-period=0 --force

根本预防与最佳实践

为了减少此类事件的发生,建议采取以下加固措施:

  1. 保持运行时更新:及时升级 containerd/Docker,包含的 bug 修复能大幅减少挂载泄漏的边缘场景。
  2. 合理设置优雅退出时间 :准确配置 terminationGracePeriodSeconds,确保应用在 kubelet 强行介入前有充足时间完成退出。
  3. 监控 OOM 事件 :节点的高负载或内核 OOM 行为容易遗留异常的挂载状态,甚至产生不可杀死的 D-state 进程。
  4. 存储组件健康度:确保 CSI 驱动和 PVC finalizers 能够正常完成工作,并对卡住的 finalizers 建立告警机制。

运维速查手册

bash 复制代码
# === 1. Docker 环境清理 ===
docker ps --no-trunc | grep <pod-name-or-uid> || true
docker stop <containerID> || true
# sudo systemctl restart docker # 终极手段

# === 2. containerd 环境清理 ===
crictl ps | grep <pod-name-or-uid> || true
crictl stopp <containerID> && crictl rmp <containerID> || true
# sudo systemctl restart containerd # 终极手段

# === 3. Kubernetes API 清理 ===
kubectl delete pod <POD> -n <NS>                # 正常删除
kubectl delete pod <POD> -n <NS> --grace-period=0 --force  # 清理完底层后的强制删除

结语 :下次当你看到 Pod 永远处于 Terminating 时,不要直接敲击 --force 的手。

相关推荐
阿里云云原生18 分钟前
TechTalk丨从能买奶茶到能进企业,Agent 绕不开同一组问题
云原生·agent
逐流人1 小时前
手工建集群到全自动发布:k8s集群部署与DevOps流水线整合实战
ci/cd·云原生·kubernetes·云计算·devops
我不会起名字3222 小时前
K8s Pod 反复重启 CrashLoopBackOff:从 exit code 到根因的 5 步排查
云原生·容器·kubernetes·k8s
瑞克-桑切斯2 小时前
ERP 企业生产管理系统部署手册
运维·docker·云原生·容器·eureka
我不会起名字3223 小时前
K8s 节点磁盘打满导致 Pod 被驱逐:4 类占用与回收顺序
运维·kubernetes·k8s·diskpressure·evicted
孔令飞3 小时前
把 Agent 搬进飞书:飞书 Agent 机器人演示
云原生·go·ai编程
瑞克-桑切斯3 小时前
docker mail商城
运维·docker·容器
QXWZ_IA4 小时前
定位数据怎么推给 IoT 平台?消息协议选型与接入准备
网络·人工智能·物联网·安全
java_logo4 小时前
Docker 部署 ReMe:轻松搭建 AI Agent 记忆管理平台
人工智能·docker·容器·ai agent·轩辕镜像·reme·agentscope-ai
不庸.4 小时前
WPScan 实战入门:从 Docker 靶场到弱密码爆破
运维·安全·web安全·docker·容器·安全性测试·wpscan