<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>CPU Limit on Zero Day Notes</title>
        <link>https://blog.5772447.xyz/tags/cpu-limit/</link>
        <description>Recent content in CPU Limit on Zero Day Notes</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>zh</language>
        <lastBuildDate>Tue, 27 Oct 2026 09:00:00 +0800</lastBuildDate><atom:link href="https://blog.5772447.xyz/tags/cpu-limit/index.xml" rel="self" type="application/rss+xml" /><item>
            <title>cgroup CPU 节流：容器延迟周期性抖动</title>
            <link>https://blog.5772447.xyz/posts/742c1c67/</link>
            <pubDate>Tue, 27 Oct 2026 09:00:00 +0800</pubDate>
            <guid>https://blog.5772447.xyz/posts/742c1c67/</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;公司有一套 Kubernetes 集群，跑着十几个微服务。其中一个用 Go 写的 API 服务，3 个副本，每个副本的资源配置是 requests 500m、limits 1 CPU。节点是 32 核的物理机，整体 CPU 利用率长期不到 30%，资源看起来很宽裕。&lt;/p&gt;&#xA;&lt;p&gt;这个服务上线一年多，平均延迟一直不错。上个月业务侧做了一次促销活动，流量涨了大约一倍，用户开始反馈“接口一阵一阵地慢”：大部分请求几十毫秒就返回，但每隔一段时间会冒出一批一两秒的请求。监控上 CPU 使用率仍然只有三成左右，没有 OOM，没有重启，日志里也没有报错，看起来不像资源不够。&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;平均延迟变化不大，p99 从 200ms 涨到 1.5s 左右，呈周期性尖峰。&lt;/li&gt;&#xA;&lt;li&gt;尖峰出现的时间间隔很规律，大约每 100ms 就有一次小的停顿，流量越大越明显。&lt;/li&gt;&#xA;&lt;li&gt;容器 CPU 使用率按分钟平均只有 0.3 核左右，远低于 1 核的 limit；节点 CPU 也很空闲。&lt;/li&gt;&#xA;&lt;li&gt;没有 OOMKilled、没有探针失败、没有重启；下游数据库和缓存的延迟都正常。&lt;/li&gt;&#xA;&lt;li&gt;GC 日志里的停顿很短，不足以解释秒级的 p99。&lt;/li&gt;&#xA;&lt;li&gt;把副本数从 3 加到 6，平均负载降了，但尖峰仍然存在，只是单次影响小了一点。&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;第一步排除下游。看链路追踪，慢请求的时间几乎都花在服务自身的排队和调度上，调用数据库、Redis 的耗时正常；GC 日志的最大停顿只有几毫秒，也对不上。问题在容器内部的 CPU 调度。&lt;/p&gt;&#xA;&lt;p&gt;第二步看常规指标。Grafana 上容器的 CPU 使用率是 0.3 核左右，节点 CPU 30%，怎么看都不像被限流。但“平均使用率”是分钟级的 rate，会把几百毫秒内的突发抹平；如果突发在极短时间内把配额打满，平均值仍然可以很低。&lt;/p&gt;&#xA;&lt;p&gt;第三步查 cgroup 的节流计数。进容器读 &lt;code&gt;/sys/fs/cgroup/cpu.stat&lt;/code&gt;（cgroup v2；v1 在 &lt;code&gt;/sys/fs/cgroup/cpu/cpu.stat&lt;/code&gt;），重点关注三个字段：&lt;code&gt;nr_periods&lt;/code&gt;、&lt;code&gt;nr_throttled&lt;/code&gt;、&lt;code&gt;throttled_usec&lt;/code&gt;。这台容器的 &lt;code&gt;nr_throttled&lt;/code&gt; 约占 &lt;code&gt;nr_periods&lt;/code&gt; 的 55%，&lt;code&gt;throttled_usec&lt;/code&gt; 累计很大——也就是说，超过一半的调度周期里，这个 cgroup 被内核强行停过。&lt;/p&gt;&#xA;&lt;p&gt;第四步确认配额。&lt;code&gt;cat /sys/fs/cgroup/cpu.max&lt;/code&gt; 输出 &lt;code&gt;100000 100000&lt;/code&gt;，即每 100ms 周期最多使用 100ms CPU，正好是 1 核。Kubernetes 的 &lt;code&gt;limits.cpu: 1&lt;/code&gt; 落到 cgroup 就是这条配额，和节点的 32 核没有关系。&lt;/p&gt;&#xA;&lt;p&gt;第五步解释机理。这个 Go 服务没有设置 &lt;code&gt;GOMAXPROCS&lt;/code&gt;，旧版本 Go 运行时会按宿主机核数初始化，容器里看到 32 个核，于是开了 32 个 P（逻辑处理器）。流量突发时，几十个 goroutine 同时跑，几毫秒内就把 100ms 的配额烧光；内核从此刻起冻结整个 cgroup，直到下一个 100ms 周期开始。期间所有可运行的线程都在等配额，请求排队，表现为周期性尖峰。平均值只有 0.3 核，但瞬时需求远超 1 核，这正是“CPU 没跑满却被节流”的原因。&lt;/p&gt;&#xA;&lt;p&gt;第六步验证。把 limit 从 1 核调到 2 核，同时给容器加 &lt;code&gt;GOMAXPROCS=2&lt;/code&gt;，重新压测：&lt;code&gt;nr_throttled / nr_periods&lt;/code&gt; 从 55% 降到 2% 以下，p99 回到 200ms 以内。只调 GOMAXPROCS 不调 limit 也能缓解，但并发能力会下降；只调 limit 不调 GOMAXPROCS，突发仍然偏大。两个一起改效果最好。&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;按实测重新定 limit&lt;/strong&gt;：拉出过去两周的 CPU 使用分布，按 p95 加上突发余量设置 limit。这个服务从 1 核调到 2 核，request 保持 1 核不变，调度密度和成本都可控。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;让运行时感知配额&lt;/strong&gt;：给 Go 容器设置 &lt;code&gt;GOMAXPROCS&lt;/code&gt; 等于 CPU limit（或使用 &lt;code&gt;automaxprocs&lt;/code&gt; 自动读取 cgroup 配额）。Go 1.25 起默认感知 cgroup 配额，但存量镜像还是旧版本，不能假设运行时已经处理好。Java 服务同理，关注 &lt;code&gt;ActiveProcessorCount&lt;/code&gt; 和 GC 线程数。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;降低突发强度&lt;/strong&gt;：检查是否有一批请求同时触发重计算、缓存穿透或大批量 GC；用连接池、限流、批量拆分把尖峰摊平。但这是辅助手段，不能替代正确的配额。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;补监控&lt;/strong&gt;：把 &lt;code&gt;container_cpu_cfs_throttled_periods_total / container_cpu_cfs_periods_total&lt;/code&gt; 做成面板和告警，阈值例如 5 分钟窗口内超过 25%。只看 CPU 使用率永远发现不了这个问题。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;不要靠调大 period 解决&lt;/strong&gt;：把 CFS period 从 100ms 调到 1s，节流次数会变少，但每次停顿更长，尾延迟可能更差。正确的方向是配额与需求匹配。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;验证&lt;/strong&gt;：压测到目标 QPS，确认 throttled 比例低于阈值、p99 达标，再看节点整体 CPU 是否有足够余量。&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;容器的 CPU limit 只有 1 核，而服务在突发流量下瞬时并发远超 1 核，100ms 周期内的配额被迅速耗尽，cgroup 被内核节流到下一个周期，请求排队形成周期性延迟尖峰&lt;/strong&gt;。&lt;/p&gt;&#xA;&lt;p&gt;更深一层，是“CPU 使用率低就等于资源够”的误判。使用率是时间平均，配额是按周期结算的；一个服务可以平均只用 0.3 核，却在每个周期的头几毫秒需要 3 核，然后被冻结 90 多毫秒。监控只看平均值，正好把这个过程完全藏起来。&lt;/p&gt;&#xA;&lt;p&gt;还有两个放大因素：一是 Go 运行时没有感知 cgroup 配额，在 1 核的容器里按 32 核开线程，突发更猛；二是 limit 当初是拍脑袋定的，没有压测依据。两者叠加，流量一涨，周期性节流就从“偶尔发生”变成了“持续可见”。&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;CPU limit 必须来自压测和真实分布，按 p95 加突发余量设定，禁止凭感觉填 1 核。&lt;/li&gt;&#xA;&lt;li&gt;所有 Go 服务显式设置 &lt;code&gt;GOMAXPROCS&lt;/code&gt; 或引入 &lt;code&gt;automaxprocs&lt;/code&gt;；Java 服务确认 &lt;code&gt;ActiveProcessorCount&lt;/code&gt; 与 limit 一致。&lt;/li&gt;&#xA;&lt;li&gt;把 CFS 节流比例纳入基础监控，和 CPU 使用率、内存、重启次数并列，按 Pod 维度展示。&lt;/li&gt;&#xA;&lt;li&gt;上线前做一次“limit 边界压测”：在目标 QPS 下观察 &lt;code&gt;cpu.stat&lt;/code&gt;，确认 throttled 比例可接受，而不是只看平均 CPU。&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%80%bb%e7%bb%93&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;总结&#xD;&#xA;&lt;/h2&gt;&lt;p&gt;CPU limit 不是“最高速度”，而是“每个周期能领多少配额”。容器里看到 32 个核，不代表能用 32 个核；平均值只有 0.3 核，也不代表每个瞬间都够用。这个服务并不是 CPU 不够，而是配额在错误的时间尺度上被瞬间花光，然后在剩下的周期里被按下暂停键。把 &lt;code&gt;cpu.stat&lt;/code&gt; 的节流比例当成一等指标，把运行时线程数和配额对齐，比盯着 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/b1ca8efe/&#34; &gt;记一次 K8s Pod OOMKilled 触发的服务雪崩&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://blog.5772447.xyz/posts/9c7278d1/&#34; &gt;容器共享内存默认 64MB：浏览器进程频繁崩溃&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;</description>
        </item></channel>
</rss>
