<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>维护窗口 on Zero Day Notes</title>
        <link>https://blog.5772447.xyz/tags/%E7%BB%B4%E6%8A%A4%E7%AA%97%E5%8F%A3/</link>
        <description>Recent content in 维护窗口 on Zero Day Notes</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>zh</language>
        <lastBuildDate>Mon, 12 Oct 2026 09:00:00 +0800</lastBuildDate><atom:link href="https://blog.5772447.xyz/tags/%E7%BB%B4%E6%8A%A4%E7%AA%97%E5%8F%A3/index.xml" rel="self" type="application/rss+xml" /><item>
            <title>PDB 配成 100%：集群维护卡在节点排空</title>
            <link>https://blog.5772447.xyz/posts/8f3aa74b/</link>
            <pubDate>Mon, 12 Oct 2026 09:00:00 +0800</pubDate>
            <guid>https://blog.5772447.xyz/posts/8f3aa74b/</guid>
            <description>&lt;h2 id=&#34;问题背景&#34;&gt;&lt;a href=&#34;#%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;公司生产 K8s 集群每季度有一次维护窗口：凌晨逐节点 cordon + drain,排空后升级内核和 kubelet,再 uncordon 放回。整个流程演练过多次，前几个节点一向顺畅，所以维护夜的值班安排很简单——一个人盯着 drain 命令往下滚，出问题再叫人。&lt;/p&gt;&#xA;&lt;p&gt;这次维护共 6 个节点，前两个节点十几分钟排完。第三个节点（worker-03）的 drain 跑起来之后，屏幕上的输出停在一行报错上反复刷新，十几分钟没有推进。窗口总共 4 小时，已经用掉近 1 小时，如果这个节点排不掉，后面 3 个节点和升级本身全部要延期，而下一个可用的维护窗口在两个月后。&lt;/p&gt;&#xA;&lt;h2 id=&#34;故障现象&#34;&gt;&lt;a href=&#34;#%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;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;kubectl drain&lt;/code&gt; 挂起，反复打印同一类报错：&lt;code&gt;Cannot evict pod as it would violate the pod&#39;s disruption budget&lt;/code&gt;,涉及 &lt;code&gt;order-api&lt;/code&gt; 的 2 个 Pod。&lt;/li&gt;&#xA;&lt;li&gt;节点本身完全正常：状态 Ready、kubelet 健康、上面其他 30 多个 Pod 陆续被正常驱逐，唯独这 2 个不动。&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;kubectl get pdb -A&lt;/code&gt; 显示 &lt;code&gt;order-api&lt;/code&gt; 的 &lt;code&gt;ALLOWED DISRUPTIONS&lt;/code&gt; 列是 &lt;strong&gt;0&lt;/strong&gt;——不只是这一刻为 0,盯着看几分钟一直是 0。&lt;/li&gt;&#xA;&lt;li&gt;换一台节点复现：只要哪个节点上跑着 &lt;code&gt;order-api&lt;/code&gt; 的 Pod,那个节点就排不掉——问题跟着 Pod 走，与节点无关。&lt;/li&gt;&#xA;&lt;li&gt;窗口剩余时间不断缩水，业务高峰时段不允许带业务中断做升级。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;排查过程&#34;&gt;&lt;a href=&#34;#%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;&lt;strong&gt;第一步，确认 drain 的报错语义。&lt;/strong&gt; 报错里写得很直白：eviction 会违反 Pod 的中断预算（disruption budget）。&lt;code&gt;kubectl drain&lt;/code&gt; 默认不是直接删 Pod,而是走 Eviction API——这套 API 会先检查 PodDisruptionBudget(PDB),允许才放行。所以节点排不掉不是网络、不是 kubelet、不是权限，而是&lt;strong&gt;有人给这组 Pod 设了&amp;quot;不允许中断&amp;quot;的规则&lt;/strong&gt;。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;第二步，看清 PDB 的具体配置。&lt;/strong&gt; &lt;code&gt;kubectl describe pdb order-api&lt;/code&gt; 输出：&lt;code&gt;minAvailable: 100%&lt;/code&gt;,&lt;code&gt;Current Healthy: 2&lt;/code&gt;,&lt;code&gt;Desired Healthy: 2&lt;/code&gt;,&lt;code&gt;Disruptions Allowed: 0&lt;/code&gt;。翻译成预算逻辑：可用性下限是&amp;quot;两个都要在&amp;quot;，现实是&amp;quot;两个都在&amp;quot;——没有余量，任何一次自愿中断（eviction）都被拒绝。这个 PDB 的额度是恒定的 0,不管排空等到什么时候都不会变成 1。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;第三步，追溯这个配置从哪来。&lt;/strong&gt; 查变更记录，这个 PDB 是两年前一次线上误删事故后加上的&amp;quot;保险&amp;quot;：当时 &lt;code&gt;order-api&lt;/code&gt; 重启带来过 40 秒抖动，团队顺手配了个 PDB&amp;quot;防止 Pod 被意外杀掉&amp;quot;。配置人按直觉把 &lt;code&gt;minAvailable&lt;/code&gt; 填了 100%——&amp;ldquo;任何时候都别少一个&amp;rdquo;，本意是保护，实际语义却是&lt;strong&gt;永久禁止一切自愿中断&lt;/strong&gt;。因为平时没人排空这个节点上的 Pod,两年里它从未被触发过。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;第四步，把同类地雷一次摸清。&lt;/strong&gt; 借这次机会全集群扫了一遍 PDB：除了 &lt;code&gt;order-api&lt;/code&gt;,还有 3 个单副本 Deployment 配着 &lt;code&gt;minAvailable: 1&lt;/code&gt;(等价于&amp;quot;唯一副本永不中断&amp;quot;),以及 2 个 &lt;code&gt;maxUnavailable: 0&lt;/code&gt; 的写法——本质都是零额度。这次它们没添乱，只是因为当晚没跑在要维护的节点上，下次就会轮到。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;第五步，评估出路。&lt;/strong&gt; 三条路：其一，&lt;code&gt;kubectl drain --disable-eviction&lt;/code&gt; 绕过 PDB 直接删 Pod——等于拆掉保险再让节点排空，虽然 &lt;code&gt;order-api&lt;/code&gt; 有 2 副本、删掉一个服务无感，但这个开关会同时忽略所有 PDB,属于&amp;quot;用大炮打蚊子&amp;quot;，不采用；其二，临时调大副本数——把 2 副本扩到 3,&lt;code&gt;minAvailable: 100%&lt;/code&gt; 仍是零额度，无效；其三，修正 PDB 本身，让它回到&amp;quot;可用性预算&amp;quot;的语义。评估服务当前处于流量低谷、2 副本结构允许任何时刻少 1 个，选第三条。&lt;/p&gt;&#xA;&lt;h2 id=&#34;解决方案&#34;&gt;&lt;a href=&#34;#%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;ol&gt;&#xA;&lt;li&gt;&lt;strong&gt;当场修复&lt;/strong&gt;：把 &lt;code&gt;order-api&lt;/code&gt; 的 PDB 从 &lt;code&gt;minAvailable: 100%&lt;/code&gt; 改为 &lt;code&gt;maxUnavailable: 1&lt;/code&gt;——含义是&amp;quot;允许同时中断 1 个，且至多 1 个&amp;quot;，2 副本时中断额度恒为 1。patch 落下后 drain 立刻推进，该节点在窗口内完成升级。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;全量清理&lt;/strong&gt;：维护结束后按排查第四步的清单修复全部零额度 PDB:单副本负载不配阻塞式 PDB(或将 &lt;code&gt;minAvailable&lt;/code&gt; 降为 0),多副本负载统一用 &lt;code&gt;maxUnavailable: 1&lt;/code&gt; 或 &lt;code&gt;minAvailable: N-1&lt;/code&gt; 的写法。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;预检固化&lt;/strong&gt;：维护窗口开始前增加自动预检脚本——扫描全集群 PDB,输出所有 &lt;code&gt;ALLOWED DISRUPTIONS = 0&lt;/code&gt; 的条目；再对&amp;quot;待维护节点上的 Pod ↔ PDB&amp;quot;做一次对账，存在零额度就直接在窗口开始前卡住流程，而不是排到一半才发现。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;演练回归&lt;/strong&gt;：维护演练脚本加入 PDB 检查项；PDB 变更纳入常规变更评审，禁止再出现&amp;quot;凭直觉填 100%&amp;ldquo;的配置。&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h2 id=&#34;根因分析&#34;&gt;&lt;a href=&#34;#%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;直接根因是 &lt;strong&gt;&lt;code&gt;order-api&lt;/code&gt; 的 PDB 配置为 &lt;code&gt;minAvailable: 100%&lt;/code&gt;,中断预算恒为零,节点排空的 eviction 请求全部被拒绝,drain 永久等待&lt;/strong&gt;。深层根因是把 PDB 理解成了&amp;quot;保护锁&amp;quot;而非&amp;quot;可用性预算&amp;rdquo;:PDB 的语义从来不是&amp;quot;不许中断&amp;quot;,而是&amp;quot;在保住最低可用数的前提下,允许有限度的中断&amp;quot;;当最低可用数等于总数时,预算恒为零,保护机制就变成了维护死锁。再叠加缺少 PDB 的定期审计,两年前的临时&amp;quot;保险&amp;quot;无人复核,地雷一直埋着，直到维护窗口踩上。&lt;/p&gt;&#xA;&lt;h2 id=&#34;预防措施&#34;&gt;&lt;a href=&#34;#%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;制定 PDB 配置规范：任何 PDB 必须留出至少 1 个中断额度（&lt;code&gt;maxUnavailable ≥ 1&lt;/code&gt; 或 &lt;code&gt;minAvailable ≤ 副本数 - 1&lt;/code&gt;）；单副本负载如确需保护，先扩副本再加 PDB；&lt;/li&gt;&#xA;&lt;li&gt;维护窗口前自动预检&amp;quot;零额度 PDB&amp;quot;与&amp;quot;待维护节点上的 Pod ↔ PDB 对账&amp;quot;，未通过不允许进入窗口；&lt;/li&gt;&#xA;&lt;li&gt;季度 PDB 审计纳入例行巡检，与容量巡检同表执行；&lt;/li&gt;&#xA;&lt;li&gt;值班手册补充&amp;quot;排空被 PDB 卡住&amp;quot;的决策顺序：修正 PDB &amp;gt; 扩副本冗余 &amp;gt; 最后才评估 &lt;code&gt;--disable-eviction&lt;/code&gt;（并明确其会忽略全部 PDB 的代价）。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;总结&#34;&gt;&lt;a href=&#34;#%e6%80%bb%e7%bb%93&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;总结&#xD;&#xA;&lt;/h2&gt;&lt;p&gt;这次事故里 PDB 工作得完全正确——它忠实执行了&amp;quot;零中断&amp;quot;的配置，只是配置本身把保护写成了死锁。运维工具里带&amp;quot;预算&amp;quot;&amp;ldquo;额度&amp;quot;字样的机制（PDB、资源配额、限流），共性是&lt;strong&gt;上限不等于永远可用&lt;/strong&gt;：填成&amp;quot;满&amp;quot;或&amp;quot;零&amp;quot;都会让机制退化。凡是两年前以&amp;quot;保险&amp;quot;名义加上的配置，都值得拿&amp;quot;现在还有效吗、会不会挡住正常运维&amp;quot;两个问题重新过一遍——保护机制的第一课，是让保护与日常运转能共存。&lt;/p&gt;&#xA;&lt;h2 id=&#34;延伸阅读&#34;&gt;&lt;a href=&#34;#%e5%bb%b6%e4%bc%b8%e9%98%85%e8%af%bb&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;延伸阅读&#xD;&#xA;&lt;/h2&gt;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://blog.5772447.xyz/posts/e01939e6/&#34; &gt;kubelet 证书轮换失败致节点批量 NotReady&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://blog.5772447.xyz/posts/c0337851/&#34; &gt;磁盘压力触发驱逐：日志与镜像未治理致 Pod 批量被删&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;</description>
        </item></channel>
</rss>
