
上一篇【第28篇】污点和容忍——K8s的“拒之门外“机制下一篇【第30篇】QoS——K8s的三六九等资源优先级摘要给Pod配资源就像给租客分房子——给少了住不下给多了浪费。K8s里的requests和limits就是房租和押金requests是你申请的最小保障面积Scheduler按这个数帮你找房子limits是你的最大使用上限超过了要么被限速CPU要么被赶走内存。CPU和内存在这套规则里表现完全不同——CPU是好说话的朋友超过limits不会死只是被按着脑袋减速运行内存是急性子的保安一超过limits立刻OOM Kill连招呼都不打。这两个差异如果不理解清楚生产环境迟早踩坑。本文从mCPU单位讲到OOM触发条件从合理配比讲到怎么用Grafana监控数据反推requests值最后聊聊资源超售的风险和收益——毕竟集群里所有Pod的limits加起来超过Node容量是常态但超多少算安全一、Requests vs Limits——“保底和封顶”1.1 你写了两行配置Scheduler做了两件不同的事apiVersion:v1kind:Podmetadata:name:my-appspec:containers:-name:appimage:nginxresources:requests:# ← 保底调度时用这个数找Nodecpu:500m# 我需要至少 0.5 核 CPUmemory:512Mi# 我需要至少 512Mi 内存limits:# ← 封顶运行时不能超过这个数cpu:1000m# 我最多用 1 核 CPUmemory:1Gi# 我最多用 1Gi 内存【Requests 和 Limits 的双重身份】 Requests请求量 Limits限制量 ┌─────────────────────────┐ ┌─────────────────────────┐ │ 身份1Scheduler的尺子 │ │ 身份1kubelet的警察 │ │ Node还剩多少资源 │ │ 你超限了CPU限速/ │ │ 用requests去算 │ │ 内存OOM Kill │ │ │ │ │ │ 身份2资源预留 │ │ 身份2cgroup的围墙 │ │ 把requests的量从Node │ │ 把limits的值写进 │ │ 可分配资源里扣掉 │ │ CPU shares / │ │ │ │ memory.limit_in_bytes │ └─────────────────────────┘ └─────────────────────────┘ 调度阶段 ┌────────────────────────────────────────────────────────────┐ │ Scheduler 判断 Node-1 能不能放下这个Pod │ │ │ │ Node-1: 4C 8Gi Pod requests: │ │ 已调度Pod requests 总和 cpu: 500m │ │ cpu: 3000m (3核) mem: 512Mi │ │ mem: 6Gi │ │ │ │ 剩余可分配cpu1000m, mem2Gi │ │ 新Pod需要cpu500m, mem512Mi │ │ → 都够调度到Node-1 │ │ │ │ ★ 注意Scheduler只看 requests不管 limits 多大 │ └────────────────────────────────────────────────────────────┘1.2 CPU和内存——可压缩 vs 不可压缩【CPU和内存的本质区别——能不能憋着不动】 CPU可压缩资源 内存不可压缩资源 ┌────────────────────────┐ ┌────────────────────────┐ │ 超过limits怎么办 │ │ 超过limits怎么办 │ │ │ │ │ │ 不会死 │ │ 直接死 │ │ 只是被throttle—— │ │ OOM Killer一刀下去 │ │ 像被按着头跑步变走路 │ │ │ │ │ │ │ │ 原理 │ │ 原理 │ │ Linux CFS调度器 │ │ 内存不用就是占着 │ │ 把CPU时间片收紧 │ │ 不能压缩只能杀掉 │ │ │ │ │ │ 表现 │ │ 表现 │ │ 响应变慢但不会挂 │ │ Container重启 │ │ P99延迟飙升 │ │ 数据丢失 │ └────────────────────────┘ └────────────────────────┘ 类比 CPU像水龙头——开小点水少但不会断 流 内存像箱子——能装的量固定塞不下了就爆要点这就是为什么生产环境的内存Limits要特别谨慎——设太低应用多分配了几MB就被Kill设太高一个Pod的内存泄漏就能拖死整台Node。CPU则宽松得多超了就超了只是慢一点。我的经验法则是CPU的requestslimits可以宽松内存的requestslimits最好严格。1.3 CPU单位的两种写法# CPU 在K8s里用毫核millicores/m表示# 1000m 1 核 1 vCPU 1个超线程# 写法1毫核cpu:100m# 0.1 核——10%的一个CPU核心cpu:500m# 0.5 核——半个CPU核心cpu:2000m# 2 核——两个CPU核心# 写法2小数cpu:0.1# 等同于 100mcpu:0.5# 等同于 500mcpu:2# 等同于 2000mcpu:1# 等同于 1000m# 100m CPU 到底能跑多少# 如果Node是3.0GHz CPU:# 100m 10%的一个核心 300MHz 的算力# 不是独占300MHz而是CFS调度器保证你每100ms能跑10ms# 内存单位# Mi Mebibyte (1024 * 1024 bytes)# Gi Gibibyte# M Megabyte (1000 * 1000 bytes)——一般不用memory:128Mi# 128 Mebibytesmemory:1Gi# 1 Gibibyte 1024 Mimemory:512Mi# 常见的微服务内存分配二、CPU超限之后的Throttle——“你慢下来了”2.1 CPU Throttle的原理【CPU Throttle——你被限速了】 正常状态使用 Limits ┌──────────────────────────────────────────────┐ │ Pod CPU Limit: 1000m (1核) │ │ Pod实际使用: 500m │ │ │ │ CFS周期 (100ms): │ │ ┌────────────────────────────────────────┐ │ │ │████████████████░░░░░░░░░░░░░░░░░░░░░░░░│ │ │ │ 使用50ms 空闲50ms │ │ │ └────────────────────────────────────────┘ │ │ 配额用不完不受影响 │ └──────────────────────────────────────────────┘ 超限状态使用 Limits ┌──────────────────────────────────────────────┐ │ Pod CPU Limit: 1000m (1核) │ │ Pod实际需求: 2000m (想用2核) │ │ │ │ CFS周期 (100ms): │ │ ┌────────────────────────────────────────┐ │ │ │████████████████████░░░░░░░░░░░░░░░░░░░░│ │ │ │ 使用100ms 强制等待50ms │ │ │ └────────────────────────────────────────┘ │ │ ┌────────────────────────────────────────┐ │ │ │████████████████████░░░░░░░░░░░░░░░░░░░░│ │ │ │ 使用100ms 强制等待50ms │ │ │ └────────────────────────────────────────┘ │ │ 实际获得1核的算力被压到limits水平 │ │ 表现P99延迟飙升、请求超时 │ └──────────────────────────────────────────────┘# 查看Pod是否被CPU Throttlekubectltoppod my-app# NAME CPU(cores) MEMORY(bytes)# my-app 950m 128Mi# ↑ 如果接近或等于limits可能正在被throttle# 查看CPU Throttle详细指标需要Prometheus cAdvisor# 常用PromQL# rate(container_cpu_cfs_throttled_seconds_total{podmy-app}[5m])# ↑ 如果这个值持续0说明Pod在被throttle2.2 CPU Throttle的实际影响场景CPU limits实际使用效果表现轻度超限1000m1200m轻微throttleP99延迟略有上升用户感知不到中度超限1000m2000m明显throttleP99延迟翻倍部分请求超时严重超限1000m5000m严重throttle大量请求超时服务基本不可用未设limits无限制任意不会被throttle可能导致其他Pod受影响要点CPU overcommit超售在K8s中是允许的——你可以在一个4核的Node上跑10个request为1核的Pod总和10核4核物理核K8s会让它们分时共享。这在低负载时完全没问题——10个Pod都不可能同时跑满。但如果突然一起忙起来10个Pod每核只能分到0.4核的算力都要被throttle。这就是超售的风险平时风平浪静出事集体拉胯。三、内存超限之后的OOM Kill——“直接埋了”3.1 OOM Kill的工作流程【OOM Kill 流程——警察破门而入】 阶段1Pod超限 ┌──────────────────────────────────────────────┐ │ Pod Memory Limit: 512Mi │ │ Pod实际使用: 600Mi超了88Mi │ │ │ │ kubelet: 超限了超限了 │ └───────────────────┬──────────────────────────┘ │ ▼ 阶段2OOM Kill ┌──────────────────────────────────────────────┐ │ 1. kubelet根据OOM Score选择要杀掉的进程 │ │ → 通常是本Pod里使用内存最多的进程 │ │ 2. 发送 SIGKILL不是SIGTERM没法优雅关闭 │ │ 3. 容器退出码137128 9 SIGKILL │ └───────────────────┬──────────────────────────┘ │ ▼ 阶段3Pod重启 ┌──────────────────────────────────────────────┐ │ 1. Container状态 → Terminated (OOMKilled) │ │ 2. kubelet 根据 restartPolicy 决定是否重启 │ │ 3. restartPolicy: Always → 自动重启 │ │ 4. CrashLoopBackOff → 频繁OOM的Pod会被延后 │ └──────────────────────────────────────────────┘# 查看Pod是否发生过OOM Killkubectl describe pod my-app# 看 Last State上一次容器的状态:# Last State: Terminated# Reason: OOMKilled# Exit Code: 137# Started: Mon, 28 Jul 2026 10:15:00 0800# Finished: Mon, 28 Jul 2026 10:15:30 0800# 查看Pod的重启次数——异常高说明频繁OOMkubectl get pod my-app# NAME READY STATUS RESTARTS AGE# my-app 1/1 Running 15 (重启15次!) 2h# 查看OOM事件kubectl get events --field-selectorreasonOOMKilling# LAST SEEN TYPE REASON OBJECT# 2m Warning OOMKilling pod/my-app Memory cgroup out of memory3.2 Node级别的OOM——整个节点被拖死【两种OOM——Pod级别 vs Node级别】 Pod级别OOM常见 Node级别OOM灾难 ┌──────────────────────────────┐ ┌──────────────────────────────┐ │ Pod limits: 512Mi │ │ Node总内存: 8Gi │ │ Pod用了600Mi → kubelet杀进程 │ │ 所有Pod usage总和 8Gi │ │ │ │ → 内核OOM Killer介入 │ │ 影响范围单个Pod │ │ │ │ 可控性高配好limits就OK │ │ 影响范围这台Node上所有Pod │ └──────────────────────────────┘ │ 可控性低Linux内核决定的 │ └──────────────────────────────┘ Node OOM的OOM_SCORE计算简化版 ┌──────────────────────────────────────────────────────┐ │ OOM Score (内存使用比例 × 10) │ │ (oom_score_adj) │ │ │ │ K8s给不同类型的Pod设置了不同的oom_score_adj │ │ • Guaranteed Pod: -998 (基本不会被杀) │ │ • Burstable Pod: min(max(2, 1000 × OOMScoreAdj), 999) │ │ • BestEffort Pod: 1000 (优先被杀) │ │ │ │ OOM Score越高越容易被内核选中杀死 │ └──────────────────────────────────────────────────────┘要点Node级别的OOM是真正的灾难——内核OOM Killer不受K8s控制它会扫描这台Node上所有进程选OOM Score最高的那个杀掉。你可能好端端的BestEffort Pod因为别人家的内存泄漏被内核处决了。避免Node OOM的唯一方法所有Pod都要设内存limits并且总和不要超过Node物理内存。3.3 OOM Kill实战模拟# 故意写一个会OOM的Pod——内存泄漏模拟器apiVersion:v1kind:Podmetadata:name:oom-testspec:containers:-name:memory-hungryimage:python:3.11-alpinecommand:-python--c-|# 不断分配内存直到OOM data [] while True: data.append(x * 1024 * 1024) # 每次分配1MBresources:limits:memory:128Mi# 只能用到128Mirequests:memory:64Mi# 部署并观察kubectl apply-foom-test.yaml# 看Pod状态变化kubectl get pod oom-test-w# NAME READY STATUS RESTARTS AGE# oom-test 0/1 OOMKilled 0 10s# oom-test 1/1 Running 1 15s ← 自动重启了# oom-test 0/1 OOMKilled 1 25s ← 又OOM了# oom-test 0/1 CrashLoopBackOff 1 30s ← 进入退避模式# 看详情kubectl describe pod oom-test# State: Waiting# Reason: CrashLoopBackOff# Last State: Terminated# Reason: OOMKilled# Exit Code: 137# 修复方案——根据实际内存使用调整limitsapiVersion:v1kind:Podmetadata:name:oom-test-fixedspec:containers:-name:memory-hungryimage:python:3.11-alpinecommand:[python,-c,data [x * 1048576 for _ in range(200)]; import time; time.sleep(3600)]resources:limits:memory:512Mi# ← 提高到512Mi200×1MB 开销requests:memory:256Mi四、怎么设Requests和Limits——“给多少不浪费又不翻车”4.1 基于监控数据反推——“让数据说话”【从监控到配置——资源规划的完整路径】 步骤1部署时先用宽松值 ┌──────────────────────────────────────────────┐ │ resources: │ │ requests: │ │ cpu: 200m # 先设一个偏小的值 │ │ memory: 256Mi │ │ limits: │ │ cpu: 1000m # 设一个较大的上限 │ │ memory: 1Gi │ └──────────────────────────────────────────────┘ 步骤2跑一段时间收集监控数据Grafana Prometheus ┌──────────────────────────────────────────────┐ │ 观察7天覆盖至少一个业务周期 │ │ │ │ CPU Usage 曲线 │ │ ┌────────────────────────────────────────┐ │ │ │ ▁▁▁▃▅█▇▅▃▁▁▁▁▁▁▁▃▅█▇▅▃▁▁▁ │ │ │ │ P50: 150m P95: 600m P99: 900m │ │ │ └────────────────────────────────────────┘ │ │ │ │ Memory Usage 曲线 │ │ ┌────────────────────────────────────────┐ │ │ │ ▄▄▄▄▄▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▄ │ │ │ │ P50: 180Mi P95: 350Mi P99: 450Mi │ │ │ └────────────────────────────────────────┘ │ └──────────────────────────────────────────────┘ 步骤3根据P95/P99设定最终值 ┌──────────────────────────────────────────────┐ │ resources: │ │ requests: │ │ cpu: 300m # P95的50% │ │ memory: 400Mi # 略高于P95 │ │ limits: │ │ cpu: 1500m # P99的1.5倍 │ │ memory: 600Mi # P95的1.5倍 │ └──────────────────────────────────────────────┘4.2 各语言/框架的内存特征应用类型内存特征推荐策略原因Java/Spring启动高JVM堆稳定requestslimits相等JVM的-Xmx设多少就占多少设不等会浪费Go启动低逐步增长requests limits1:2初始小GC后释放但高峰需要上限Python/Django逐步爬升requests limits1:2~3懒加载不稳定Node.js基本稳定requests ≈ limits接近单线程内存基本固定Nginx/静态服务非常稳定requests limits相等内存基本不变Redis/缓存由数据量决定requests limits相等业务数据增长需要精确规划# 示例Java Spring Boot 应用的合理配置apiVersion:v1kind:Podmetadata:name:java-appspec:containers:-name:appimage:my-java-app:v1.0env:-name:JAVA_OPTSvalue:-Xms512m -Xmx512m# JVM堆固定512Miresources:requests:cpu:500mmemory:768Mi# JVM堆512Mi 堆外256Milimits:cpu:1000m# CPU可以宽松memory:768Mi# 内存跟requests一样——相等# 设requestslimits的原因# JVM -Xmx512m 堆外开销 ≈ 768Mi多给了JVM也不会用反而浪费4.3 常见坑和配置原则# 坑1requests设太小——调度到资源紧张的Node# 你的Pod requests只有100m CPU实际要用800m# → Scheduler认为这台Node还能再塞9个这样的Pod# → 结果塞了3个全部CPU Throttle# 坑2limits设太小——正常运行都被kill# Java应用启动时内存峰值800Milimits设512Mi# → JVM还没初始化完就被OOM Kill启动即死循环# 坑3不设limits——一颗老鼠屎坏一锅粥# 一个Pod没设内存limits发生了内存泄漏# → 逐渐吃掉Node上所有空闲内存# → Node OOM → 内核随机杀Pod → 可能杀到核心服务# 坑4所有Pod request总和超过Node容量——集群资源假满载# Node: 4C 8Gi# Pod-A: request 2C 4Gi, Pod-B: request 2C 4Gi# → 看起来Node装满了# → 但实际使用Pod-A只用了0.5C 2Gi, Pod-B也只用了0.5C 2Gi# → 实际只是计划满物理资源还多的是【合理的Requests vs Limits配比】 CPU可压缩可以大胆超售 ┌─────────────────────────────────────────┐ │ requests: 保证值 → 设P50-P90的值 │ │ limits: 上限值 → 设P95的1.5倍左右 │ │ requests:limits 比例 → 1:2 到 1:4 │ │ │ │ 例子P50200m, P95600m │ │ requests: 300m │ │ limits: 1500m │ └─────────────────────────────────────────┘ 内存不可压缩谨慎超售 ┌─────────────────────────────────────────┐ │ requests: 保证值 → 设P95-P99的值 │ │ limits: 上限值 → 设P95的1.2-1.5倍 │ │ requests:limits 比例 → 1:1 到 1:2 │ │ │ │ 例子P95350Mi, P99450Mi │ │ requests: 400Mi │ │ limits: 512Mi │ └─────────────────────────────────────────┘要点一个重要的原则——requests决定的是调度质量limits决定的是运行安全。requests设太小Scheduler把Pod堆到资源不足的Node上limits设太小正常运行都可能被kill。我的习惯做法是新服务上线第一个月用宽松配置requests偏小limits偏大跑一个月后看Grafana监控数据再调整为精确配置。五、资源超售Overcommit——花小钱办大事的风险艺术5.1 超售场景分析【超售是什么——你卖了100间房但只有80间】 物理资源 vs 声明的资源 ┌─────────────────────────────────────────────────────┐ │ │ │ Node-1 物理配置4 核 CPU8Gi 内存 │ │ │ │ 已调度的 Pod 及其 requests │ │ ┌──────────┬──────────┬──────────┐ │ │ │ Pod │ CPU Req │ Mem Req │ │ │ ├──────────┼──────────┼──────────┤ │ │ │ web-1 │ 500m │ 512Mi │ │ │ │ web-2 │ 500m │ 512Mi │ │ │ │ api-1 │ 300m │ 256Mi │ │ │ │ api-2 │ 300m │ 256Mi │ │ │ │ redis │ 200m │ 1Gi │ │ │ │ worker │ 500m │ 512Mi │ │ │ │ es-1 │ 1000m │ 2Gi │ │ │ │ es-2 │ 1000m │ 2Gi │ │ │ ├──────────┼──────────┼──────────┤ │ │ │ 合计 │ 4300m │ 7Gi │ │ │ │ │ (超了300m)│(接近上限)│ │ │ └──────────┴──────────┴──────────┘ │ │ │ │ CPU超售4300m request 4000m 物理核 │ │ → 所有Pod同时跑满 → 每Pod只能分到 4/4.393%的算力 │ │ → 影响轻微throttle一般不致命 │ └─────────────────────────────────────────────────────┘【CPU超售风险 vs 内存超售风险】 CPU超售可以接受 内存超售必须避免 ┌──────────────────────┐ ┌──────────────────────┐ │ 风险性能下降 │ │ 风险Node OOM │ │ 可控throttle慢而已 │ │ 不可控内核随机杀Pod │ │ 恢复负载降了自动恢复 │ │ 恢复被杀的要重建 │ │ │ │ │ │ 安全阈值 │ │ 安全阈值 │ │ CPU总requests │ │ 内存总requests │ │ 可达物理核的1.5-3倍 │ │ 应 ≤ 物理内存 × 0.8 │ │ │ │ 留20%给系统开销 │ └──────────────────────┘ └──────────────────────┘5.2 超售的安全计算公式# CPU 超售安全公式# ┌────────────────────────────────────────────────┐# │ 总和(所有Pod的cpu.requests) │# │ ───────────────────────────── ≤ 安全倍数 │# │ 总和(所有Node的CPU核数) │# └────────────────────────────────────────────────┘# 安全倍数参考# • 1.0 → 保守不做超售—— 企业核心系统# • 1.5 → 安全 —— 互联网服务推荐# • 2.0 → 中等风险 —— 测试环境# • 3.0 → 高风险 —— 只有CI/CD环境这么玩# 内存超售安全公式# ┌────────────────────────────────────────────────┐# │ 总和(所有Pod的memory.requests) │# │ ────────────────────────────────── ≤ 80% │# │ 总和(所有Node的内存) │# │ 永远留20%给系统kubelet、内核、buffer/cache │# └────────────────────────────────────────────────┘# 实战检查脚本kubectl describe nodes|grep-A5Allocated resources# Allocated resources:# Resource Requests Limits# cpu 3500m (87%) 7500m (187%)# memory 6Gi (75%) 10Gi (125%)# ↑ 87%安全 ↑ 187%——limits超售无所谓要点CPU limits超售非常常见——187%完全正常。但内存requests超过85%就要警惕了。我见过最惨的案例一个团队把20个Java Pod的memory requests的总和设到Node内存的95%留了5%给系统——结果系统OOM Scanner扫描时触发了内核OOM随机处决了一批Pod。从那以后他们都严格遵守80%上限。本篇小结Requests和Limits是K8s资源管理的两个核心参数搞懂了它们你的Pod才不会莫名其妙被杀或卡死requests是保证量——Scheduler用它找Nodekubelet用它预留资源设太小会导致调度到资源紧张的Nodelimits是上限值——CPU超了throttle变慢内存超了OOM Kill直接死两种资源的超限后果完全不同CPU可压缩、内存不可压缩——CPU超售可以接受1.5-3倍内存超售很危险必须≤物理内存的80%设多少看监控——上线先用宽松值跑一个月收集Grafana数据后再精确调整到P95/P99Java/Go/Python内存特征不同——Java最好requestslimitsGo可以1:2Redis等缓存型要精确规划资源配好了但K8s怎么决定资源紧张时先杀谁下一篇咱们聊QoS——K8s的三六九等资源优先级带你看看Guaranteed、Burstable、BestEffort这三个等级到底怎么打分。上一篇【第28篇】污点和容忍——K8s的“拒之门外“机制下一篇【第30篇】QoS——K8s的三六九等资源优先级