<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>维护 · XB 现场笔记</title><link>https://www.xbcatm.com/zh/tags/%E7%BB%B4%E6%8A%A4/</link><description>基于 HTTP、DNS、TLS、Caddy、SSH、发布与恢复的可复现运维现场笔记。</description><generator>Hugo</generator><language>zh-CN</language><atom:link href="https://www.xbcatm.com/zh/tags/%E7%BB%B4%E6%8A%A4/index.xml" rel="self" type="application/rss+xml"/><item><title>PodDisruptionBudget 限制经驱逐接口产生的中断</title><link>https://www.xbcatm.com/zh/posts/k8s-pod-disruption-budget-protects-quorum/</link><pubDate>Sat, 10 Apr 2027 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/zh/posts/k8s-pod-disruption-budget-protects-quorum/</guid><description>PDB 约束 Eviction API 请求；选择范围、就绪状态、策略类型与应用法定多数决定实际保护含义。</description><content:encoded>&lt;p&gt;PodDisruptionBudget 限制一组选中 Pod 通过驱逐产生的中断。它可以协助维护期间保留服务容量或法定多数，但不能保证两者必然存在。保护取决于调用者使用 Eviction API、选择器选中正确成员，以及 Ready 条件真正反映应用所需能力。只建立一份对象而不核对这些前提，并不能让维护自动安全。&lt;/p&gt;
&lt;p&gt;允许中断多少，应来自应用的失败模型。三成员多数派系统通常需要两个正常成员，但 Pod 就绪本身不证明成员身份、数据同步或成员之间的连通性。PDB 是应用健康检查与维护流程的补充，不会理解数据库协议，也不会自动纠正已经失去同步却仍报告 Ready 的副本。&lt;/p&gt;
&lt;h2 id="保护边界在驱逐接口"&gt;保护边界在驱逐接口&lt;/h2&gt;
&lt;p&gt;节点崩溃、网络分区和节点压力驱逐，都不会被 PDB 阻止。但已经不可用的成员仍可能减少后续维护可使用的额度。直接删除 Pod 或整个工作负载会绕过预算，工作负载控制器正常滚动更新时的删除也不受它限制。因此“主动中断”不自动等于“必然经过预算保护”，必须看实际调用路径。&lt;/p&gt;
&lt;p&gt;正常 drain 通过驱逐请求工作，在预算阻挡时重试；选择直接删除的调用方则绕过这层保护。依赖预算之前，应确认维护工具真实行为。Deployment 与 StatefulSet 的滚动可用性由各自更新策略控制，所以要协调发布和节点维护，不能假定一份 PDB 就会把所有会影响副本的操作自动串行化。&lt;/p&gt;
&lt;h2 id="计算之前先弄清分母"&gt;计算之前先弄清分母&lt;/h2&gt;
&lt;p&gt;minAvailable 与 maxUnavailable 只能选择其一。三个健康副本配整数 minAvailable 二，在普通稳定状态下允许驱逐一个健康成员。maxUnavailable 一则相对于预期规模表达允许量。currentHealthy 计算符合条件的 Ready Pod，并排除正在删除或已经记录待中断的成员；只处于 Running 阶段并不够，进程在跑也可能完全没有服务能力。&lt;/p&gt;
&lt;p&gt;expectedPods 并不总等于控制器的期望 replicas。此次核对的 Kubernetes 1.34.0 实现中，整数 minAvailable 使用选中 Pod 对象的数量；百分比 minAvailable 与 maxUnavailable 则从所属控制器取得预期规模。因此它与 StatefulSet replicas 不一致，可能来自策略类型或协调时刻，不能直接断定选择器拼错。不同字段的分母需要按实际计算路径理解。&lt;/p&gt;
&lt;p&gt;状态额度大体是 currentHealthy 减去 desiredHealthy，再把负数限制为零，同时还有安全检查与尚未完成驱逐的记账。解释状态时，也应核对 observedGeneration 与 conditions。零只是当前决定，不是诊断结论；它可能表示刻意严格的策略、已有不可用 Pod、正在处理的驱逐，或者控制器同步出现问题。看到一个零就删预算，会跳过最关键的风险判断。&lt;/p&gt;
&lt;p&gt;百分比向上取整。七个副本的一半要求留下四个健康成员，而一个期望副本配百分之三十 maxUnavailable，仍允许失去唯一副本。写成百分比并不自动保证小规模工作负载安全。应检查应用可能达到的最小规模，包括自动扩缩与维护变化之后的状态，而不是只在当前五个副本时算一次就永远沿用。&lt;/p&gt;
&lt;h2 id="不健康策略改变排空行为"&gt;不健康策略改变排空行为&lt;/h2&gt;
&lt;p&gt;默认 IfHealthyBudget 下，Running 但未就绪的 Pod，在 currentHealthy 至少等于 desiredHealthy 时可以驱逐。如果应用已经低于这个门槛，该 Pod 可能阻挡排空，让它有机会恢复。预算并不是看不见它，而是根据不健康状态与整组健康数量做了保守选择。CrashLoopBackOff 或错误就绪探针因此可能让维护长时间等待。&lt;/p&gt;
&lt;p&gt;AlwaysAllow 允许这些运行中但不健康的 Pod 不受该健康门槛限制而被驱逐，不表示健康 Pod 可以无限制删除。选择策略时要同时考虑恢复与维护，尤其要判断替代 Pod 是否也会被同一个错误探针卡住。改变不健康驱逐策略可以解除某类阻塞，却不会修复造成不健康的应用问题。&lt;/p&gt;
&lt;p&gt;三个健康投票成员允许一次中断，是常见的保守选择。成员数量变化时，应重新计算法定多数与可用性；允许一个并不适合所有规模或成员变更协议。下一次健康成员驱逐可能需要等待替代成员真正就绪，不能只看到新 Pod 出现就继续操作，更不能忽视数据同步和重新加入集群所需时间。&lt;/p&gt;
&lt;h2 id="坏掉的样子"&gt;坏掉的样子&lt;/h2&gt;
&lt;p&gt;最小健康数等于当前全部可用副本，可以是刻意阻止健康驱逐的设计，不一定属于配置错误。应记录维护如何获得余量。drain 很慢时，先看替代副本调度、就绪、既有中断与应用负责人的可用性要求，不要单纯因为操作卡住就移除预算，把原本需要决策的停机风险变成默默发生。&lt;/p&gt;
&lt;p&gt;什么都没有匹配到的选择器保护不了任何东西。相反，policy/v1 中空选择器会匹配命名空间的所有 Pod，可能意外扩大范围。应把完整选择器，包括 matchExpressions，与真实标签和所属控制器对照。单独一个 expectedPods 数字，不能证明选中了正确应用；范围过窄和范围过宽都可能破坏原本的维护意图。&lt;/p&gt;
&lt;h2 id="快速验证"&gt;快速验证&lt;/h2&gt;
&lt;p&gt;在指定的只读上下文和命名空间中检查已有对象。以下原命令保持不变，本轮没有在真实集群重新运行。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;kubectl get pdb -A -o custom-columns=NAME:.metadata.name,MIN:.spec.minAvailable,MAX:.spec.maxUnavailable,EXPECTED:.status.expectedPods,HEALTHY:.status.currentHealthy,ALLOWED:.status.disruptionsAllowed
kubectl get pdb zk-pdb -o jsonpath=&amp;#39;{.spec.selector.matchLabels}{&amp;#34;\n&amp;#34;}&amp;#39;
kubectl get statefulset zookeeper -o jsonpath=&amp;#39;{.spec.replicas}{&amp;#34;\n&amp;#34;}&amp;#39;
kubectl get pods -l app=zookeeper --field-selector=status.phase=Running --no-headers | wc -l
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;最后一条统计 Running Pod，不是 Ready Pod。选择器命令只显示 matchLabels，因此可能遗漏 matchExpressions。这些输出可以提供初步线索，但在判定 HEALTHY 或 EXPECTED 异常之前，需要阅读完整 PDB 与 Pod 对象。额度为零不会自动说明原因；额度为正也不证明应用层的法定多数一定能承受下一次驱逐，仍须结合真实成员健康。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kubernetes.io/docs/tasks/run-application/configure-pdb/"&gt;Configure a PodDisruptionBudget&lt;/a&gt;, selectors, rounding and unhealthy-Pod policies，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kubernetes.io/docs/concepts/workloads/pods/disruptions/"&gt;Pod disruptions&lt;/a&gt;, eviction boundaries, involuntary failures and rolling updates，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://raw.githubusercontent.com/kubernetes/kubernetes/v1.34.0/pkg/controller/disruption/disruption.go"&gt;Kubernetes 1.34.0 disruption controller&lt;/a&gt;, getExpectedPodCount and countHealthyPods distinguish integer minimums, controller scale and outstanding disruptions，已核验 2026-10-15。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>vacuum 让增长中的数据库不虚胖</title><link>https://www.xbcatm.com/zh/posts/vacuum-growth/</link><pubDate>Sun, 16 Aug 2026 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/zh/posts/vacuum-growth/</guid><description>区分可复用空间与文件缩小、清理吞吐不足与可见性边界受阻，以及真实 vacuum 代价延迟与演示积压模型。</description><content:encoded>&lt;p&gt;在多版本存储里，删除只是记录变化、而不立刻释放空间。回收这些空位需要维护工作，一旦被跳过，代价就会悄悄累积。&lt;/p&gt;
&lt;h2 id="删除不等于已经回收"&gt;删除不等于已经回收&lt;/h2&gt;
&lt;p&gt;UPDATE 或 DELETE 会留下旧行版本，直到相关快照都不再需要它们。长事务可以在其他会话持续修改数据时推迟这个时刻，但长事务本身不会凭空让一张没有写入的表膨胀。提交删除、旧版本不再可见、空间得到回收，是不同边界。不能只看删除语句成功，就认定这些页面已经可以重新分配。&lt;/p&gt;
&lt;p&gt;普通 VACUUM 通常只是让表文件内部的空间可供新行复用，不会缩小文件。末尾完全空闲的页面有时能够截断；一般压缩则需要 VACUUM FULL 一类重写操作，付出额外空间和更强锁的代价。清理成功但文件大小不变，并不意味着失败。要把活数据、内部可复用空间以及真正归还操作系统的空间分别观察，才不会对正常状态反复执行昂贵重写。&lt;/p&gt;
&lt;h2 id="清理与统计是不同任务"&gt;清理与统计是不同任务&lt;/h2&gt;
&lt;p&gt;autovacuum 由启动进程和多个工作进程组成，并非一名常驻 worker 按固定时刻清完所有表。它根据活动估计和年龄相关规则决定是否执行 VACUUM 或 ANALYZE。ANALYZE 独立收集规划器分布统计，普通 VACUUM 不能替代它。插入驱动的工作与防回卷工作也说明，死元组数量并不是维护的唯一触发条件，不能因为暂时没有大量删除就忽略事务年龄。&lt;/p&gt;
&lt;p&gt;基于代价的延迟，会在估计 I/O 工作累计到一定程度后暂停一段时间。代价上限不是“一次调用最多删掉多少行，达到后就退出”。阈值影响资格，worker 是否有空影响调度，存储能力和限流则影响推进速度。排查需要先判断卡在哪个阶段，不能把各种迟缓都归结为某个比例系数太大，也不能把模拟程序里的清理额度当成真实数据库参数。&lt;/p&gt;
&lt;h2 id="坏掉的样子"&gt;坏掉的样子&lt;/h2&gt;
&lt;p&gt;第一类问题是产生旧版本的速度超过回收速度。连续几个观察窗口中，死元组估计与关系体积不断上涨。应结合已经完成的运行、取消情况、worker 可用性、I/O 与写入速率检查。资源确实成为瓶颈时，额外硬件可能有帮助；但重启数据库本身不会让仍需保留的版本变得可回收。在相同 I/O 上限下，降低阈值或增加 worker 数量，也不保证最终吞吐更高。&lt;/p&gt;
&lt;p&gt;第二类问题是清理在跑，却因为旧快照或保留的 xmin 而无法回收特定版本。干预之前要检查事务及复制状态。事务开始得很早只是线索，不能证明它持有这次受影响的可见性边界。结束应用事务或删除复制槽可能中断工作，甚至让消费者需要重建，因此应先找到负责人、确认保留目的与恢复要求，而不是为了让监控数字下降就直接清除对象。&lt;/p&gt;
&lt;p&gt;第三类问题是仅凭一个 n_dead_tup 值或一条慢计划断定膨胀。统计数量是估计，页面布局、可见性、选择率和统计过时都可能改变读取成本。应比较趋势、vacuum 报告和真实执行计划。文件较大但内部空间可复用，可能是健康稳态；死元组较少，也不排除版本保留或冻结问题。不能让单一计数替代完整因果判断。&lt;/p&gt;
&lt;h2 id="快速验证"&gt;快速验证&lt;/h2&gt;
&lt;p&gt;把程序保存为 &lt;code&gt;vacuum_model.py&lt;/code&gt;，用 &lt;code&gt;python3 vacuum_model.py&lt;/code&gt; 执行。它只用标准库对比触发条件、有限清理吞吐和完全无法回收的积压。工作单位与检查间隔都是人为设定。基础值加比例因子的触发方式，只借用了 autovacuum 的一种思路，省略 PostgreSQL 18 的最大阈值、插入触发、冻结与真实调度。RECLAIM_CAPACITY 是玩具模型的吞吐额度，不是 vacuum_cost_limit，也不能拿它的数值去配置生产数据库。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;&amp;#34;&amp;#34;&amp;#34;Illustrative cleanup-backlog model, not a PostgreSQL vacuum simulator.&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;BASE_THRESHOLD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;SCALE_FACTOR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;CHECK_INTERVAL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;RECLAIM_CAPACITY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1600&lt;/span&gt; &lt;span class="c1"&gt;# arbitrary work units per model cleanup opportunity&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;LIVE_TUPLES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10_000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;THRESHOLD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BASE_THRESHOLD&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;SCALE_FACTOR&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;LIVE_TUPLES&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;needs_vacuum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dead&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;dead&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;THRESHOLD&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_workload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;churn_per_tick&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ticks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pinned&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;dead&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;runs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;max_dead&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;tick&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ticks&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;dead&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;churn_per_tick&lt;/span&gt; &lt;span class="c1"&gt;# each update leaves one dead version&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;max_dead&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_dead&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dead&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tick&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;CHECK_INTERVAL&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;needs_vacuum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dead&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;runs&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;pinned&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;dead&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dead&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;RECLAIM_CAPACITY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;dead&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;dead&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;runs&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;runs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;max_dead&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;max_dead&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;healthy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;run_workload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;churn_per_tick&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ticks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;healthy&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;runs&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;healthy workload should trigger vacuum&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;healthy&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;dead&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;THRESHOLD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;healthy&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;healthy&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;max_dead&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;healthy&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;overloaded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;run_workload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;churn_per_tick&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ticks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;overloaded&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;runs&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;overloaded workload still triggers vacuum&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;overloaded&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;dead&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;THRESHOLD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;overloaded&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;pinned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;run_workload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;churn_per_tick&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ticks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pinned&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;pinned&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;runs&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;healthy&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;runs&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pinned&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;healthy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;pinned&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;dead&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pinned&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;pinned&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;dead&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;healthy&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;dead&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pinned&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;healthy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;healthy: runs=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;healthy&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;runs&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; dead=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;healthy&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;dead&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; max=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;healthy&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;max_dead&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;overloaded: runs=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;overloaded&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;runs&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; dead=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;overloaded&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;dead&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; max=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;overloaded&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;max_dead&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;pinned: runs=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pinned&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;runs&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; dead=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pinned&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;dead&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;PASS: model threshold, limited throughput, and pinned backlog&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vm"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;__main__&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;两种语言的完整代码均在 Python 3.12.14 通过。健康模型最终剩一千六百单位，过载剩一万四千，完全受阻剩八千。健康负载触发几次清理后回到阈值以下；过载仍然触发，却越积越多；完全受阻的总量等于全部新产生工作。这三个不同结果说明，应区分“处理能力不足”和“暂时不能回收”，而不是看到清理运行过就认为问题已经消失。&lt;/p&gt;
&lt;p&gt;这些数值是模型输出，不是 PostgreSQL 测量。真实 vacuum 不会因为达到这个人为额度就停止；真实旧快照也未必阻挡每一个死版本。程序没有连接数据库，没有重现 worker 调度、锁冲突或磁盘延迟，也不预测一次生产清理会耗时多久。理解这个对照之后，仍需从数据库取得证据，再决定问题属于吞吐、保留边界还是其他维护条件。&lt;/p&gt;
&lt;h2 id="延伸阅读"&gt;延伸阅读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.xbcatm.com/zh/posts/postgres-autovacuum-prunes-dead-rows/"&gt;autovacuum 悄悄修剪死行&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.xbcatm.com/zh/posts/postgres-vacuum-full-compaction/"&gt;VACUUM FULL 重建一张表而不是修剪&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://www.postgresql.org/docs/18/routine-vacuuming.html"&gt;PostgreSQL：常规 vacuum&lt;/a&gt;，为什么、何时 vacuum，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://www.postgresql.org/docs/18/maintenance.html"&gt;PostgreSQL：维护&lt;/a&gt;，保持表健康，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://www.postgresql.org/docs/18/monitoring-stats.html"&gt;PostgreSQL：监控统计&lt;/a&gt;，读活动指标，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://www.postgresql.org/docs/18/runtime-config-vacuum.html#RUNTIME-CONFIG-RESOURCE-VACUUM-COST"&gt;PostgreSQL 18 vacuum 代价延迟&lt;/a&gt;，依据估计 I/O 工作暂停，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>