7.9 资源限制与 QoS

一句话

requests 决定 “调度”,limits 决定 “运行时上限”,两者组合决定 QoS 等级与驱逐顺序;最贵的错误是把 requests 设成 0、limits 设成峰值——前者让节点超卖到 OOM,后者让服务在日常波动里被节流。

requests 与 limits 的四个组合

requests limits 实际行为 风险
不设 不设 完全不限量,BestEffort 一个 Pod 能吃掉整台机器
设了 不设 调度按 requests,运行可超用 节点超卖,高负载时互相踩踏
设了 设了(limit > request) 典型 Burstable 超用被节流(CPU)或被杀(内存)
相等 相等 Guaranteed 稳但贵,且 limit 会变成硬顶

QoS 三档:不是标签,是驱逐顺序

QoS 判据 驱逐顺序 适合
Guaranteed 每个容器都设了 CPU/内存的 request 与 limit 且相等 最后被驱逐 核心交易、延迟敏感
Burstable 至少一个容器设了 request/limit,但不满足 Guaranteed 中间 大多数业务
BestEffort 全部容器都没设 request/limit 最先被驱逐 批处理、可中断任务

驱逐不是随机的:节点内存压力(memory.available < 100Mi 之类的默认阈值)时,kubelet 按 QoS 等级 + 超出 request 的程度 排序,先杀 BestEffort,再杀 “用得比 request 多” 的 Burstable。

CPU 与内存:两种失败方式

资源 超限后果 观测指标 缓解
CPU 节流(变慢,不杀) container_cpu_cfs_throttled_periods_total 比例 提高 limit、留 30% 余量
内存 OOM Kill(直接杀) container_memory_working_set_bytes、OOMKilled 次数 提高 limit、排查泄漏、给 page cache 留空间
ephemeral-storage 驱逐 container_fs_usage_bytes 日志走 stdout、emptyDir.sizeLimit

内存的坑在于 limits.memory 统计的是 cgroup 用量(含 page cache):容器读写文件多时,page cache 会吃掉额度,触发 OOM 时 RSS 看起来还很低。机制见 6.3 namespace (隔离) 与 cgroups (限制)。

怎么定这两个数(可抄的方法)

requests.memory ≈ P95 工作集 × 1.2
limits.memory   ≈ P99 工作集 × 1.5   (或 P95 × 2,视 GC/缓存策略)
requests.cpu    ≈ 常态用量(P50–P75),用于调度与 HPA 基准
limits.cpu      ≈ 峰值用量 × 1.5,或干脆不设(用节点层共享,换取弹性)

三条经验:

  • requests 定得太小:节点超卖,扩容时 “看起来还有余量” 实际已经 OOM。
  • limits 定得太小:CPU 长期节流,P99 抖动;内存频繁 OOMKilled 且查不出泄漏。
  • requests = limits(Guaranteed):稳定但成本最高;只给核心链路,不要全站标配。

命名空间的两道闸

对象 管什么 典型配置
ResourceQuota 命名空间总量(requests.cpu、limits.memory、Pod 数、PVC 数) 防止一个团队吃掉整个集群
LimitRange 单个 Pod/容器的默认值与上下限 没写 requests 时给默认值,避免 BestEffort 泛滥
apiVersion: v1
kind: LimitRange
metadata: {name: defaults, namespace: team-a}
spec:
  limits:
    - type: Container
      default:        {cpu: "500m", memory: "512Mi"}   # 没写 limits 时的默认
      defaultRequest: {cpu: "200m", memory: "256Mi"}   # 没写 requests 时的默认
      max:            {cpu: "4", memory: "8Gi"}        # 上限,防止“一个大 Pod 吃满”

反模式

反模式 后果
全站不写 requests 调度按 0 算,节点超卖,集中 OOM
limits.cpu 设成核数并期待独占 只是上限;竞争时仍按 weight 分配,还会被节流
用 limits.cpu 当 “保底” 保底是 requests 的事,limit 只会限制上限
给所有服务设 Guaranteed 成本翻倍,且峰值被硬顶住,扩不出来
没有 LimitRange 的命名空间 新服务默认 BestEffort,最先被驱逐
只看 CPU 不看内存工作集 内存 OOM 被误判为 “代码 bug”
日志写容器层且不设 emptyDir.sizeLimit 节点磁盘被写满,Pod 被驱逐

三条铁律

🔑 requests 管调度、limits 管运行时;两者都不设 = 把节点交给运气。

🔑 CPU 超限变慢、内存超限被杀;排查方向由这条区分决定。

🔑 Guaranteed 只给核心链路;全站 Guaranteed 的账单通常没人愿意付。

自检

Q1 一个 Pod 的 requests.cpu 是 100m、limits.cpu 是 100m,压测时 P99 飙升但节点 CPU 只有 30%。为什么?

▸ 答案:被自己的 quota 节流。limits.cpu: 100m 表示每 100ms 周期最多 10ms CPU 时间,节点再空闲也不能超用;多线程场景下周期末会等下一周期,表现为 P99 尖刺。验证看 nr_throttled 比例;修法是提高 limit 或降低单实例并发。

Q2 节点 OOM 了,但被杀的 Pod RSS 只有 200Mi,limit 是 1Gi。怎么解释?

▸ 答案:可能是 page cache 吃掉了额度:memory.max 统计 cgroup 的总用量(含页缓存),容器大量读写文件时缓存上涨,触到 1Gi 就 OOM,而 RSS 只反映匿名内存。另一个可能是节点级 OOM(不是 cgroup OOM):多个 Pod 加起来超过节点内存,内核选 “oom_score 最高” 的进程杀,此时看的是节点而非单个 limit。

Q3 为什么 BestEffort 的 Pod 最先被驱逐?

▸ 答案:因为它没有声明任何 requests,调度时按 0 计入,对节点的 “贡献” 最小;kubelet 驱逐时按 “QoS 等级 + 是否超出 request” 排序,BestEffort 排在最前。设计意图是 “让没申请资源的负载先让位给有申请的负载”。所以关键服务至少要设 requests(哪怕很小),避免落到 BestEffort。

Q4 HPA 按 CPU 利用率扩缩容,为什么 requests 设得不准会导致 HPA 失效?

▸ 答案:因为 CPU 利用率的分母是 requests。requests 设得过高,同样的实际用量算出来的利用率偏低,HPA 永远不扩容;设得过低,利用率虚高,频繁触发扩容甚至抖动。正确做法是让 requests 接近常态用量(P50–P75),并给 HPA 配 behavior(扩容快、缩容慢)与稳定窗口。

Q5 团队把所有服务都设成 Guaranteed,账单涨了 80%,怎么优化?

▸ 答案:按 “链路重要性” 分层:核心交易保 Guaranteed(少量),其余改 Burstable(request 按 P95、limit 按 P99×1.5),批处理改 BestEffort + Spot。再用 HPA 把 “峰值冗余” 换成弹性。Guaranteed 的本质是 “用钱买确定性”,全站买确定性等于为所有服务付冗余费。

跨篇相关:cgroup 与 namespace 的机制见 6.3 namespace (隔离) 与 cgroups (限制);QoS 与节点容量规划见 57.2 容量模型、57.3 冗余系数;弹性与 HPA 的云侧约束见 5.4 弹性伸缩与自动扩缩容;驱逐与自愈的关系见 7.6 自愈、滚动更新与回滚;调度只看 requests 见 7.5 调度与亲和性;性能指标与 P99 的定义见 34.1 性能指标 (延迟 / 吞吐 / P99)。