【K8S 运维实战】11-资源管理Requests与Limits

📅 发布时间:2026/7/24 0:10:26
【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 核,那还能调度 requests6 核的 Pod,但 requests7 核的调度不上(显示 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。节点内存压力kubelet 驱逐1. BestEffort Pod最先杀2. Burstable Pod按超 requests 比例排序3. Guaranteed Pod最后才杀4. 系统进程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)kubectlexecpod--cat/sys/fs/cgroup/memory/memory.usage_in_bytes# v1 总用量kubectlexecpod--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: cpu2 (整数) → 独占 2 个 CPU 核 Pod requests: cpu500m → 从共享池分配 Pod requests: cpu2.5 → 不是整数,从共享池分配(static 策略要求整数)什么时候用:延迟敏感型应用(数据库、实时计算、高性能网关),CPU 独占可以避免上下文切换开销。配合 NUMA(Non-Uniform Memory Access)亲和,把 CPU 和内存绑在同一个 NUMA 节点,减少跨节点内存访问延迟。# 看 kubelet CPU Manager 状态kubectl getnodenode-ojsonpath{.status.allocatable.cpu}sshnodecat /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: cpu1, memory2Gi ← 没设 limits 时用 │ │ defaultRequest: cpu500m, memory512Mi ← 没设 req 时│ │ max: cpu8, memory16Gi ← 单 Pod 上限 │ │ min: cpu100m, memory128Mi ← 单 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:1memory:1Gilimits: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:500mmemory:512Milimits:cpu:2memory: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:100mmemory:128Milimits:cpu:500mmemory: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:100mmemory:128Milimits: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-A6Last State# 输出:# Reason: OOMKilled# Exit Code: 137# 看 CPU 限流kubectltoppod pod-cpu-throttle-nresource-demo# CPU 可能显示 200m(被限流到 limit),但实际应用想用更多# 在节点上看 cgroup 限流sshnodecat /sys/fs/cgroup/cpu/cpu.stat | grep throttled# nr_throttled 持续增加,说明在限流2.4 资源规格推荐表按业务类型的资源规格推荐(生产环境):业务类型Requests CPURequests MemLimits CPULimits MemQoS核心 DB(MySQL/PG)48Gi48GiGuaranteed缓存(Redis)24Gi24GiGuaranteed消息队列(Kafka)24Gi48GiBurstableAPI 网关22Gi44GiBurstableWeb 服务(Java)12Gi24GiBurstableWeb 服务(Go)500m512Mi11GiBurstable后台任务500m512Mi11GiBurstable日志采集(DS)100m128Mi500m512MiBurstable监控 Agent(DS)100m128Mi500m512MiBurstable核心原则:核心有状态服务用 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:1memory:1GidefaultRequest:# 没设 requests 时的默认值cpu:500mmemory:512Mimax:# 单容器上限cpu:8memory:16Gimin:# 单容器下限cpu:100mmemory:128MimaxLimitRequestRatio:# limits/requests 的最大比值(防 Burstable 过度)cpu:4memory: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:40Gilimits.cpu:40limits.memory:80Gipersistentvolumeclaims:10requests.storage:100Gipods: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:2Gikubectl 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-iforbidden\|quota2.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:MaxRAMPercentage75-jarapp.jar# MaxRAMPercentage75 表示 heap 占 limit 的 75%,剩 25% 给堆外# 方案 2:限制直接内存java-XX:MaxDirectMemorySize512m-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 限流sshnodecat /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-nns# Pod 创建后看 resourceskubectl get podpod-ojsonpath{.spec.containers[0].resources}# 如果是空的,说明 LimitRange 没匹配(检查 type: Container 配置)踩坑 6:ResourceQuota 限制太严,新 Pod 全调度不上现象:namespace 里所有新 Deployment 都创建失败,报quota exceeded。原因:ResourceQuota 的 hard 设得太小,或某个 Deployment 过度申请把 quota 用满了。解决:# 看谁占了 quotakubectl get pods-nns-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 resourcequotaname-nns--typejson\-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.cpu2,limits.cpu4,实际用量 P501,P993。这个配置合理吗?有什么改进空间?Guaranteed 的 Pod 一定不会被 OOMKilled 吗?什么情况下还是会 OOM?ResourceQuota 的requests.cpu和limits.cpu分别限制什么?为什么两者都要设?延伸阅读Resource Management for Pods and ContainersQuality of Service ClassesCPU Manager PoliciesLimitRangeResourceQuotacgroup v2 迁移指南