摘要 :在 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 跟踪器中由来已久。核心原因通常分为两类:
- 挂载泄漏 / 运行时占用(Host-level) :容器运行时泄露了挂载点,或者有进程在这些挂载点内持有了文件描述符。常见症状包括 kubelet 无法移除 Pod 的 volume 路径;
umount报告 "not mounted" 但/proc/mounts中依然存在记录。 - 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
根本预防与最佳实践
为了减少此类事件的发生,建议采取以下加固措施:
- 保持运行时更新:及时升级 containerd/Docker,包含的 bug 修复能大幅减少挂载泄漏的边缘场景。
- 合理设置优雅退出时间 :准确配置
terminationGracePeriodSeconds,确保应用在 kubelet 强行介入前有充足时间完成退出。 - 监控 OOM 事件 :节点的高负载或内核 OOM 行为容易遗留异常的挂载状态,甚至产生不可杀死的
D-state进程。 - 存储组件健康度:确保 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 的手。