Kubernetes 资源限制与 QoS——CPU/Memory limit 设置不当的血泪教训
Kubernetes 资源限制与 QoS——CPU/Memory limit 设置不当的血泪教训
适读人群:在 K8s 上跑生产负载的工程师,遇到过 OOM 或者 CPU 节流的人 | 阅读时长:约 15 分钟 | 核心价值:彻底搞清资源限制的工作机制,避开会让你凌晨被叫醒的坑
那次事故发生在一个周三凌晨 2 点 53 分。
告警是我的手机先响的:某核心支付接口 P99 延迟飙到了 4.7 秒。我爬起来登上集群,第一眼看到的是大量 Pod 的 CPU 使用率在 limits 附近反复抖动,节流(throttle)率高达 73%。
但是,kubectl top pod 显示 CPU 使用才到 limit 的 60%……
搞明白这里面的逻辑,花了我整整一个上午。
CPU limit 的真实工作机制
这是我见过被误解最多的 K8s 知识点。
很多人以为:CPU limit 是"容器最多用这么多 CPU 核数",就像内存上限一样。
这个理解是错的。
K8s 的 CPU limit 底层通过 Linux cgroup cpu.cfs_quota_us 和 cpu.cfs_period_us 实现:
cpu.cfs_period_us默认 100ms(每 100ms 为一个周期)cpu.cfs_quota_us= limit(核数) × 100ms
举个例子:limits.cpu: "0.5",意思是每 100ms 的调度周期内,容器最多只能使用 50ms 的 CPU 时间。
关键问题来了:如果你的应用在某个瞬间(比如处理一批并发请求时)需要在 10ms 内用满 2 个核,那么:
- 所需 CPU 时间:10ms × 2核 = 20ms
- 但配额只有 50ms/100ms,而且这 20ms 是突发消耗,可能在周期内很快用完
- 配额用完后,即使宿主机 CPU 空闲,容器也必须等下一个 100ms 周期
这就是 CPU Throttling(CPU 节流)——即使平均 CPU 使用率只有 60%,瞬间突发也会触发节流,表现为延迟陡升。
踩坑实录一:CPU limit 导致 P99 延迟飙升
现象:如上所述,CPU 使用率平均才 60%,P99 延迟却高达 4.7s。
原因:CPU throttling。用以下命令可以查到节流情况:
# 查看容器的 CPU 节流统计
kubectl exec -it <pod-name> -- cat /sys/fs/cgroup/cpu/cpu.stat
# 输出示例:
# nr_periods 12847 (总调度周期数)
# nr_throttled 9421 (被节流的周期数)
# throttled_time 47329847832 (被节流的总时间,纳秒)解法:
方案一(快速止血):临时提高 CPU limit:
resources:
requests:
cpu: "500m"
limits:
cpu: "2000m" # 从 800m 提高到 2000m方案二(根本解法):对延迟敏感的服务,去掉 CPU limit,只保留 CPU request:
resources:
requests:
cpu: "500m" # 保证调度时有这么多资源
# limits.cpu 不设置!
limits:
memory: "512Mi"去掉 CPU limit 后,容器可以在宿主机有空余 CPU 时突发使用,不会被人为节流。
很多人对"不设 CPU limit"很不安——"那不是可以把所有 CPU 都吃光吗?"实际上:
- K8s 调度器按 requests 来分配资源,不按 limits
- 如果宿主机 CPU 不够,按 requests 的比例竞争
- 不设 CPU limit 只是允许突发,不是允许无限制占用
我们的生产环境延迟敏感服务全都不设 CPU limit,改成监控告警(当 CPU 使用超过 requests 的 2 倍时告警),这是更合理的做法。
Memory limit 和 OOM Kill
内存的机制和 CPU 不同。内存不能"节流",超出 limit 的后果只有一个:OOM Kill,容器被强制重启。
Container killed due to OOM: OOMKilled这是 K8s Pod 里最常见的问题之一。
OOM Kill 的几种场景
场景一:应用有内存泄漏,内存持续增长,最终超过 limit:
# 查看 Pod 的 OOM 历史
kubectl describe pod <pod-name> | grep -A5 "Last State"
# 输出示例:
# Last State: Terminated
# Reason: OOMKilled
# Exit Code: 137 (被信号 9 杀死)场景二:Java 应用 JVM 堆内存超出 limit,但 JVM 本身不知道容器 limit:
# 必须配置容器感知
env:
- name: JAVA_OPTS
value: >-
-XX:+UseContainerSupport
-XX:MaxRAMPercentage=75.0
-XX:InitialRAMPercentage=50.0-XX:+UseContainerSupport(JDK 8u191+ 默认开启)让 JVM 读取 cgroup 内存限制而不是宿主机总内存。MaxRAMPercentage=75.0 设置堆最多用 limit 的 75%,留出 25% 给非堆内存、Metaspace、OS 缓冲区。
场景三:limit 设置过低,正常运行就会超出:
# 看 Pod 的内存使用历史
kubectl top pod <pod-name>
# 结合 Prometheus metrics 看最近一周的峰值踩坑实录二:Memory limit 设置参考了平均值而不是峰值
现象:服务平时跑得好好的,每到流量高峰(下午 2 点~4 点),某个 Pod 就会 OOM 重启,每天重启 3~5 次。
原因:设置 limits.memory: 512Mi 时参考的是平均内存使用(380MB),而流量高峰时业务处理峰值会到 520MB,超过了 limit。
解法:
- 先把 limit 临时提高到 768MB 止血
- 用 Prometheus + Grafana 看过去 2 周的内存使用 P99
- 按 P99 峰值 × 1.3 的系数设置 limit
# PromQL 查询内存 P99
histogram_quantile(0.99,
rate(container_memory_working_set_bytes{pod=~"myapp-.*"}[5m])
)QoS(服务质量)等级
K8s 根据 requests 和 limits 的配置,自动给 Pod 分配 QoS 等级,这个等级决定了资源紧张时谁先被驱逐:
Guaranteed(最高优先级,最不容易被驱逐)
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "512Mi" # requests 和 limits 完全相同
cpu: "500m"条件:每个容器都设置了 requests 和 limits,且两者相等。
Burstable(中等优先级)
resources:
requests:
memory: "256Mi"
cpu: "200m"
limits:
memory: "512Mi"
cpu: "1000m"条件:设置了 requests 但 limits > requests,或者只设置了 requests。
BestEffort(最低优先级,最先被驱逐)
# 什么都不设置
resources: {}条件:没有设置任何 requests 和 limits。
资源不足时,K8s 会先驱逐 BestEffort Pod,再驱逐 Burstable Pod,最后才动 Guaranteed Pod。
建议:核心业务服务设置 Guaranteed 或 Burstable(requests 按实际需求,limits 留一定余量),非核心的批处理任务可以设 BestEffort。
踩坑实录三:LimitRange 没配置导致开发者乱设资源
现象:有些开发者不了解 K8s,部署服务时没有设置 resources,集群里跑了一堆 BestEffort Pod。当某台节点内存压力大时,这些 Pod 被批量驱逐,服务中断。
解法:用 LimitRange 为命名空间设置默认资源限制和最大/最小值约束:
apiVersion: v1
kind: LimitRange
metadata:
name: default-limits
namespace: production
spec:
limits:
- type: Container
# 默认值(当 Pod 未设置 requests/limits 时自动应用)
default:
cpu: "500m"
memory: "512Mi"
defaultRequest:
cpu: "100m"
memory: "128Mi"
# 允许的最大值(防止乱设超大 limit)
max:
cpu: "4"
memory: "4Gi"
# 允许的最小值
min:
cpu: "50m"
memory: "64Mi"
- type: Pod
# Pod 级别的限制(所有容器之和)
max:
cpu: "8"
memory: "8Gi"配合 ResourceQuota 限制命名空间总资源量:
apiVersion: v1
kind: ResourceQuota
metadata:
name: production-quota
namespace: production
spec:
hard:
requests.cpu: "20"
requests.memory: 40Gi
limits.cpu: "40"
limits.memory: 80Gi
pods: "50"
services: "20"资源设置实用建议
# 用 VPA(Vertical Pod Autoscaler)获取资源建议值
kubectl get vpa <vpa-name> -o yaml
# recommendation.containerRecommendations 里会有建议的 requests/limits
# 手动查看过去一段时间的资源使用
kubectl top pod --sort-by=memory
kubectl top node总结几条我觉得最重要的原则:
- CPU limit 对延迟敏感服务危害极大,考虑只设 request
- Memory limit 一定要设,否则内存泄漏会影响整台节点
- limit 按峰值的 1.3 倍设,不要按平均值
- Java 服务必须开 UseContainerSupport,否则 JVM 会按宿主机内存分配堆
- 核心服务设置 Guaranteed QoS,requests = limits
资源配置没有放之四海而皆准的答案,需要结合实际压测数据来定。但把上面这些坑都避开,能减少 80% 的因资源配置导致的线上问题。
