<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Terminating on Zero Day Notes</title>
        <link>https://blog.5772447.xyz/tags/terminating/</link>
        <description>Recent content in Terminating on Zero Day Notes</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>zh</language>
        <lastBuildDate>Tue, 21 Jul 2026 07:00:00 +0800</lastBuildDate><atom:link href="https://blog.5772447.xyz/tags/terminating/index.xml" rel="self" type="application/rss+xml" /><item>
            <title>一次 Kubernetes Pod 长期卡在 Terminating 状态的排查记录</title>
            <link>https://blog.5772447.xyz/posts/97876a5b/</link>
            <pubDate>Tue, 21 Jul 2026 07:00:00 +0800</pubDate>
            <guid>https://blog.5772447.xyz/posts/97876a5b/</guid>
            <description>&lt;h2 id=&#34;一问题背景&#34;&gt;&lt;a href=&#34;#%e4%b8%80%e9%97%ae%e9%a2%98%e8%83%8c%e6%99%af&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;一、问题背景&#xD;&#xA;&lt;/h2&gt;&lt;p&gt;交易系统的核心 API 服务以 Deployment 形式跑在 Kubernetes 集群上，平时靠滚动更新（rolling update）发版，预期旧 Pod 在 30 秒优雅退出窗口内 shutdown、新 Pod 接管流量。某次例行发布（v2.3.1）触发滚动更新后，监控却开始报警：部分接口 5xx 比例陡增、响应超时。我们登陆集群一看，发现旧版本的 Pod 并没有按预期被清掉，而是大量堆积在 &lt;code&gt;Terminating&lt;/code&gt; 状态，且 &lt;code&gt;AGE&lt;/code&gt; 已经显示几十分钟，远超正常的 30 秒。节点资源被这些&amp;quot;死而不僵&amp;quot;的 Pod 占着，新 Pod 调度拥挤、部分直接 Pending，发布流水线也卡在&amp;quot;等待旧副本退出&amp;quot;。&lt;/p&gt;&#xA;&lt;h2 id=&#34;二故障现象&#34;&gt;&lt;a href=&#34;#%e4%ba%8c%e6%95%85%e9%9a%9c%e7%8e%b0%e8%b1%a1&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;二、故障现象&#xD;&#xA;&lt;/h2&gt;&lt;p&gt;现场表现非常反直觉：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;kubectl get pods&lt;/code&gt; 显示约 20 个旧 Pod 状态为 &lt;code&gt;Terminating&lt;/code&gt;，&lt;code&gt;AGE&lt;/code&gt; 高达 40 分钟以上，普通 &lt;code&gt;kubectl delete pod xxx&lt;/code&gt; 没有任何反应。&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;kubectl describe pod&lt;/code&gt; 能看到 &lt;code&gt;deletionTimestamp&lt;/code&gt; 已经设置，但 Events 最后一条基本是空或者停留在 &amp;ldquo;Killing&amp;rdquo; 之前，说明 APIServer 已经标记删除，Pod 对象却迟迟没被真正移除。&lt;/li&gt;&#xA;&lt;li&gt;登上对应节点用 &lt;code&gt;crictl ps&lt;/code&gt; / &lt;code&gt;docker ps&lt;/code&gt; 查看，这些&amp;quot;Terminating&amp;quot;的容器进程其实还活着，端口也仍在监听——它们是&amp;quot;僵尸&amp;quot;，不是&amp;quot;已退出&amp;quot;。&lt;/li&gt;&#xA;&lt;li&gt;新 Pod 大量 &lt;code&gt;Pending&lt;/code&gt;：调度器认为节点资源已被占用（Terminating 的 Pod 依然计入 &lt;code&gt;requests&lt;/code&gt;），有油却加不进新车。&lt;/li&gt;&#xA;&lt;li&gt;线上出现诡异 5xx：这些旧容器进程明明还活着、端口还监听，但流量已经进不来——因为 Endpoint 控制器早把它们的 IP 从 Service 的 Endpoints 里摘掉了。于是造成&amp;quot;进程活着、服务已下线&amp;quot;的真空窗口，请求被导向尚未就绪的新 Pod 或干脆无可用后端。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;三排查过程&#34;&gt;&lt;a href=&#34;#%e4%b8%89%e6%8e%92%e6%9f%a5%e8%bf%87%e7%a8%8b&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;三、排查过程&#xD;&#xA;&lt;/h2&gt;&lt;p&gt;第一步，先确认 Pod 是不是真的进了删除态。&lt;code&gt;kubectl get pod &amp;lt;p&amp;gt; -o jsonpath=&#39;{.metadata.deletionTimestamp}&#39;&lt;/code&gt; 有值、&lt;code&gt;{.metadata.deletionGracePeriodSeconds}&lt;/code&gt; 也有值，说明对象确实处于&amp;quot;标记删除但等待完成&amp;quot;阶段——&lt;code&gt;Terminating&lt;/code&gt; 不等于&amp;quot;正在删&amp;quot;，而是&amp;quot;删不动&amp;quot;。&lt;/p&gt;&#xA;&lt;p&gt;第二步，区分两类卡死：节点失联型 vs 节点健康型。先 &lt;code&gt;kubectl get node&lt;/code&gt; 确认节点都是 &lt;code&gt;Ready&lt;/code&gt;，节点上的 kubelet 正常工作，排除&amp;quot;kubelet 失联、apiserver 标记删除却没人在节点侧执行&amp;quot;的情况。既然节点健康，问题一定出在&amp;quot;节点侧删不掉&amp;quot;或&amp;quot;对象删除被门闩卡住&amp;quot;。&lt;/p&gt;&#xA;&lt;p&gt;第三步，查 finalizer。&lt;code&gt;kubectl get pod &amp;lt;p&amp;gt; -o jsonpath=&#39;{.metadata.finalizers}&#39;&lt;/code&gt; 暴露了端倪：这批 Pod 上挂着团队自研控制器写入的自定义 finalizer（如 &lt;code&gt;example.com/cleanup&lt;/code&gt;）。该 finalizer 约定由控制器在清理完外部资源后摘除，但当时控制器因上游依赖故障未能完成 reconcile，finalizer 永不清除——而 K8s 的删除语义规定：&lt;strong&gt;只要还有 finalizer 未清空，对象就永远停在 Terminating&lt;/strong&gt;。这正是&amp;quot;删不动&amp;quot;的第一类根因。&lt;/p&gt;&#xA;&lt;p&gt;第四步，查 preStop。即便没有 finalizer，仍有大量 Pod 卡着，于是对照 Pod spec 发现 &lt;code&gt;lifecycle.preStop&lt;/code&gt; 里 exec 了一段脚本，脚本用 &lt;code&gt;sleep 60&lt;/code&gt; 来&amp;quot;等待上游连接优雅排空&amp;quot;。但 Pod 的 &lt;code&gt;terminationGracePeriodSeconds&lt;/code&gt; 只有 30 秒。这里有个常被忽略的时序：K8s 删除一个 Pod 时，先发 SIGTERM，然后&lt;strong&gt;执行 preStop，且 preStop 的耗时算在 grace period 之内&lt;/strong&gt;，preStop 完成后才发 SIGKILL。也就是说，30 秒的窗口要先被 preStop 的 60 秒 sleep 吃掉，preStop 永远跑不完，SIGKILL 永远不会发出，容器进程就这么挂着——这是&amp;quot;删不动&amp;quot;的第二类根因，也是现场最致命的一条。&lt;/p&gt;&#xA;&lt;p&gt;第五步，交叉验证流量真空。&lt;code&gt;kubectl get endpoints &amp;lt;svc&amp;gt;&lt;/code&gt; 确认旧 Pod IP 已从 Endpoints 摘掉，解释了为何进程活着却 5xx：它们已被 Service 层逻辑&amp;quot;除名&amp;quot;，新请求不会再来，而新 Pod 又因为节点资源被僵尸占着起不来，形成发布中断的死锁。&lt;/p&gt;&#xA;&lt;h2 id=&#34;四解决方案&#34;&gt;&lt;a href=&#34;#%e5%9b%9b%e8%a7%a3%e5%86%b3%e6%96%b9%e6%a1%88&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;四、解决方案&#xD;&#xA;&lt;/h2&gt;&lt;p&gt;止血不能无脑 &lt;code&gt;kubectl delete --force --grace-period=0&lt;/code&gt;。强制删除会绕过优雅退出与 finalizer 流程，对有状态卷/有外部资源绑定的 Pod 可能留下孤儿资源，且节点侧容器可能清理不干净。正确做法是分情况处理：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;finalizer 残留、控制器已无法恢复&lt;/strong&gt;：安全摘除 finalizer，让对象立即进入&amp;quot;可被真正删除&amp;quot;状态，kubelet 随即执行容器停止：&#xA;&lt;code&gt;kubectl patch pod &amp;lt;p&amp;gt; -p &#39;{&amp;quot;metadata&amp;quot;:{&amp;quot;finalizers&amp;quot;:[]}}&#39; --type=merge&lt;/code&gt;&#xA;对多个 Pod 可批量 &lt;code&gt;kubectl get pods --field-selector=status.phase=Running | grep Terminating&lt;/code&gt; 后循环 patch。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;preStop 死等&lt;/strong&gt;：对已处于删除态的 Pod 无法再改 grace period。稳妥做法是先同样 patch 摘除 finalizer 跳过 preStop 阶段强制退出，同时修复镜像里的 preStop 脚本（去掉长 sleep、改为只做信号转发/快速关闭监听），再重新滚动发版。&lt;/li&gt;&#xA;&lt;li&gt;清理完僵尸后，确认 &lt;code&gt;kubectl get pods&lt;/code&gt; 无残留 Terminating，新 Pod 正常 &lt;code&gt;Running&lt;/code&gt; 并重新加入 Endpoints，5xx 与 Pending 随即消失。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;五根因分析&#34;&gt;&lt;a href=&#34;#%e4%ba%94%e6%a0%b9%e5%9b%a0%e5%88%86%e6%9e%90&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;五、根因分析&#xD;&#xA;&lt;/h2&gt;&lt;p&gt;本质是对 Pod 删除语义的理解偏差：&lt;strong&gt;设置了 &lt;code&gt;deletionTimestamp&lt;/code&gt; 不等于立即删除，也不等于 30 秒后一定删除&lt;/strong&gt;。一个 Pod 真正从集群消失，必须同时满足两个条件——（1）所有 &lt;code&gt;finalizers&lt;/code&gt; 被清空；（2）节点侧 kubelet 停掉全部容器，而停止容器又要求 preStop 在 &lt;code&gt;terminationGracePeriodSeconds&lt;/code&gt; 内完成、之后 SIGKILL 兜底。这两道&amp;quot;门闩&amp;quot;任一卡住，对象就永久停在 Terminating。其中 preStop 耗时计入 grace period 这一细节最易被忽视：很多人以为 SIGTERM 之后还有整整 30 秒，实际上这 30 秒要先被 preStop 占用。&lt;/p&gt;&#xA;&lt;h2 id=&#34;六预防措施&#34;&gt;&lt;a href=&#34;#%e5%85%ad%e9%a2%84%e9%98%b2%e6%8e%aa%e6%96%bd&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;六、预防措施&#xD;&#xA;&lt;/h2&gt;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;preStop 只做轻量操作&lt;/strong&gt;：转发 SIGTERM、关闭监听端口、flush 日志，绝不 &lt;code&gt;sleep&lt;/code&gt; 或等待外部依赖。如确有等待诉求，时长必须明显小于 &lt;code&gt;terminationGracePeriodSeconds&lt;/code&gt;，并预留 SIGKILL 缓冲（建议 grace ≥ preStop 预估耗时 + 5s）。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;finalizer 必须有控制器兜底&lt;/strong&gt;：reconcile 逻辑要保证在异常/退出路径上也能清理 finalizer；给控制器加存活探针、健康监控与崩溃告警，避免&amp;quot;写 finalizer 的控制器自己先挂了&amp;quot;。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;合理设置优雅退出窗口&lt;/strong&gt;：默认 30s 不够长关闭的业务适当上调，但别无脑拉满——窗口越长，卡死时僵尸存活越久。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;补齐监控&lt;/strong&gt;：对持续 &lt;code&gt;Terminating&lt;/code&gt; 超过 1 分钟的 Pod 告警；节点 NotReady 告警；控制器 Pod 状态告警；发布流水线在滚动后检查 Terminating 残留再判定成功。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;发布规范&lt;/strong&gt;：滚动更新显式配置 &lt;code&gt;maxUnavailable&lt;/code&gt; / &lt;code&gt;maxSurge&lt;/code&gt;，避免一次性驱逐过多副本放大影响面。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;七总结&#34;&gt;&lt;a href=&#34;#%e4%b8%83%e6%80%bb%e7%bb%93&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;七、总结&#xD;&#xA;&lt;/h2&gt;&lt;p&gt;&lt;code&gt;Terminating&lt;/code&gt; 不是&amp;quot;正在删除&amp;quot;，而是&amp;quot;删除被卡住&amp;quot;。定位时先看两点：一是 finalizer 是否残留、控制器是否还活着；二是节点是否健康、preStop 是否超时吞噬了优雅窗口。止血优先用 patch 安全摘除 finalizer 让僵尸退场，根治则落在镜像里的 preStop 设计（轻量、不等待）与控制器对 finalizer 的兜底清理上。把删除语义吃透，发布才不会在&amp;quot;旧副本退不掉、新副本起不来&amp;quot;的死锁里翻车。&lt;/p&gt;&#xA;</description>
        </item></channel>
</rss>
