一个服务在容器里被限制 2 核 CPU、2G 内存,压测时 QPS 上不去,top 里看到进程只用了一核多一点。运维说"没到限额",开发说"机器很空"——两边说的都对,因为看的是不同的数。
容器限额有三组数字:配置里写的、cgroup 实际生效的、进程真实用到的。三组对不上是常态,而对不上的时候,决定行为的往往是最小的那一个,不是最显眼的那个。
1. 三组数字与它们的管辖范围
| 数字 | 谁写 | 谁读 | 决定什么 |
|---|---|---|---|
容器 limits |
编排配置 | 调度器 + cgroup | 上限与 OOM |
容器 requests |
编排配置 | 调度器 | 调度与 QoS |
| cgroup 实际值 | 内核 | 内核 | 真正的约束 |
| 进程用量 | 应用 | 监控 | 观察与报警 |
关键点是第二行:requests 只影响调度,不影响运行。调度器按它决定把 Pod 放到哪台机器,放上去之后内核只认 limits。所以"请求 500m、限制 2 核"的 Pod 被调度到一台只剩 600m 的机器上时,它实际可能拿到远少于 2 核的 CPU。
三组数字之间还有个时间差:改配置之后,运行中的容器不会自动换限额(多数运行时要求重建容器或至少重启)。于是"我已经把限额调到 4 核了"与"容器还在 2 核里跑"可以同时为真。
第三行同样重要:编排层写的限额可能被运行时改写(比如被限制到整数核),也可能因为内核版本不同而被解释得不一样。排查时必须看 cgroup 里的实际值,而不是看配置文件。
// 看容器里真正生效的限额(v2 与 v1 路径不同)
cat /sys/fs/cgroup/cpu.max # v2:如 "200000 100000" = 2 核
cat /sys/fs/cgroup/memory.max # v2:字节数或 "max"
cat /sys/fs/cgroup/cpu/cpu.cfs_quota_us # v1
cat /sys/fs/cgroup/memory/memory.limit_in_bytes
// 若这里显示的值与编排配置不同,说明中间被改写或没生效
2. CPU 限额的三种行为差异
CPU 限额在不同编排版本里含义不同,这一条造成的困惑最多。
| 配置形态 | 含义 | 后果 |
|---|---|---|
limits.cpu: 2 |
硬上限 2 核 | 超过即被限流(throttle) |
limits.cpu: 500m |
硬上限 0.5 核 | 单线程服务可能刚好够,多线程严重不足 |
只设 requests |
无硬上限,只有权重 | 空闲时可借用,繁忙时被压 |
requests == limits |
Guaranteed 类 | 稳定但不可借用 |
用一张对照能看清"够不够"这件事在什么条件下成立:
// 单线程服务:0.5 核往往够;多线程服务:0.5 核可能不如单线程
threads=1 limit=0.5 → 每 100ms 有 50ms 可用,单线程能吃满 50ms,够用
threads=8 limit=0.5 → 8 个线程争 50ms,上下文切换与节流叠加,吞吐下降
// 结论:限额与并发度必须一起定,不能分别调
第一行与第三行的差别极大:有硬上限的容器在超过时会被"节流",表现为延迟尖刺,而 CPU 使用率看起来并不高——因为被限流的时间不计入使用量。
// 节流是否发生:比 CPU 使用率更能解释延迟尖刺
cat /sys/fs/cgroup/cpu.stat | grep -E "nr_throttled|throttled_usec"
// nr_throttled 上升 → 容器在持续被限流
// 此时 cpu 使用率可能只有 60%,但延迟已经恶化
3. 内存限额与 OOM 的判定
内存限额是唯一会直接杀进程的限额,它的判定由内核而非编排层做。
| 层 | 依据 | 触发后果 |
|---|---|---|
| 容器 OOM | cgroup 内存上限 | 容器内进程被杀(exit 137) |
| 节点 OOM | 宿主可用内存 | 可能杀掉任意 Pod |
| 应用自身限制 | 堆/池上限 | 抛异常或拒绝 |
判断"到底是谁杀了我"有一个直接的依据:exit code 137 表示被 SIGKILL,而 cgroup OOM 会在内核日志里留下记录。两者的区别在于,节点 OOM 可能杀掉任意 Pod,容器 OOM 只会杀当前容器内的进程。
第一行与第三行的分工值得记住:容器限额是最后一道防线,应用自身的上限是第一道。只依赖容器限额意味着每次超限都是一次进程死亡;应用内限额(如 JVM 堆上限)能把超限变成可处理的错误。
// JVM 在容器里需要知道真实限额,否则按宿主内存算堆
-XX:MaxRAMPercentage=70 // 按容器 limit 的 70% 设堆
-XX:+UseContainerSupport // 8u191+ 默认开启,旧版本需显式加
-XX:MaxDirectMemorySize=256m // 堆外要单独限,否则总量超限
// 堆 + 堆外 + 线程栈 + 代码缓存 必须小于容器 limit,留 15–25% 余量
4. 为什么"机器很空但服务很慢"
这类矛盾的成因可以归成四类。
| 现象 | 机制 | 验证方式 |
|---|---|---|
| 节点空但容器被限流 | 有硬上限,超了就被节流 | 看 nr_throttled |
| 节点空但拿不到 CPU | requests 过高导致调度到拥挤节点 |
看节点实际负载与 requests 总和 |
| 延迟高但使用率低 | 等待下游或 GC,不是 CPU 问题 | 看链路耗时与 GC 日志 |
| 内存够但增长被杀 | 堆外或页缓存计入 cgroup | 看 cgroup 用量而非进程 RSS |
验证方法很直接:同时读两个数字,一个是 cgroup 的用量,一个是进程自身的 RSS。两者差得越多,页缓存与堆外占比越高。这个差值是判断"该压堆还是该压缓存"的唯一依据。
cat /sys/fs/cgroup/memory.current # cgroup 统计(含页缓存)
grep VmRSS /proc/1/status # 进程 RSS(不含页缓存)
// 差值大 → 优先怀疑页缓存与堆外;差值小 → 是堆本身在涨
第四项很隐蔽:cgroup 统计的内存包含页缓存,而 top 里看到的 RSS 不包含。于是一个读大量文件的进程,cgroup 用量早早逼近限额,而进程自己看起来"只用了 300M"。
5. 限额该定多少
| 依据 | 方法 | 风险 |
|---|---|---|
| 按峰值用量 × 1.3 | 需要历史监控 | 峰值不出现时浪费 |
| 按压测拐点 | 需要可信压测 | 压测环境与生产差 |
按 requests 对齐 |
简单一致 | 可能远离真实需求 |
| 按 SLA 倒推并发 | 需要容量模型 | 模型错则全错 |
还有一种更常见的现实:限额是按"当初压测的数字"定的,之后代码与依赖都变重了,限额没跟着改。这类问题的症状是"以前能跑的量现在跑不动",而任何单点指标看起来都正常——限额是唯一没被重新评估的那个参数。
第三种(requests == limits)在实践中很受欢迎,因为它消除了借用带来的不确定性。代价是资源利用率低——每个 Pod 都按自己的上限占位,节点上剩余的空隙无法被别的 Pod 使用。
6. 排错:五种"限额没问题但服务有问题"
| 现象 | 成因 | 处理方向 |
|---|---|---|
| 延迟尖刺而 CPU 不高 | 被 CPU 限额节流 | 查 nr_throttled,提高限额或减少线程 |
| 容器反复重启 exit 137 | 内存超限被杀 | 查 cgroup 用量,含页缓存与堆外 |
| 压测 QPS 上不去而机器空闲 | requests 被调度到拥挤节点 |
看节点 requests 总和与真实负载 |
| 单实例吞吐低于预期 | CPU 限额是小数核,多线程争抢 | 限额取整数核或减少并发 |
| 扩副本没有线性提升 | 共享下游或单实例限额 | 看下游容量与限额比 |
第四项在"500m"这类配置上最常见:0.5 核的意思是每 100ms 只有 50ms 可用,线程越多,被节流的概率越高,吞吐可能反而低于单线程。
7. 边界:什么情况不必精细设限额
| 场景 | 为什么 | 做法 |
|---|---|---|
| 单机部署、独占机器 | 无共享 | 只需应用内限额 |
| 开发/测试环境 | 无稳定性要求 | 不设限额或给足 |
| 批处理任务(可被抢占) | 用低优先级代替限额 | 用 QoS 类而非硬限 |
| 资源充足的私有集群 | 冲突概率低 | 设 requests,不设硬 limits |
还有一条与变更流程有关的边界:限额调整应当被当作变更,而不是调参。它直接影响容量与故障行为,改动之后需要重新观察一轮峰值;把它当成随手可调的旋钮,等于把容量管理变成了试错。
判断口径一句话:这个容器会不会与别人抢同一份资源。不会,限额只是形式;会,限额就是行为的一部分,必须与实际用量对得上。
8. 自测
limits与requests分别由谁读取、分别决定什么?为什么"请求 500m、限制 2 核"的 Pod 可能拿不到 2 核?- CPU 被节流时,为什么使用率看起来不高?哪个文件能证明节流正在发生?
- 容器 OOM 与应用自身限额的分工是什么?只依赖容器限额有什么代价?
- 为什么节点很空但容器拿不到 CPU?这与
requests的设定有什么关系? - cgroup 的内存统计为什么可能远高于进程 RSS?哪类应用最容易踩到这个差?
- 小数核限额(如 500m)为什么可能让多线程吞吐低于单线程?