【K8S 运维实战】11-资源管理Requests与Limits
资源管理:Requests/Limits 与 QoS 分级
一句话定位:为什么设了 Limits 还是被 OOM + QoS 三级怎么用 + ResourceQuota 实战。
写在前面
“我明明设了 memory limit 4G,Pod 还是 OOMKilled 了,这是什么玄学?”——这是我遇到过最经典的资源管理困惑。还有一类:“节点资源没用满,但 Pod 调度不上,显示 Insufficient cpu”。这两类问题的根源,都在于没搞清楚 Requests 和 Limits 的区别,以及它们对调度和运行时的不同影响。
资源管理是 K8s 里最容易被低估的部分。很多人以为"设个 limits 就完了",其实 Requests 决定调度、Limits 决定运行时上限、QoS 决定驱逐优先级,三者环环相扣。设错了,要么调度不上,要么被 OOM,要么节点压力下第一个被杀。这篇把 Requests/Limits 的语义、QoS 三级、CPU Manager、cgroup v1/v2 差异、LimitRange/ResourceQuota 讲透,给一份能直接用的资源规格推荐表和 QoS 治理规范。
核心问题
- Requests 和 Limits 到底有什么区别?分别影响什么?
- 为什么设了 Limits 还是被 OOM?Guaranteed 有什么用?
- QoS 三级(Burstable/BestEffort/Guaranteed)怎么分级?谁先被驱逐?
- CPU Manager 和 NUMA 绑核是干什么的?什么时候要用?
- LimitRange 和 ResourceQuota 怎么配合?怎么防 namespace 资源被吃满?
一、原理剖析
1.1 Requests 与 Limits 的双重作用
Requests 和 Limits 是两个独立的概念,作用于不同阶段:
┌────────────┬──────────────────────────┬──────────────────────────┐ │ │ 调度阶段 │ 运行时阶段 │ ├────────────┼──────────────────────────┼──────────────────────────┤ │ Requests │ ✅ 调度器看这个 │ ❌ 不直接限制 │ │ │ 节点 allocatable - requests│ 但影响 QoS 分级 │ │ │ >= 0 才能调度 │ │ ├────────────┼──────────────────────────┼──────────────────────────┤ │ Limits │ ❌ 调度器不看 │ ✅ cgroup 限制上限 │ │ │ │ CPU:限流(throttling) │ │ │ │ Memory:超限 → OOMKilled │ └────────────┴──────────────────────────┴──────────────────────────┘关键认知:
- Requests 是调度的依据。节点有 16 核,已调度 Pod 的 requests 总和是 10 核,那还能调度 requests=6 核的 Pod,但 requests=7 核的调度不上(显示 Insufficient cpu)。和实际用了多少无关。
- Limits 是运行时的硬上限。CPU limit 2 核,Pod 就算空闲也只能用 2 核;Memory limit 4G,用到 4G 就被 OOMKilled。
- Requests != 实际使用。Requests 是"保证给这么多",Limits 是"最多用这么多",实际用量在两者之间波动。
节点:16 CPU, 64G Memory 已调度 Pod 的 requests 总和:10 CPU, 40G Memory 节点 allocatable:6 CPU, 24G Memory(还能调度) 但实际运行时,这些 Pod 可能只用 3 CPU, 15G Memory 节点还有 13 CPU, 49G Memory 空闲 但调度器只看 requests,不看实际使用1.2 QoS 三级分类
K8s 根据 Requests 和 Limits 的配置,自动把 Pod 分成三个 QoS 等级:
┌─────────────────────────────────────────────────────────────┐ │ Guaranteed(保证级) │ │ 条件:每个容器的 requests == limits(CPU 和 Memory 都要) │ │ 特点:节点资源紧张时最后被驱逐 │ │ 典型:核心数据库、关键中间件 │ ├─────────────────────────────────────────────────────────────┤ │ Burstable(突发级) │ │ 条件:不满足 Guaranteed,且至少一个容器有 requests │ │ 特点:按 requests 超出比例排序驱逐 │ │ 典型:大部分业务应用 │ ├─────────────────────────────────────────────────────────────┤ │ BestEffort(尽力级) │ │ 条件:所有容器的 requests 和 limits 都没设 │ │ 特点:节点资源紧张时第一个被驱逐 │ │ 典型:测试 / 临时任务(生产禁用) │ └─────────────────────────────────────────────────────────────┘驱逐顺序:节点资源压力时,kubelet 按BestEffort → Burstable(按超 requests 比例)→ Guaranteed的顺序杀 Pod。
1.3 为什么设了 Limits 还是被 OOM
这是最高频的困惑。根源在于内存和 CPU 的限制行为不同:
CPU Limit(可压缩资源): 超限 → throttling(限流,等下一个时间片) Pod 不会死,只是变慢 Memory Limit(不可压缩资源): 超限 → OOMKilled(内核 cgroup OOM killer 杀进程) Pod 直接死为什么设了 4G limit 还 OOM:
看错了指标:
kubectl top pod显示的是container_memory_usage_bytes(含 cache),但 OOM 判断用的是container_memory_working_set_bytes(不含 page cache 可回收部分)。你的应用实际用了 3.5G working set + 1G cache,top显示 4.5G 看着没超,但 working set 没超 limit,不会 OOM。反过来,working set 一超 limit,立即 OOM。Java/Go 的内存模型:Java JVM heap + metaspace + 直接内存 + 线程栈,加起来很容易超 limit。JVM 的
-Xmx只管 heap,不管堆外。Go 的 GC 也有延迟,瞬间内存峰值超 limit。内存碎片:cgroup 的 memory.limit_in_bytes 是硬限,但内核内存回收有延迟,瞬间峰值可能触发 OOM。
cgroup v1 vs v2 的差异:v1 的内存统计有 memory.kmem(内核内存)单独算,某些场景下应用内存没超但 kmem 超了导致 OOM。v2 统一了统计,行为更可预测。
# 看真正的内存指标(不是 top)kubectlexec<pod>--cat/sys/fs/cgroup/memory/memory.usage_in_bytes# v1 总用量kubectlexec<pod>--cat/sys/fs/cgroup/memory.memory.max# v2 limit# Prometheus 里的关键指标:# container_memory_working_set_bytes ← OOM 判断依据# container_memory_usage_bytes ← 含 cache,不能用来判断 OOM1.4 CPU Manager 与 NUMA 绑核
CPU Manager 是 kubelet 的一个特性,把 CPU 核心"独占"分配给 Guaranteed Pod:
默认(static 策略): - BestEffort/Burstable:CPU 共享池,按 requests 比例争用 - Guaranteed(整数核 requests):独占 CPU 核,不和其他 Pod 共享 Pod requests: cpu=2 (整数) → 独占 2 个 CPU 核 Pod requests: cpu=500m → 从共享池分配 Pod requests: cpu=2.5 → 不是整数,从共享池分配(static 策略要求整数)什么时候用:延迟敏感型应用(数据库、实时计算、高性能网关),CPU 独占可以避免上下文切换开销。配合 NUMA(Non-Uniform Memory Access)亲和,把 CPU 和内存绑在同一个 NUMA 节点,减少跨节点内存访问延迟。
# 看 kubelet CPU Manager 状态kubectl getnode<node>-ojsonpath='{.status.allocatable.cpu}'ssh<node>'cat /var/lib/kubelet/cpu_manager_state'# 看绑核映射启用要在 kubelet 配置里:
# /var/lib/kubelet/config.yamlcpuManagerPolicy:static# none|staticreservedSystemCPUs:"0,1"# 系统保留的 CPU 核,不分配给 Pod1.5 cgroup v1 vs v2 的差异
K8s 1.25+ 默认支持 cgroup v2(如果内核启用)。v1 和 v2 的差异:
cgroup v1: - 每个子系统独立目录(memory/cpu/blkio/...) - memory.kmem 单独算(内核内存) - 部分场景有统计不准确问题 - 老系统(CentOS 7、Ubuntu 18.04)默认 v1 cgroup v2: - 统一层级(一个目录下所有子系统) - memory 统一统计(含 kmem) - 更准确的 OOM 判断 - 新系统(Ubuntu 22.04、RHEL 9)默认 v2 - K8s 1.25 GA# 看节点 cgroup 版本stat-fc%T /sys/fs/cgroup/# cgroup2fs → v2# tmpfs → v1生产注意:cgroup v2 下,某些老的监控指标路径变了,Prometheus 的 cAdvisor exporter 要升级到支持 v2 的版本。
1.6 LimitRange 与 ResourceQuota
这两个是 namespace 级的资源治理工具:
- LimitRange:给单个 Pod/Container 设默认值和上下限。没设 resources 的 Pod 会被自动加上默认值。
- ResourceQuota:给整个 namespace 设总量上限。namespace 里所有 Pod 的 requests 总和不能超 quota。
┌─────────────────────────────────────────────────────────┐ │ Namespace: team-a │ │ │ │ ResourceQuota: │ │ requests.cpu: 100, requests.memory: 200Gi │ │ limits.cpu: 200, limits.memory: 400Gi │ │ persistentvolumeclaims: 20 │ │ │ │ LimitRange: │ │ default: cpu=1, memory=2Gi ← 没设 limits 时用 │ │ defaultRequest: cpu=500m, memory=512Mi ← 没设 req 时│ │ max: cpu=8, memory=16Gi ← 单 Pod 上限 │ │ min: cpu=100m, memory=128Mi ← 单 Pod 下限 │ └─────────────────────────────────────────────────────────┘二、实战操作
2.1 环境准备
kubectl create ns resource-demo2.2 三种 QoS 的 Pod 配置
# qos-demo.yaml---# Guaranteed:requests == limits(CPU 和 Memory 都要相等)apiVersion:v1kind:Podmetadata:name:pod-guaranteednamespace:resource-demospec:containers:-name:appimage:nginx:1.27resources:requests:cpu:"1"memory:"1Gi"limits:cpu:"1"# 必须和 requests 相等memory:"1Gi"# 必须和 requests 相等---# Burstable:requests < limits,或只设了 requestsapiVersion:v1kind:Podmetadata:name:pod-burstablenamespace:resource-demospec:containers:-name:appimage:nginx:1.27resources:requests:cpu:"500m"memory:"512Mi"limits:cpu:"2"memory:"2Gi"---# BestEffort:啥都不设(生产禁用)apiVersion:v1kind:Podmetadata:name:pod-besteffortnamespace:resource-demospec:containers:-name:appimage:nginx:1.27# 没有 resources 字段kubectl apply-fqos-demo.yaml# 看 QoS 分级kubectl get pods-nresource-demo-ocustom-columns=\NAME:.metadata.name,\QOS:.status.qosClass# 期望:# pod-guaranteed Guaranteed# pod-burstable Burstable# pod-besteffort BestEffort2.3 OOMKilled 复现与排查
# oom-demo.yamlapiVersion:v1kind:Podmetadata:name:pod-oomnamespace:resource-demospec:containers:-name:appimage:polinux/stress:1.0.4resources:requests:cpu:"100m"memory:"128Mi"limits:cpu:"500m"memory:"256Mi"# limit 256Micommand:["stress","--vm","1","--vm-bytes","512M","--vm-hang","1"]# 申请 512M,远超 limit 256Mi,必然 OOM---apiVersion:v1kind:Podmetadata:name:pod-cpu-throttlenamespace:resource-demospec:containers:-name:appimage:busybox:1.36resources:requests:cpu:"100m"memory:"128Mi"limits:cpu:"200m"# limit 0.2 核command:["/bin/sh","-c","while true; do :; done"]# 死循环吃满 CPUkubectl apply-foom-demo.yamlsleep30# 看 OOMkubectl describe pod pod-oom-nresource-demo|grep-A6"Last State"# 输出:# Reason: OOMKilled# Exit Code: 137# 看 CPU 限流kubectltoppod pod-cpu-throttle-nresource-demo# CPU 可能显示 200m(被限流到 limit),但实际应用想用更多# 在节点上看 cgroup 限流ssh<node>'cat /sys/fs/cgroup/cpu/cpu.stat | grep throttled'# nr_throttled 持续增加,说明在限流2.4 资源规格推荐表
按业务类型的资源规格推荐(生产环境):
| 业务类型 | Requests CPU | Requests Mem | Limits CPU | Limits Mem | QoS |
|---|---|---|---|---|---|
| 核心 DB(MySQL/PG) | 4 | 8Gi | 4 | 8Gi | Guaranteed |
| 缓存(Redis) | 2 | 4Gi | 2 | 4Gi | Guaranteed |
| 消息队列(Kafka) | 2 | 4Gi | 4 | 8Gi | Burstable |
| API 网关 | 2 | 2Gi | 4 | 4Gi | Burstable |
| Web 服务(Java) | 1 | 2Gi | 2 | 4Gi | Burstable |
| Web 服务(Go) | 500m | 512Mi | 1 | 1Gi | Burstable |
| 后台任务 | 500m | 512Mi | 1 | 1Gi | Burstable |
| 日志采集(DS) | 100m | 128Mi | 500m | 512Mi | Burstable |
| 监控 Agent(DS) | 100m | 128Mi | 500m | 512Mi | Burstable |
核心原则:
- 核心有状态服务用 Guaranteed:数据库、缓存,必须独占资源,避免被驱逐。
- 业务服务用 Burstable:requests 给"保证能跑"的量,limits 给"峰值能到"的量,中间留 buffer。
- Java 应用的 limits.memory 要留 25% buffer:JVM heap + metaspace + 堆外,limit 设
-Xmx的 1.25 倍以上。 - DaemonSet 用小 requests:节点级服务 requests 小一点,避免占太多调度资源。
2.5 LimitRange 配置
# limitrange-demo.yamlapiVersion:v1kind:LimitRangemetadata:name:default-limitsnamespace:resource-demospec:limits:-type:Containerdefault:# 没设 limits 时的默认值cpu:"1"memory:"1Gi"defaultRequest:# 没设 requests 时的默认值cpu:"500m"memory:"512Mi"max:# 单容器上限cpu:"8"memory:"16Gi"min:# 单容器下限cpu:"100m"memory:"128Mi"maxLimitRequestRatio:# limits/requests 的最大比值(防 Burstable 过度)cpu:"4"memory:"2"---# 测试:不设 resources 的 Pod 会被自动加默认值apiVersion:v1kind:Podmetadata:name:pod-defaultnamespace:resource-demospec:containers:-name:appimage:nginx:1.27# 没设 resourceskubectl apply-flimitrange-demo.yaml kubectl apply-f-<<EOF apiVersion: v1 kind: Pod metadata: name: pod-default namespace: resource-demo spec: containers: - name: app image: nginx:1.27 EOF# 看自动加的默认值kubectl get pod pod-default-nresource-demo-ojsonpath='{.spec.containers[0].resources}'# 应该有 default 的 limits 和 defaultRequest 的 requests2.6 ResourceQuota 配置
# resourcequota-demo.yamlapiVersion:v1kind:ResourceQuotametadata:name:team-quotanamespace:resource-demospec:hard:requests.cpu:"20"# 所有 Pod requests.cpu 总和上限requests.memory:"40Gi"limits.cpu:"40"limits.memory:"80Gi"persistentvolumeclaims:"10"requests.storage:"100Gi"pods:"50"# Pod 数量上限# 按 QoS 限制(防 BestEffort 满天飞):# BestEffort Pod 的数量限制(1.30 支持)---# 测试:超过 quota 会被拒apiVersion:apps/v1kind:Deploymentmetadata:name:big-appnamespace:resource-demospec:replicas:30selector:matchLabels:app:big-apptemplate:metadata:labels:app:big-appspec:containers:-name:appimage:nginx:1.27resources:requests:cpu:"1"# 30 * 1 = 30 CPU,超过 quota 20memory:"2Gi"kubectl apply-fresourcequota-demo.yaml kubectl apply-f-<<EOF apiVersion: apps/v1 kind: Deployment metadata: name: big-app namespace: resource-demo spec: replicas: 30 selector: matchLabels: app: big-app template: metadata: labels: app: big-app spec: containers: - name: app image: nginx:1.27 resources: requests: cpu: "1" memory: "2Gi" EOF# 看 quota 使用情况kubectl describe resourcequota team-quota-nresource-demo# 会显示:# requests.cpu: 20 / 20 ← 已用满# pods: 30 / 50# 新 Pod 调度会被拒(报错 Forbidden, quota exceeded)kubectl get events-nresource-demo|grep-i"forbidden\|quota"2.7 QoS 分级治理规范
# QoS 治理规范(作为团队约定)## 1. 核心服务(数据库/缓存/MQ):Guaranteed# - requests == limits# - 按 P99 容量设,留 30% buffer## 2. 业务服务(API/Web):Burstable# - requests = P50 用量# - limits = P99 用量 * 1.5# - maxLimitRequestRatio <= 4(CPU)## 3. 批处理任务:Burstable# - requests 小一点,limits 给够## 4. DaemonSet(基础设施):Burstable# - requests 尽量小(100m/128Mi)# - limits 适度(500m/512Mi)## 5. 禁用 BestEffort:# - LimitRange 设 min(100m/128Mi),强制所有 Pod 有 requests三、踩坑与排查
踩坑 1:设了 memory limit 但还是 OOMKilled
现象:Java 应用-Xmx2g,limit 设 3Gi,还是 OOM。
原因:JVM 的内存不止 heap。heap(2g) + metaspace(~256m) + 直接内存(默认和 heap 一样大) + 线程栈(每线程 1m) + JIT cache,轻松超 3Gi。
解决:
# 方案 1:用容器感知的 JVM(Java 10+)# JVM 自动识别 cgroup,按 limit 比例分配 heapjava-XX:+UseContainerSupport-XX:MaxRAMPercentage=75-jarapp.jar# MaxRAMPercentage=75 表示 heap 占 limit 的 75%,剩 25% 给堆外# 方案 2:限制直接内存java-XX:MaxDirectMemorySize=512m-Xmx2g-jarapp.jar# 方案 3:limit 调大到 -Xmx 的 1.5 倍resources: limits: memory:"3Gi"# -Xmx2g 的 1.5 倍踩坑 2:CPU limit 设太低,应用变慢
现象:API 响应延迟从 50ms 涨到 500ms,但内存没超,Pod 没 OOM。
原因:CPU limit 太低,被 cgroup 限流(throttling)。CPU 是可压缩资源,超限不限流而是排队,表现就是变慢。
排查:
# 在节点上看 CPU 限流ssh<node>'cat /sys/fs/cgroup/cpu/<pod-cgroup-path>/cpu.stat'# nr_periods: 调度周期数# nr_throttled: 被限流的周期数# throttled_time: 被限流的总时间# 如果 nr_throttled 持续增长,就是在限流# Prometheus 查询rate(container_cpu_cfs_throttled_periods_total[5m])/ rate(container_cpu_cfs_periods_total[5m])>0.1# 限流比例超过 10% 就该关注解决:调大 CPU limit,或改成 Guaranteed(整数核独占)。
踩坑 3:节点资源没用满,但 Pod 调度不上
现象:kubectl describe node显示 CPU 用量才 50%,但新 Pod 调度显示Insufficient cpu。
原因:调度器看的是 requests 总和,不是实际用量。节点 16 核,已调度 Pod 的 requests 总和 15 核,虽然实际只用 8 核,但调度器认为"没资源了"。
解决:
- 调整已有 Pod 的 requests(过度申请的降下来);
- 扩节点;
- 用 overcommit(节点超卖),但风险自担——突发时节点扛不住。
踩坑 4:BestEffort Pod 在节点压力下被杀
现象:某个工具 Pod 突然消失,describe 显示 Evicted。
原因:BestEffort Pod 是驱逐第一优先级,节点内存压力时第一个被杀。
解决:给所有生产 Pod 都设 resources(哪怕 requests 很小),升到 Burstable。
踩坑 5:LimitRange 的 default 没生效
现象:装了 LimitRange,但新 Pod 还是没 resources。
原因:LimitRange 的default只对不设 resources 的容器生效。如果你显式设了空的resources: {},或用resources: {limits: {}},行为可能和预期不符。另外,LimitRange 要在 Pod 创建前存在。
解决:
# 确认 LimitRange 存在kubectl get limitrange-n<ns># Pod 创建后看 resourceskubectl get pod<pod>-ojsonpath='{.spec.containers[0].resources}'# 如果是空的,说明 LimitRange 没匹配(检查 type: Container 配置)踩坑 6:ResourceQuota 限制太严,新 Pod 全调度不上
现象:namespace 里所有新 Deployment 都创建失败,报quota exceeded。
原因:ResourceQuota 的 hard 设得太小,或某个 Deployment 过度申请把 quota 用满了。
解决:
# 看谁占了 quotakubectl get pods-n<ns>-ocustom-columns=\NAME:.metadata.name,\CPU_REQ:.spec.containers[0].resources.requests.cpu,\MEM_REQ:.spec.containers[0].resources.requests.memory|sort-k2-h# 调整过度申请的 Pod,或调大 quotakubectl patch resourcequota<name>-n<ns>--type=json\-p='[{"op":"replace","path":"/spec/hard/requests.cpu","value":"40"}]'四、最佳实践
资源设置
- Requests 按 P50,Limits 按 P99:requests 给日常用量,limits 给峰值,中间留 buffer。
- 核心服务用 Guaranteed:数据库、缓存,requests == limits,独占资源。
- Java 应用 limits.memory = -Xmx * 1.25:留足堆外空间,用
MaxRAMPercentage。 - CPU limit 别设太低:限流比 OOM 更隐蔽,监控
cpu_cfs_throttled_periods。 - 别只设 limits 不设 requests:这样 QoS 是 Burstable,但调度按 limits 算(1.30 之前),资源浪费。
QoS 治理
- 生产禁用 BestEffort:LimitRange 强制设 min,所有 Pod 至少 Burstable。
- 关键路径用 Guaranteed:核心链路上的服务,Guaranteed 保证不被驱逐。
- DaemonSet 用 Burstable 小 requests:节点级服务别占太多调度资源。
LimitRange / ResourceQuota
- 每个业务 namespace 必配 ResourceQuota:防止单团队吃满集群。
- LimitRange 设 default 和 max:兜底,防止忘设 resources 或申请过大。
- 按 QoS 限制 Pod 数:1.30 支持按 QoS 类限制,防 BestEffort 满天飞。
监控
- 监控 working_set_bytes 不是 usage_bytes:OOM 判断依据是前者。
- 监控 CPU 限流比例:
throttled_periods / total_periods > 10%就告警。 - 监控节点 allocatable 和实际用量:超 80% 就该扩容。
- 定期 review 资源申请:实际用量和 requests 偏差大的要调整,避免过度申请。
五、小结
资源管理的核心是"调度 vs 运行时"的双重维度:Requests 决定调度(节点够不够 allocatable),Limits 决定运行时(cgroup 限流或 OOM)。理解这一点,就能解释"为什么节点没用满却调度不上"和"为什么设了 limit 还 OOM"这两个经典问题。
QoS 三级是资源压力下的"生存优先级":Guaranteed 最后被杀,BestEffort 第一个被杀。生产环境必须把所有 Pod 至少升到 Burstable,核心服务用 Guaranteed。LimitRange 和 ResourceQuota 是 namespace 级的治理工具,防止资源被滥用。
监控上要盯三个指标:container_memory_working_set_bytes(OOM 判断)、container_cpu_cfs_throttled_periods(限流)、节点 allocatable(调度余量)。这三个指标设好告警,大部分资源问题都能提前发现。下一篇讲调度器原理和亲和性,把"Pod 为什么调度到这个节点"讲透。
思考题
- 一个 Pod 的 requests.cpu=2,limits.cpu=4,实际用量 P50=1,P99=3。这个配置合理吗?有什么改进空间?
- Guaranteed 的 Pod 一定不会被 OOMKilled 吗?什么情况下还是会 OOM?
- ResourceQuota 的
requests.cpu和limits.cpu分别限制什么?为什么两者都要设?
延伸阅读
- Resource Management for Pods and Containers
- Quality of Service Classes
- CPU Manager Policies
- LimitRange
- ResourceQuota
- cgroup v2 迁移指南
