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)。