<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>PID 耗尽 on Zero Day Notes</title>
        <link>https://blog.5772447.xyz/tags/pid-%E8%80%97%E5%B0%BD/</link>
        <description>Recent content in PID 耗尽 on Zero Day Notes</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>zh</language>
        <lastBuildDate>Tue, 03 Nov 2026 09:00:00 +0800</lastBuildDate><atom:link href="https://blog.5772447.xyz/tags/pid-%E8%80%97%E5%B0%BD/index.xml" rel="self" type="application/rss+xml" /><item>
            <title>线程泄漏拖垮节点：Pod 没有 pids 上限</title>
            <link>https://blog.5772447.xyz/posts/e9ddc234/</link>
            <pubDate>Tue, 03 Nov 2026 09:00:00 +0800</pubDate>
            <guid>https://blog.5772447.xyz/posts/e9ddc234/</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;公司有一套 5 节点的 Kubernetes 集群，跑着订单、库存、支付等十几个 Java 服务。资源限制按惯例只配了 CPU 和内存，没有人给容器设过进程数上限；kubelet 的 &lt;code&gt;podPidsLimit&lt;/code&gt; 也保持默认的 -1，也就是不限制。大家默认“容器里能起多少进程是应用自己的事”。&lt;/p&gt;&#xA;&lt;p&gt;订单服务有个重试逻辑：调用下游库存接口失败时，把请求交给一个新的 &lt;code&gt;Thread&lt;/code&gt; 去重试，最多重试五次。平时下游很快，重试很少发生。上个月底大促，库存库出现慢查询，接口超时变多，重试线程开始堆积。&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;NotReady&lt;/code&gt;，上面的 Pod 被批量驱逐，业务出现短暂 502。&lt;/li&gt;&#xA;&lt;li&gt;登录节点非常卡，普通命令还能执行，但 &lt;code&gt;kubectl exec&lt;/code&gt;、&lt;code&gt;systemctl restart&lt;/code&gt; 都报 &lt;code&gt;resource temporarily unavailable&lt;/code&gt;。&lt;/li&gt;&#xA;&lt;li&gt;kubelet 日志刷屏：&lt;code&gt;failed to create new process: resource temporarily unavailable&lt;/code&gt;，伴随 runc 创建容器失败。&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;ps -eLf | wc -l&lt;/code&gt; 数出来三万多条，接近该节点 &lt;code&gt;kernel.pid_max&lt;/code&gt; 的 32768。&lt;/li&gt;&#xA;&lt;li&gt;找到订单服务的一个 Pod，&lt;code&gt;/sys/fs/cgroup/.../pids.current&lt;/code&gt; 显示三万左右，几乎全是线程。&lt;/li&gt;&#xA;&lt;li&gt;应用日志里有 &lt;code&gt;java.lang.OutOfMemoryError: unable to create new native thread&lt;/code&gt;，但容器内存并没有到 limit。&lt;/li&gt;&#xA;&lt;li&gt;重启该 Pod 后节点恢复，NotReady 状态自动解除，其他服务陆续回到正常。&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;code&gt;kubectl describe node&lt;/code&gt; 显示 &lt;code&gt;NotReady&lt;/code&gt;，Conditions 里 &lt;code&gt;PIDPressure&lt;/code&gt; 为 True。kubelet 日志的关键错误是 fork 失败：&lt;code&gt;resource temporarily unavailable&lt;/code&gt;。这不是 CPU、内存或磁盘的问题，而是系统创建不出新进程了。&lt;/p&gt;&#xA;&lt;p&gt;第二步确认 PID 总量。节点上 &lt;code&gt;cat /proc/sys/kernel/pid_max&lt;/code&gt; 是 32768，&lt;code&gt;ps -eLf | wc -l&lt;/code&gt; 已经三万多，&lt;code&gt;/proc/loadavg&lt;/code&gt; 最后一位（最近创建的 PID）也接近上限。Linux 的 PID 表是节点级共享资源，线程和进程都占 PID；表满之后，任何进程再 fork 都会返回 EAGAIN，kubelet 无法调用 runc，节点自然就 NotReady 了。这里还要区分两个上限：&lt;code&gt;pid_max&lt;/code&gt; 限制 PID 总量，&lt;code&gt;threads-max&lt;/code&gt; 限制全系统线程总数，容器里的线程同时受两者约束，排查时两个都要看，不能只盯一个。&lt;/p&gt;&#xA;&lt;p&gt;第三步找谁吃掉了 PID。遍历 &lt;code&gt;/sys/fs/cgroup/kubepods/.../pids.current&lt;/code&gt;，绝大多数 Pod 只有几十到几百，只有一个订单服务的 Pod 到了三万。&lt;code&gt;kubectl exec&lt;/code&gt; 已经进不去，只能通过节点上的 nsenter 进命名空间，执行 &lt;code&gt;jstack&lt;/code&gt; 抓线程栈。&lt;/p&gt;&#xA;&lt;p&gt;第四步看线程栈。&lt;code&gt;jstack&lt;/code&gt; 输出里密密麻麻全是 &lt;code&gt;retry-worker-*&lt;/code&gt; 线程，状态大多是 &lt;code&gt;RUNNABLE&lt;/code&gt; 停在 &lt;code&gt;SocketInputStream.socketRead0&lt;/code&gt;，少量在等数据库连接。对照代码，重试逻辑每次失败就 &lt;code&gt;new Thread(...)&lt;/code&gt;，没有线程池、没有上限、没有超时；下游一慢，线程只增不减，直到把节点 PID 表耗尽。&lt;/p&gt;&#xA;&lt;p&gt;第五步确认为什么没有拦住。Deployment 里只有 &lt;code&gt;resources.limits.cpu/memory&lt;/code&gt;，没有 pids 相关配置；kubelet 启动参数没有 &lt;code&gt;--pod-max-pids&lt;/code&gt;，默认 -1；容器运行时也没有设 pids limit。也就是说，这个 Pod 可以无限创建线程，唯一的“上限”是整台节点的 PID 表——而它并不知道自己正在拖垮同节点的其他服务。&lt;/p&gt;&#xA;&lt;p&gt;第六步验证。先在节点上杀掉该 Pod 的进程（kubelet 已经无法正常回收），节点 PID 数量回落，NotReady 解除；再把重试逻辑改成固定大小的线程池加超时，最后在测试集群用脚本模拟线程泄漏，确认 pids 上限会先于节点 PID 表触发，Pod 自己被限制而不是拖垮节点。&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;：节点 PID 耗尽时，kubelet 可能已经无法执行命令，优先通过带外或 SSH 杀掉异常进程，或直接重启节点；恢复后把问题 Pod 驱逐到其他节点，避免再次踩踏。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;设置 pids 上限&lt;/strong&gt;：kubelet 增加 &lt;code&gt;--pod-max-pids=4096&lt;/code&gt;（对应 &lt;code&gt;podPidsLimit&lt;/code&gt;），给所有 Pod 一个统一的进程数上限；对 Java、Go 这类线程多的服务，可按服务单独评估。容器运行时侧也可以配置 pids limit 作为第二道闸。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;修复线程模型&lt;/strong&gt;：重试不再 &lt;code&gt;new Thread&lt;/code&gt;，改用固定大小的线程池 + 有界队列；设置连接超时、重试退避和熔断，下游持续慢时快速失败，而不是无限堆积线程。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;补监控&lt;/strong&gt;：采集每个 Pod 的 &lt;code&gt;pids.current&lt;/code&gt; / &lt;code&gt;container_processes&lt;/code&gt;，以及节点 PID 使用率；阈值设在 80%，并在 &lt;code&gt;PIDPressure&lt;/code&gt; 变成 True 之前告警。同时监控 Java 线程数，线程数持续上涨就是泄漏信号。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;压测与演练&lt;/strong&gt;：在测试环境注入线程泄漏，验证 pids 上限能在 Pod 级别拦住，节点保持 Ready；把“PID 耗尽”纳入故障演练清单。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;验证&lt;/strong&gt;：生产观察一周，订单服务线程数稳定在池大小附近，节点 PID 使用率回落到 10% 以下，未再出现 NotReady。&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;订单服务的无界重试逻辑在下游变慢时持续创建新线程，Pod 又没有 pids 上限，线程耗尽节点级 PID 表，kubelet 和 runc 无法创建新进程，节点进入 NotReady&lt;/strong&gt;。&lt;/p&gt;&#xA;&lt;p&gt;更深一层，是把资源限制理解成了只有 CPU 和内存。PID 是节点级的共享资源，容器默认可以无限使用；kubelet 的 &lt;code&gt;podPidsLimit&lt;/code&gt; 默认 -1，等于把整台节点的 PID 表交给了每一个 Pod。只要有一个应用写出无界线程创建，它就能影响同节点的所有服务，故障半径从“一个 Pod”扩大到“一台节点”。&lt;/p&gt;&#xA;&lt;p&gt;还有一层是重试逻辑的设计问题。重试本身没错，但“每次失败开一个新线程”把下游的慢放大成了本地的线程爆炸；没有超时、没有退避、没有熔断，重试就从保护机制变成了攻击自己的武器。&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;所有 Pod 默认配置 pids 上限，Java/Go 服务按线程模型单独评估；kubelet 的 &lt;code&gt;podPidsLimit&lt;/code&gt; 不允许保持 -1。&lt;/li&gt;&#xA;&lt;li&gt;代码规范禁止无界创建线程：重试必须走有界线程池或异步框架，必须带超时、退避和熔断。&lt;/li&gt;&#xA;&lt;li&gt;监控三件套：Pod &lt;code&gt;pids.current&lt;/code&gt;、节点 PID 使用率、应用线程数；任一持续上涨就告警，不等 &lt;code&gt;PIDPressure&lt;/code&gt;。&lt;/li&gt;&#xA;&lt;li&gt;测试环境定期做线程泄漏注入，验证 pids 上限和 Pod 驱逐行为符合预期。&lt;/li&gt;&#xA;&lt;li&gt;容量评估时把“线程数”纳入服务画像，Java 服务尤其要关注连接池、线程池和重试策略的乘积效应。&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;CPU 和内存限制保护的是资源配额，pids 限制保护的是节点本身。一个没有 pids 上限的 Pod，理论上可以创建到把整台机器的 PID 表吃光，让 kubelet 连一个容器都拉不起来；而应用侧只需要一段“失败就 new 一个线程重试”的代码，就能触发这条链路。把 PID 当成和 CPU、内存一样的资源来管，把重试关进有界线程池，节点才不会因为一个服务的坏习惯而整体倒下。&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/742c1c67/&#34; &gt;cgroup CPU 节流：容器延迟周期性抖动&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://blog.5772447.xyz/posts/b1ca8efe/&#34; &gt;K8s Pod OOMKilled：服务雪崩&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;</description>
        </item></channel>
</rss>
