资讯详情

资讯详情

行业资讯

从requests到OOM Killer:Kubernetes Pod资源管理全链路解析

凌晨两点线上某服务连着三次重启kubectl get events里OOMKilled红通通地躺着。第一反应很直接把 memory 的 limit 从 512Mi 加到 1024Mi重新发布然后坐等。结果第二天凌晨同样的时间点Evicted又来了。这类场景我见过太多次问题不在“limit 不够大”而在大家把 Pod 资源模型想成了一行数字但 K8S 里这套资源机制从调度器一路贯穿到内核的 cgroup 和 OOM Killer每个字段管的是不同层的事。这篇内容我就把 Pod 资源这条链路从头到尾捋一遍把 requests、limits、QoS、驱逐排序这些概念串起来讲适合正在被 Pod 无故被杀困扰、或者想把资源配到心里有数的人。1. 从Pod被杀现场说起资源这条隐形链路到底连着哪些内核机制1.1 三种“死亡方式”的差异OOMKilled、Evicted 与启动即失败很多人看到 Pod 状态不对第一反应都是“加内存”。但 Pod 死法不一样背后对应的机制完全不同不先分清再动手基本白改。OOMKilled是容器内进程触发了内核 OOM Killer。最常见的原因有两个一是容器 cgroup 内存上限设得太低进程申请内存时被内核直接干掉二是整台节点的物理内存被耗尽内核开始全局扫杀。两者的共同点是动手杀进程的不是 Kubernetes而是操作系统内核。Evicted则完全是另一回事它是 kubelet 主动驱逐。kubelet 每隔一段时间检查节点上的驱逐信号比如memory.available太低、nodefs.available太低、imagefs.available太低一旦越过阈值它会挑选一批 Pod 杀掉或标记为失败让节点恢复稳定。Event 里会写The node was low on resource: memory或者disk。启动即失败和上面两种也不一样。比如CreateContainerConfigError、CreateContainerError通常是镜像拉取、存储卷挂载、环境变量引用出了问题跟资源配额没有直接关系但很多人排查时会把它们混在一起绕半天弯子。所以排查的第一步永远是先看 Event 和 Pod 的 Last State而不是凭“可能内存不够”就先把 limit 拉高。1.2 从YAML三行文字到内核动作一条完整的资源链路一个最普通的 Deployment 片段长这样resources: requests: cpu: 200m memory: 256Mi limits: cpu: 500m memory: 512Mi这三行写下去之后会依次走过这么一条链路API Server 持久化资源信息跟着 Pod 定义进入 Etcd这只是个“声明”没有任何人执行它。调度器记账Scheduler 在 Filter 阶段会看每个节点的Allocatable然后算出“所有已调度 Pod 的 requests 总和”加上新 Pod 的 requests不能超过节点剩余可分配量。注意它只看 requests完全不看 limits。kubelet 监听与创建Pod 被绑定到某节点后kubelet 从 API Server Watch 到变化调用容器运行时创建容器。运行时设置 cgroup容器运行时比如 containerd在 cgroup 目录里创建对应的控制组并把 limits 数值写进内核文件。CPU 对应cpu.max或cpu.cfs_quota_us内存对应memory.max或memory.limit_in_bytes。内核执行限制进程运行后一旦 CPU 使用超过配额CFS 带宽控制会强制“节流”内存超过上限内核就会回收页面回收不动就 OOM。kubelet 回收状态容器被杀或驱逐后kubelet 更新 Pod 状态Controller 决定是否重启或重新调度。关键结论是requests 是调度器用来签“工位合同”的limits 是内核用来执行“上限刑罚”的两者根本不在同一层生效。这也是为什么很多人把 limit 调得再大调度问题依然解决不了。1.3 为什么“调大 limit”经常解决不了问题结合上面的链路调大 limit 只在一种情况下有效容器确实是死在自己 cgroup 内存上限太低导致的 OOM。但如果问题出在节点物理内存被耗尽或者根本就是磁盘水位触发了驱逐那你改 limit 等于改了个寂寞。还有个更容易忽略的场景内存工作集逼近 limit 时内核会频繁回收 page cache容器响应变慢但还没死。你以为“还没到 limit 所以没问题”其实性能已经劣化到阈值边缘了。比较靠谱的排查顺序是先kubectl describe pod xxx看 Last State 的 Exit Code 和 Message再翻节点监控确认当时的内存水位、磁盘水位和 cgroup 实际用量最后才决定是不是要改 resource 定义。这个顺序看着简单但真实环境里很多人是直接跳到最后一步的。2. requests和limits一个是工位合同一个是天花板限高2.1 request调度器签工位合同的依据request 本质上是在告诉调度器“我这个 Pod 至少要占用这么多资源你把我放到节点上后这块资源别人就不能用了。” 一个节点的可分配资源被所有 Pod 的 requests 总和瓜分不管这些 Pod 实际用没用完。想象一个办公室你给这个项目组划了 10 个工位即使项目组只来了 3 个人剩下的 7 个工位也在合同期内锁着别的团队搬不进来。Pod 的 request 就是这张工位合同。K8S 调度器按“工位数”不按 requests 总和来算剩余容量。所以在资源紧张的集群里把 request 调大会导致调度明显变困难调小调度容易但节点超卖风险上升。很多人只关心 limit 够不够却不知道真正卡调度的是 request。2.2 limitkubelet设置运行上限的依据limit 是运行时上限。kubelet 会要求容器运行时把这个值写进 cgroup内核据此限制进程使用。这跟调度器没有一毛钱关系调度器在过滤节点时根本不看 limit。这意味着你完全可以把 request 设成 100mlimit 设成 8 核。调度器只会按 100m 帮你找位置但实际运行时这个容器可以冲高到 8 核。这种配置是合法的也常用于“允许突发但保底调度”的场景但代价是节点可能被突发流量打穿。反过来你也不能把 limit 设成 100m 但 request 设成 2 核因为调度器会按 2 核记账而内核会把你限制在 100m等于白白浪费调度配额。2.3 CPU可压缩与内存不可压缩两种limit的本质差异CPU 和内存的 limit 行为有着本质区别维度CPU内存资源性质可压缩不可压缩超过 limit 时CPU 节流throttle进程不停止触发内核 OOM进程可能被杀死对同节点影响节流后让出 CPU影响相对可控内存耗尽可能拖垮整台节点关键监控指标container_cpu_cfs_throttled_periodscontainer_memory_working_set_bytes拿生活场景类比CPU limit 像高速公路的限速牌超速不是扣车而是被慢悠悠地压在限速上走内存 limit 像一张信用卡的额度你刷爆了不是被警告而是直接被停卡。这条差异决定了配置策略上的大方向CPU 可以靠 introspection 和扩容扛过去内存问题则必须留足缓冲。CPU limit 设小了你最多看到容器变慢、耗时上升、被 throttled但它不会死内存 limit 设小了那就是一场事故。所以要给内存留的余量通常比 CPU 高一个量级。3. 调度器只读request看清“看得见的账”和“看不见的墙”3.1 节点到底有多少可分Allocatable、预留与驱逐阈值很多人的资源账是这么算的节点 4 核 16G那我放 4 个 1 核 4G 的 Pod 总没问题吧实际一调度就 Pending报错Insufficient cpu或Insufficient memory。原因是节点并不是所有 Capacity 都能分给 Pod。kubelet 在计算节点可分配资源时会扣掉两块系统预留通过--system-reserved和--kube-reserved指定给操作系统守护进程、kubelet、容器运行时预留的份额。驱逐阈值比如memory.available低于 100Mi 时触发驱逐这部分空间也不能算作可分配。Allocatable Capacity - 系统预留 - 驱逐阈值。实际生产环境里很多人没配置--system-reserved。这意味着 kubelet、containerd、sshd 这些进程用的内存没有从任何 Pod 的 request 里扣除它们会和业务 Pod 抢物理内存。结果节点明明“还有空闲”却提前进入 MemoryPressure业务 Pod 莫名被驱逐。这就是“看不见的墙”。3.2 明明节点很闲却调度不上去碎片化的本质另一种常见 Pending 是节点 CPU 实际使用率才 20%但新 Pod 就是放不上去。这不是 Bug而是因为调度器记账用的是 requests 总和不是实时用量。举个直观例子某节点 4 核上面跑着 4 个 Pod每个 requests 都是 900m实际用量都只有 100m。此时节点剩余 request 是 400m实际空闲却接近 3.6 核。你想放一个 requests 为 500m 的新 Pod调度器一看剩余 400m直接拒绝。这就像奶茶店收银抽屉里全是零钱你要给一张 100 元整钞店员就是找不开。每个 Pod 的 request 都是预占用的“整钞”哪怕它实际消费很小别人也借不了这块。要缓解碎片化我能给的思路有四条把长期稳定服务的 request 调到贴近真实用量而不是拍脑袋写大合理使用节点池让规格差异不要过大用 PriorityClass 加抢占高优先级 Pod 实在插不进去时可以挤掉低优先级 Pod接受一定程度的超卖但必须配合监控和驱逐兜底。3.3 亲和性、污点与拓扑分布资源账之外的隐藏成本还有一类 Pending连Insufficient cpu都不是而是调度器在别的维度上卡死。比如节点上有污点而 Pod 没有对应容忍Pod 带了nodeAffinity把可选节点锁死在一小撮机器上开了topologySpreadConstraints要求 Pod 必须打散到不同可用区或节点哪怕某个区资源明明更空。这些约束和资源是两套独立系统但常常叠加在一起。排障的时候如果只盯着资源很容易陷进去。我一般先看kubectl describe pod里的 Events里面会明确写“0/N nodes are available”以及具体原因是Insufficient cpu、node(s) didnt match node selector还是node(s) had untolerated taint。原因不一样解法完全不同。4. 内核如何执行limit从CPU周期到OOM Killer的完整链路4.1 CFS带宽控制100ms周期里的时间片游戏CPU 的 limit 不是“每秒只能用 0.5 秒”这么简单。内核的 CFS 带宽控制把时间切成一段段的周期默认 100ms再给每个容器分配一个配额。比如 limit 为 500m意味着在每个 100ms 周期里容器最多能占用 50ms 的 CPU 时间。如果容器在周期内把配额用完内核会把它“节流”到下一个周期。这相当于一个闸机你不是被禁止前进而是排队到下一轮再走。对延迟敏感的服务来说节流直接表现为 P99 毛刺和超时但 CPU 使用率看起来又没满。判断是否被节流监控指标要盯container_cpu_cfs_throttled_periods和container_cpu_cfs_throttled_periods / container_cpu_cfs_periods。如果节流比例长期高于 10%要么调高 CPU limit要么减少容器内的线程竞争。一个容易踩的坑是Java 或 Go 这类运行时内部有多线程limit2 只是一个周期最多 200ms CPU 时间但它可以在 2 个核上同时跑满 100ms。千万别把进程内的线程数理解成“受 limit 保护”limit 只限制总量不限制并发形态。4.2 内存cgroup上限与OOM评分内存的 limit 写在 cgroup 的memory.max里。内核会持续监控这个 cgroup 的内存使用当接近上限时先尝试回收 page cache 等可回收页面回收完还不够就触发 OOM Killer。OOM Killer 不是随机杀它按照每个进程的oom_score排序分高的先杀。这个分数大致由进程的物理内存占用和 oom_score_adj 共同决定。容器里的 pid 1 如果吃内存最狠通常它自己就是第一个牺牲品。对 Java 应用来说这里有个典型悲剧老版本 JVM 不感知容器 cgroup limit默认堆大小按物理机内存来算结果堆一扩立刻撞 limit进程反复被杀。JDK 8u191 之后默认开启UseContainerSupport但如果你还在用老镜像得显式加-XX:MaxRAMPercentage75.0这类参数。排查内存问题不要只看container_memory_usage_bytes更准的是container_memory_working_set_bytes。它排除了可以被内核回收的 page cache反映的是“真正脏着的、赖在内存里不走的部分”。工作集贴着 limit 走才是崩溃的前兆。4.3 从网上抄limit值为什么容易翻车我在社区里见过太多“通用模板”什么“Java 服务 memory limit 设 2G 准没错”“Go 服务 requests 给 500m 就行”。这些数值对别人可能合理但换一个业务就完全不是一回事。举个实际例子同样是 Nginx 网关静态文件缓存命中率高的工作集可能只有 100Mi但开启了 Lua 脚本和复杂 access log 后工作集能冲到 500Mi。你照着网上模板写 256Mi容器三天两头 OOM写 4G又白白占着节点内存调度器按 request 记账后面全是资源浪费。正确姿势是结合第 6 节说的基线采集法拿自己业务的数据来定。别人的模板只能作为初始参考不能作为长期配置。5. QoS三兄弟与节点驱逐次序危机关头的生存排序5.1 三档QoS怎么判定只看request和limit的设定关系K8S 会根据 Pod 里每个容器的 requests 和 limits 设定盖一个 QoS 章共三档Guaranteed、Burstable、BestEffort。判定规则如下QoS 等级判定条件风险倾向Guaranteed每个容器都设置了 requests 和 limits且同一资源的二者相等被驱逐优先级最低Burstable至少一个容器设置了 requests 或 limits但不满足 Guaranteed 条件常见主形态超用风险中等BestEffort所有容器都没有设置任何 requests 和 limits被驱逐优先级最高注意这个判定是“每个容器”维度的。如果 Pod 里有一个 Java 容器配了 Guaranteed另一个 sidecar 只写了 requests 没写 limits整个 Pod 就会被降级成 Burstable。所以想拿 Guaranteed 的稳定性所有容器都得严格配齐。QoS 最重要的意义是当节点资源告急轮到 kubelet 决定“谁先走”的时候它是分档排序的核心依据之一。但它只是排序规则并不保证 Guaranteed 就一定不死。5.2 驱逐流程先来后到但又不完全是kubelet 的驱逐不是简单的 FIFO。实际排序逻辑大致是按 PriorityClass 排序优先级低的先被驱逐优先级相同时按 QoS 排序BestEffort 先于 BurstableGuaranteed 最后仍然相同时按 Pod 内存使用量相对于 request 的超用比例排序超用比例大的先走。这个排序在 Eviction API 的代码里有明确实现不是玄学。对你的启示是核心在线服务Priority 和 QoS 都要拉满做成 Guaranteed离线任务、Batch Job可以安心用 Burstable 或 BestEffort反正它优先级低牺牲起来不心疼只是 QoS 高但 Priority 低在驱逐场景下依然可能先被杀。一个常被忽略的细节是Pod 被驱逐后状态是FailedEvent 里写Evicted。Pod 对象还在只是容器没了配套的 PVC 可能还占着。做清理的时候别忘了kubectl delete pod把尸骨收走。5.3 Guaranteed不是免死金牌很多人以为 QoS 设成 Guaranteed、Priority 设高Pod 就安全了。这是认知盲区。kubelet 驱逐确实会尽量不碰 Guaranteed但别忘了还有一个独立于 kubelet 的“暴力部门”——内核 OOM Killer。如果你的容器内存工作集超过了 cgroup limit内核会直接杀进程这个时候 QoS 章一点用都没有。QoS 管的是 kubelet 的驱逐顺序OOM Killer 管的是内核的内存战备线两层机制完全平行。另外还有个场景整台节点陷入 MemoryPressurekubelet 的硬驱逐阈值已经触发了但如果根因是节点上的系统进程吃内存就算你把所有业务 Pod 都调成 Guaranteed该被殃及还是会被殃及。所以配好节点级--system-reserved、--kube-reserved比在 Pod 层面死磕 QoS 更治本。6. 从原理到策略一次靠谱的资源清单长什么样6.1 基线采集三步法不看监控改配置等于闭眼开车讲了这么多原理最后落到实操。我自己的习惯是给任何一个服务写 resource 之前先做三天到一周的“基线采集”。第一步部署监控用 Prometheus 抓container_cpu_usage_seconds_total和container_memory_working_set_bytes。注意是 working set不是 raw usage。第二步统计 P95 / P99。requests 建议取 P95 附近再乘 1.1~1.2 的保险系数。为什么不用 P99因为 P99 往往被毛刺抬高翻倍配置会让 request 虚高白白浪费节点容量。如果一个服务的内存工作集长期稳定在 200Mi突然某天 2 分钟冲到 400Mi那可能是 GC 或缓存填充把 limit 留在 512Mi 更合适而不是把 request 改成 400Mi。第三步定 limits。CPU limit 一般是 request 的 1.5~2 倍内存 limit 是 request 的 1.5~2.5 倍具体看业务对延迟敏感度和缓存的突刺程度。拿不准的时候宁可先放宽 limit也不要放窄——内存 limit 设小了是会出人命的CPU throttled 只是变慢而已。6.2 结合HPA/VPA的容量规划requests不再是拍脑袋如果你的服务挂了 HPA那 requests 的准确性就更加关键因为 HPA 的扩缩容计算用的是CPU 当前使用量 / request不是 limit。这句话翻译一下如果 requests 设得过大HPA 永远算不出高利用率结果就是副本数涨不起来流量全压在少数几个实例上如果 requests 设得过小HPA 又会被轻微的 CPU 毛刺刺激得疯狂扩容Pod 数暴增但每个 Pod 都在节流。VPA 能自动推荐 requests但默认只给建议不会改你 YAML。即便你开了 Auto 模式它调整 requests 时也需要重新创建 Pod对在线服务多少有影响。我的建议是把 VPA 当“测量仪器”跑一段时间看它推荐的值跟你手工算的 P95 是否一致再决定要不要采纳别一上来就全自动。6.3 一套可直接套用的初始配置示例最后给一份模板适合中等规模的 web 服务你可以按自己采集到的基线调整apiVersion: apps/v1 kind: Deployment metadata: name: demo-web spec: replicas: 3 strategy: rollingUpdate: maxSurge: 1 maxUnavailable: 0 template: spec: containers: - name: app image: demo:1.0.2 resources: requests: cpu: 200m memory: 256Mi limits: cpu: 500m memory: 512Mi这份配置的设计逻辑是CPU requests 200m 保证调度器给它一个稳定的物理位置CPU limits 500m 给突发留了 2.5 倍空间避免节流严重内存 requests 256Mi 贴近多数 web 服务的空闲工作集内存 limits 512Mi 就是为了给 GC 和高峰留缓冲。同时别忽略滚动更新的资源叠加maxSurge: 1意味着发布瞬间集群里会有 4 个副本同时跑峰值容量大概是4 × 512Mi 2Gi。如果你按“3 副本刚好打满节点”去规划容量发布时就会因为资源不够而卡住滚动更新。这是 Deployment 交付时最常见的隐性故障很多人没把 Surge 算进资源账。我在实际配置中的体会是资源参数没有“一劳永逸”业务流量和镜像版本一变工作集就会跟着漂。最有效的做法是每个月回头看一眼监控问自己三个问题节流比例涨了吗工作集离 limit 多远HPA 扩容频率正常吗如果这三项里有两项不对劲就该重新走一遍基线采集调整 resource 了。这才是让资源模型长期健康运转的方式而不是每次出事都靠临时加 limit 续命。

准备好开启您的数字化征程了吗?

专业团队为您量身定制,让您的企业网站脱颖而出

立即咨询