ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Kubernetes Pod长时间Terminating:原因排查与安全清理实战

Kubernetes Pod长时间Terminating:原因排查与安全清理实战 1. 故障现象一个“删不掉”的 Pod 究竟长什么样先还原一下现场。某天你执行kubectl delete pod xxx -n yyy等了十几秒Pod 还在再等一分钟依然还在。执行kubectl get pod一看NAME 那一列后面明晃晃挂着TerminatingAGE 已经过去好几分钟甚至更久。有的还会在 STATUS 下面多一行类似Terminating (for 3m)的提示——这是在告诉你这个状态已经持续了整整三分钟。如果再配合kubectl describe pod看一眼大概率会看到 Events 区域空空如也既没有 Warning 也没有新的 Normal 事件就像 Kubernetes 把这个 Pod 彻底遗忘了。这时候你心里大概会咯噔一下不是资源不足不是镜像拉取失败而是删除请求发出去之后Pod 卡在了“即将退出”的中间态。我得先给一个结论性的判断Pod 长时间停留在 Terminating绝大多数情况下不是 API Server 的问题而是下层组件kubelet、容器运行时、存储插件没能完成清理工作或者有组件在“假装完成”之后留下了遗留物。你按三次 CtrlC 也没用重启 kubectl 也没用因为这个状态不是命令行工具卡住而是集群内部状态机卡住了。顺便说一句检索这个问题的时候很多人会混入一些数据库日志片段比如opengauss# \l warning: session unused timeout. fatal: terminating connection——那是数据库侧的“终止连接”和 Kubernetes 的Terminating完全是两码事。前者是会话超时被数据库杀掉后者是一个完整的 Pod 删除流程卡壳。排查的时候先分清对象别把两条技术线的“Terminating”混在一起看。对于刚接触 Kubernetes 不久的同学这个状态可能只是“有点慌”但对于生产环境负责人来说这意味着节点资源被僵尸 Pod 占用、命名空间无法正常销毁、甚至 StatefulSet 的滚动更新彻底卡死。所以这篇文章我把这个问题的成因拆开讲透并且给你一套有序、安全的修复路径而不是上来就--force --grace-period0一把梭——因为强制删除在很多场景下本身就是事故的起点。2. 删除一个 Pod 的正常流程以及“卡住”的本质是什么2.1 kubelet 的删除流程优雅终止是一场“给足面子”的谈判要理解 Pod 为什么卡在 Terminating得先知道 Kubernetes 删 Pod 的正常步骤。这里我不讲源码逐行但你可以先记住一个半分钟就能讲清楚的主干流程后续排查都是围绕这条主干展开的。当你执行kubectl delete pod时API Server 会把 Pod 对象标记为删除中并在 etcd 里写入一个删除时间戳deletionTimestamp。之后 Pod 的状态变成 Terminating但对象真正从 etcd 消失要等 kubelet 汇报“清理完毕”。kubelet 收到这个 Pod 需要终止的通知后会执行一个经典的双阶段流程阶段一并行执行 preStop 钩子如果有的话同时把 Pod 从 Service 的 Endpoints/Slice 中摘除让新流量不再进来。阶段二向 Pod 内容器的主进程发送 SIGTERM然后开始倒计时等待。默认宽限期是 30 秒terminationGracePeriodSeconds你可以通过 YAML 调整这个值。若 30 秒内容器进程没有主动退出kubelet 会再发送 SIGKILL 强杀。阶段三容器进程都退出之后kubelet 清理容器运行时层面的资源容器、网络命名空间、临时存储卷然后调用 API Server 移除 Pod 的 Finalizers最终 Pod 对象从 etcd 消失。这个过程在正常情况下几秒到三十几秒内完成用户不会有明显感知。凡是超过几分钟、十几分钟甚至更久还赖在 Terminating 里的本质都是阶段二或阶段三出了岔子要么容器进程真的杀不掉要么容器已经没了但资源清理卡住要么 Finalizer 挂在那里没有组件帮它摘除。2.2 卡住的本质删除状态机的“一步没走完”我把这个事说得更直白一点。Kubernetes 里几乎所有资源的删除都不是“一删了之”而是“标记删除 等待各环节确认 最终抹除对象”的异步流程。Pod 的删除进度由多个角色共同推进任何一个环节没有回执整个流程就停在原地。最常见的一个误区是用户以为kubectl delete pod是向 kubelet 下达“立即杀掉容器”的指令。其实它只是把期望状态改成“这个 Pod 该没了”真正的执行者是节点上的 kubelet而 kubelet 又要把容器级操作委托给 containerd 或 CRI-O 这类容器运行时。链路越长中间可以卡壳的地方就越多。打个比方删除 Pod 就好比你叫了一个搬家团队来腾退房子。房东API Server已经贴了告示说这房子要退租deletionTimestamp但真正要等的东西是屋里的人容器进程愿意走、搬家师傅kubelet/容器运行时确认所有家具卷、网络、挂载点都搬完、物业Finalizer 控制器盖章。只要有一个角色说“我这边还没完”房子就一直挂着“腾退中”Terminating的招牌。这个本质理解清楚之后接下来看具体原因就有章法了无非是“屋里的人赖着不走”“搬家师傅找不到东西”“物业不盖章”三种情况再加上一个容易被人忽略的“房东自己坏了”API Server 与 kubelet 失联导致状态根本没传到位。3. 真实原因拆解我在生产环境里逐个踩过的坑3.1 Finalizer最容易被忽略的“物业不盖章”先把 Finalizer 提上来讲因为这是我排查时最容易定位、也最常被初学者跳过的一环。Finalizer 是挂在资源对象元数据上的一组字符串标识语义是“删除前必须执行某些清理动作”。比如kubernetes.io/pv-protection是让 PV 在被 Pod 使用期间不能被误删controller.cattle.io/xxx这类是 Rancher/自定义控制器加的清理钩子。只要metadata.finalizers列表里有内容API Server 就会在资源被标记删除后继续保留对象直到所有 Finalizer 被移除。问题出在两类场景场景一某个自定义控制器早已被卸载但 Finalizer 字符串还留着删除 Pod 时没有任何组件来处理这个 Finalizer于是 Pod 永远停在 Terminating。场景二控制器逻辑存在 Bug 或权限不足处理 Finalizer 时反复报错一直没把 Finalizer 从对象上摘掉。排查方法非常直接先看 Pod 的完整 JSONkubectl get pod pod-name -n namespace -o json | jq .metadata.finalizers如果输出非空比如[kubernetes.io/pvc-protection]、[example.com/my-finalizer]那基本就是 Finalizer 卡住的典型特征。如果你用的是较新版本的 kubectl还可以在describe输出里看到类似Finalizers: [kubernetes.io/pvc-protection]的一行一眼就能扫到。处理方式我会放到“安全修复方案”那一节里给完整步骤这里只是提醒你动手改 Finalizer 之前先想清楚这个 Finalizer 是谁加的、删掉它是否安全。无脑清空是典型的“一时爽”操作可能造成 PV 数据被连带删除、配置漂移等次生灾害。3.2 挂载点泄漏与卷清理失败kubelet 的“家务活”没干完如果把 Finalizer 比作“物业盖章”那卷清理就是“搬家师傅清点家具”。kubelet 在删除 Pod 前必须确认这个 Pod 用到的所有 volume 都被卸载干净。但 Pod 里的容器如果已经处于一种半死状态、或者 kubelet 曾因节点负载过高而没能正常完成卸载就会留下一个僵持住的挂载点。最常见的手工验证命令是mount | grep pod-uid或者检查/var/lib/kubelet/pods/uid/volumes目录是否还残留内容。如果你发现对应的 volume 挂载点还存在且umount时报target is busy那就说明有进程还占着这个路径。这种现象常出现在以下几种情况Pod 内容器把某个目录设为挂载源并且有子进程持续访问使用 NFS 这类网络存储时服务端连接没有及时断掉节点上某个容器运行时的 mount namespace 泄漏导致 umount 时找不到正确的命名空间。这种问题隐蔽性高单纯用kubectl delete重试多少遍都没用因为 kubelet 每次都在尝试 clean up 同一批挂载点每次都被“busy”挡回去。真正的解法是定位占用挂载点的进程并妥善处理细节我会在后面的实操章节展开。3.3 容器运行时失联containerd 不回应kubelet 干着急这一条在 containerd 和 CRI-O 环境里尤其常见。kubelet 通过 CRIContainer Runtime Interface接口调用容器运行时来删除容器但容器运行时的 gRPC 服务可能因为以下原因就是不回应containerd 自身的 goroutine 泄漏或 IO 阻塞导致 CRI 插件对删除请求超时节点磁盘满/var/lib/containerd所在分区 100%containerd 的 metadata 写入失败删除操作无法提交containerd 与 kubelet 之间的 Unix Socket 异常连接反复断开。遇到这类问题时kubectl describe pod往往能看到 kubelet 反复输出类似Failed to delete pod ... rpc error的事件。而节点上的journalctl -u kubelet或journalctl -u containerd日志里会有更具体的报错信息。我印象很深的一次故障是某节点上 containerd 因为磁盘被镜像日志塞满而完全卡死节点上所有 Terminating Pod 都纹丝不动。当时用ctr命令直接去列容器列表也超时最后是清理磁盘空间、重启 containerd 才恢复的。所以碰到批量 Pod 都卡在 Terminating 时优先怀疑容器运行时健康状态而不是一个个去折腾某个 Pod 的 Finalizer。3.4 节点 NotReady 与 kubelet 失联API Server 单方面“宣判”还有一种很常见的“假卡死”节点本身已经 NotReadykubelet 失联API Server 无法收到删除完成的回执Pod 对象就一直停在 Terminating。这种情况下你去看那个节点上的 kubelet 进程大概率是压根起不来或者网络层出了问题。排查第一步永远是确认节点状态kubectl get node如果节点是NotReady那先解决节点失联问题再回来处理 Pod。有些人在节点 NotReady 的当下就急着清 Pod结果 Pod 被强制删除了但节点恢复后 kubelet 一比对该 Pod 的 uid 发现对不上又得费劲清理孤儿容器。这里有个必须养成的习惯任何删除操作都要先确认节点的 kubelet 是否在线而不是在失联状态下硬删。3.5 preStop 钩子无限等待一个自己埋的雷preStop 钩子是我要单独拿出来说的另一个常见原因。很多团队会给业务容器配置 preStop 脚本用来做优雅下线、通知注册中心摘流量、等待存量请求处理完毕。脚本本身没有超时上限完全依赖terminationGracePeriodSeconds来兜底。如果脚本里执行的是一个长期等待的命令或者脚本依赖的服务已经不可达preStop 就可能一直卡住。默认 30 秒宽限期耗尽后kubelet 会强制 SIGKILL但假如你把terminationGracePeriodSeconds设置成了 300 秒甚至更多那这个 Pod 就会在那个荒诞的“优雅等待”里耗上五分钟。更阴间的是有些 preStop 脚本内部还嵌套了 sleep比如 sleep 60而宽限期只有 30 秒。这种配置会造成一种奇怪的现象kubelet 在 30 秒后发 SIGKILL容器被杀掉但整个删除流程依然可能因为 CRI 层的清理任务排队而继续挂着。排查方法也简单kubectl describe pod里看Containers部分的State如果显示Terminated且 Reason 是Error或Completed但有StartedAt时间晚于你执行 delete 的时间说明 preStop 确实占用了宽限期。结合 YAML 里的terminationGracePeriodSeconds就能还原整个节奏。实践经验是preStop 脚本里凡是需要调外部系统的地方一律套上timeout和重试上限别把优雅下线的命运寄托在外部服务的响应速度上。4. 安全修复方案分步操作不把“强制删除”放在第一位4.1 先给 Pod 写“病历”诊断清单三件套在动手修复之前强烈建议先花三分钟做一次标准诊断避免修错方向。我每次处理 Terminating 问题都固定跑三个命令完整记下输出后再决定下一步。# 1. 看整体状态和事件 kubectl describe pod pod-name -n namespace # 2. 看完整 JSON重点看 metadata.finalizers、spec.terminationGracePeriodSeconds、status.conditions kubectl get pod pod-name -n namespace -o yaml # 3. 看节点与容器运行时状态 kubectl get node node-name -o wide journalctl -u kubelet --since 10m journalctl -u containerd --since 10m这三组命令的输出能覆盖九成以上卡死原因。诊断顺序有个小技巧先看节点状态再看 kubelet 日志最后才看 Pod 本身。原因很简单——如果 kubelet 整体失联或 containerd 已经卡死你在 Pod 层面分析得再细致也是浪费时间先把底层服务救活再说。4.2 温和处理重发删除请求与调整宽限期如果诊断下来确认 kubelet 在线、容器运行时正常、Finalizer 为空那大概率是删除请求在某个环节“丢失”了。遇到过几次这种情况Pod 对象在 etcd 里的状态正常但 kubelet 那边的删除队列没收到通知。温和处理的第一步是我经常用的“重新触发删除”把 terminationGracePeriodSeconds 临时改成 0 之后再改回去或者直接重新执行一次删除。即便 Pod 已经 Terminating你再次执行kubectl delete也不是无效操作它会让 API Server 重新推送一次删除事件给 kubelet。# 重新触发删除 kubectl delete pod pod-name -n namespace # 若仍无效用 JSON patch 修改宽限期触发 reconcile kubectl patch pod pod-name -n namespace -p {spec:{terminationGracePeriodSeconds:0}} --typemerge这里要解释一下为什么改成 0 是个“温和操作”它只是把优雅退出的等待时间归零意味 kubelet 可以立刻发送 SIGKILL。这不会绕过 Finalizer、不会跳过资源清理只是把“等待容器自己退出”的时间缩短。很多情况下容器进程其实早就没了只是 kubelet 的终止流程因为宽限期过长或 preStop 卡住而迟迟不进入下一步改成 0 就能让流程走完。我自己处理时通常会先试这一招而不是一上来就删 Finalizer 或强制删除因为它的副作用最小。4.3 清理 Finalizer确认来源后精确摘除如果metadata.finalizers非空就要分情况处理。前面讲过Finalizer 是由某个控制器写入的删除前钩子因此正确的删除路径是“让对应的控制器先完成清理”。但对于自定义控制器已经不存在的情况你只能手动摘除。我的安全操作顺序是先确认 Finalizer 来源。查看 Pod 的ownerReferences看它归属的控制器是什么再看metadata.annotations或所属工作负载类型判断这个 Finalizer 大概率是谁加的。确认摘除后不会引发数据安全问题。比如kubernetes.io/pvc-protection这种保护性 Finalizer如果你确认 PVC 已经不需要了摘除后 Pod 删除不会额外触发 PVC 删除。用 patch 命令精确移除而不是清空整个列表。如果列表里有多个 Finalizer我建议只去掉导致卡死的那一个。正常情况下 Pod 删除时只会卡在自定义 Finalizer 上Kubernetes 自带的几个保护性 Finalizer 一般不会长期占位。# 只保留数据面保护性 Finalizer移除自定义导致的阻塞项 # 示例假设 finalizers 只有 [custom.example.com/cleanup] 一项 kubectl patch pod pod-name -n namespace -p {metadata:{finalizers:[]}} --typemerge这句命令会把 Finalizers 清空API Server 在下一轮 reconcile 时发现 Finalizers 已空就直接把 Pod 对象从 etcd 抹掉。注意我需要强调一下清空 Finalizers 之前确认这个 Pod 已没有任何业务流量且底层容器已经退出。如果容器还活着清掉 Finalizer 只会让 Pod 条目消失但容器还在节点上运行形成真正的“孤儿容器”运维上更麻烦。4.4 强制删除最后手段以及它的三条限制如果以上都试过仍无效再考虑--force --grace-period0。但先用三句话说明它的代价它绕过了 kubelet 的确认流程直接从 API Server 层抹掉 Pod 对象kubelet 可能在之后才发现“这个 Pod 已经不存在了”它不会等待 Finalizer 清理你把对象抹掉的同时Finalizer 对应的清理逻辑可能根本没执行它可能留下孤儿容器、卷锁、IP 地址泄漏等后续问题尤其在 containerd 和网络插件侧。强制删除命令kubectl delete pod pod-name -n namespace --force --grace-period0这个操作本质上等同于“先斩后奏”API Server 直接把 Pod 对象删掉然后 kubelet 在后续某次 reconcile 时发现节点上还有这个 uid 对应的容器才会补做清理。如果 kubelet 正常在线它会在几十秒内把残留容器清掉如果节点本身已经失联则会残留到节点恢复为止。强制删除之后的验证也很重要。不要看到kubectl get pod里没有这个 Pod 就以为完事了至少还要检查# 在对应节点上看有没有残留容器 crictl ps -a | grep pod-uid # 检查是否有残留挂载点 grep pod-uid /proc/mounts # 检查网络插件的 IP 是否还在 ip addr show | grep pod-ip如果强制删除后这些残留清不掉你就得进入下一层直接到节点上操作容器运行时和挂载点。这也是为什么我一直强调不要一上来就 force delete——它可能在几分钟内解决“看起来的问题”但把真正的问题推迟到节点侧。4.5 节点侧善后容器残留与挂载点清理当强制删除后仍发现残留容器或挂载点时需要在节点上做善后。先列出一套可执行的步骤每一步都有验证动作避免越清越乱。第一步确认容器实例crictl ps -a | grep pod-name如果看到容器处于Exited状态但仍旧存在直接删除crictl rm container-id如果容器显示为Running先确认该容器确实是目标 Pod 遗留的比对 labels 和 Pod UID确认无误再crictl stop后crictl rm。第二步处理挂载点。先用kubelet的 Pod 目录确认 UIDls /var/lib/kubelet/pods/ | grep pod-uid找到残留的挂载点后检查占用进程fuser -v mount-point若有进程占用先在业务层面确认能否终止该进程再 kill 掉后执行umount mount-point如果umount依然报target is busy可以尝试umount -l做 lazy 卸载。这个操作会让挂载点立即脱离命名空间但实际 IO 锁要到最后一个打开文件的进程退出后才释放。Kubernetes 场景下 lazy umount 是可以接受的因为 Pod 已经删除业务侧的读写本就应该终止。第三步清掉空的 Pod 目录rm -rf /var/lib/kubelet/pods/pod-uid这三步走完节点层面才算真正恢复干净。整套流程我不建议做成自动化脚本因为每一步都涉及“确认归属”的判断一个误删可能扫掉别的 Pod 的数据。5. 常见问题与排查技巧实录5.1 问题速查表一看就定位方向现象特征优先怀疑方向第一排查命令单个 Pod 卡 Terminating节点正常Finalizer、preStop、宽限期过长kubectl get pod -o yaml查 finalizers 和 terminationGracePeriodSeconds同节点多个 Pod 同时卡 Terminating容器运行时失联、挂载点泄漏、节点负载异常journalctl -u containerd -u kubelet --since 10m节点本身 NotReadykubelet 失联或底层网络问题kubectl get nodesystemctl status kubelet删除后 Pod 消失但节点上容器还在强制删除后的孤儿容器crictl ps -aStatefulSet 滚动更新卡住Pod 反复重建PVC 保护 Finalizer、挂载点未释放查 PVC 状态 节点 mount 信息表格之外还有一个很实用的技巧定期巡检 Terminating 时长。用一条命令就能把集群里所有超过五分钟仍处于 Terminating 的 Pod 捞出来kubectl get pods -A | awk $3Terminating {print $1, $2, $4}结合kubectl get pod -A -o json里的deletionTimestamp字段可以算出每个 Pod 已经卡了多久。建议在监控告警里加上“Terminating 超 5 分钟”的规则这个指标比 CPU 告警可靠得多因为它直接暴露了清理流程的故障。5.2 几个反直觉的实操心得第一个心得在容器运行时卡死时重启 containerd 往往比处理单个 Pod 更有效。这种场景下所有删除请求都在超时重试队列里排队你一个个去 pod 级别折腾只是隔靴搔痒。我经历过的几次批量 Terminating 最终都是用 systemctl restart containerd 解决的代价是节点上的存量容器连接会中断非必要不推荐但必要时刻它就是快刀斩乱麻。第二个心得Taint 配合驱逐比 force delete 更安全。如果节点上有一批 Pod 需要清空不要急着挨个强删先kubectl cordon节点再 evacuate 或等待控制器把工作负载调度到其他节点。对于无状态服务控制器会在目标节点恢复或替换 Pod对于有状态服务则要配合底层存储的可用性仔细评估。强制删除适合单个突发场景不适合批量手术。第三个心得看日志要按时间线对齐。排查这类故障时最忌讳的是只盯着 kubelet 日志看最后几行。正确的做法是以deletionTimestamp为起点把 kubelet 日志和 containerd 日志同时往回翻找那个时间点前后的 error、timeout、failed 关键词。很多卡死不是发生在删除的那一刻而是发生在删除之前的一次容器重启或卷挂载失败中——前者只是导火索后者才是遗留问题的根源。用journalctl --since指定对应时间段把两条日志流并排看往往几眼就能锁定真凶。6. 收尾前再分享一个压箱底的习惯我在折腾了无数次 Terminating 故障之后养成了一个看似多余但救过我好几次的习惯在删除任何 Pod 之前先记录它的 UID 和所属节点。一行命令的事kubectl get pod pod-name -n namespace -o jsonpath{.metadata.uid} {\n}{.spec.nodeName} {\n}这个习惯的回报出现在问题最复杂的那类场景里当你已经强制删除了 Pod后来又在某个节点上发现一个不明容器时你能快速比对这个容器的 label 或 annotation 里的 UID 是不是同一个。有 UID 在手你就能做出“删/不删”的决定而不是对着一个陌生容器猜半天。最后想说的是Pod 长期 Terminating 这个问题本质上是 Kubernetes 分布式系统“最终一致”特性在故障边缘的一次显形。它不可怕但也没那么可爱。只要你在操作前多确认一步状态、操作后多验证一步残留踩坑的概率就能压到很低。希望这套从诊断到安全清理的流程能让你下次遇到它时少熬夜。
返回列表