2588 字
约 8 分钟
21
S15-A23 · 容器的资源限额:limit、request 与实际用量对不上

一个服务在容器里被限制 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. 自测

  1. limits 与 requests 分别由谁读取、分别决定什么?为什么"请求 500m、限制 2 核"的 Pod 可能拿不到 2 核?
  2. CPU 被节流时,为什么使用率看起来不高?哪个文件能证明节流正在发生?
  3. 容器 OOM 与应用自身限额的分工是什么?只依赖容器限额有什么代价?
  4. 为什么节点很空但容器拿不到 CPU?这与 requests 的设定有什么关系?
  5. cgroup 的内存统计为什么可能远高于进程 RSS?哪类应用最容易踩到这个差?
  6. 小数核限额(如 500m)为什么可能让多线程吞吐低于单线程?
S15-A23 · 容器的资源限额:limit、request 与实际用量对不上
https://www.clxhxhhr.top/posts/6404/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。