我为了把节点塞得更满、提高部署密度,把 K8s 里 Pod 的内存 requests 按"平时用得很少"往低了配、想着这样一个节点能多跑好几个 Pod 省机器,结果平时风平浪静,一到业务高峰几个 Pod 同时上量,节点内存被打爆、我的服务 Pod 莫名其妙被打上 Evicted 状态杀掉重新调度,排查很久才明白 requests 是调度器分配资源的依据、我把它谎报低了等于骗调度器把节点超卖了的深度复盘
这次踩的坑,事后想想特别值得说道——因为我当时的出发点是"省资源、提效率",动机听起来无比正确,可正是这个"正确"的动作,亲手给生产环境埋了颗高峰期才会引爆的雷。
故障现场:平时好好的,一到高峰 Pod 就被 Evicted
我们的服务跑在 Kubernetes 上。为了让集群利用率高一点、少买几台机器,我在配 Pod 资源时动了个"聪明"的念头:看监控发现这些服务平时内存占用都不高,于是把 resources.requests.memory 按平时的低水位往小了写,想着 requests 写小了、调度器就能在一个节点上多塞几个 Pod,部署密度上去了,机器就省了。上线后一段时间风平浪静,我还挺得意。
可好景不长,问题在每天的业务高峰期准时上演:
- Pod 莫名被 Evicted:高峰一来,我的服务 Pod 就时不时被打上
Evicted状态、被杀掉然后重新调度到别的节点,有的甚至刚调度过去又被驱逐,反复横跳。 - 报错是节点内存压力:
kubectl describe pod看到驱逐原因是The node was low on resource: memory,节点内存不够了。 - 可按 requests 算节点根本没满:我去看节点的
Allocated resources,内存 requests 加起来才用了六七成,明明还有余量,怎么就内存压力了? - 被驱逐的总是我的服务:同一个节点上,有些 Pod 稳如泰山从不被驱逐,被赶来赶去的总是我那几个"省着配"的服务。
- 低峰期一切正常:过了高峰,节点又风平浪静,Pod 也不再被驱逐,仿佛什么都没发生。
最让我费解的就是那个矛盾:节点的 requests 明明没分配满,为什么会内存压力、还驱逐我的 Pod?这说明 Kubernetes 判断"节点装不装得下"和判断"节点是不是真的内存不够了",用的根本不是同一个东西。我得搞清楚 requests 到底是干嘛的、它和节点实际内存用量是什么关系。
第一件事:搞懂 requests 是调度的依据,而不是运行时的实际用量
带着这个矛盾去翻 Kubernetes 资源模型,我才算真正搞懂了 requests 和 limits 这对天天写、却一直没深究的字段,到底各自管什么——而这正是我栽跟头的根源。
一个容器的资源配置里有两个关键值:
- requests(请求量):这是调度器(scheduler)用来做调度决策的依据。调度器在决定把一个 Pod 放到哪个节点时,看的是这个节点上所有 Pod 的 requests 之和,加上新 Pod 的 requests,会不会超过节点的可分配资源。它本质是你向集群"申报"的、这个容器"需要预留多少"资源。
- limits(限制量):这是容器运行时能用的资源上限。容器实际能用多少由它封顶——内存用超了 limits 会被 OOMKill,CPU 用超了会被限流(throttle)。
关键的认知差就在这里:requests 决定的是"调度时给你预留、占位多少",而不是"你运行时实际会用多少";调度器只按 requests 之和来判断一个节点还能不能再塞 Pod,它既不看 limits、也不看容器实际用了多少。换句话说,requests 是我对集群许下的一个"承诺/申报"——"我这个容器,你给我按这么多预留就够了"。
现在,我那个"聪明"操作的后果就彻底清晰了。我把 requests.memory 按平时低水位往小了配,等于向调度器谎报了一个远低于真实需求的申报值。调度器信了我的话,以为每个 Pod 只需要那么一点点内存,于是在一个节点上塞进了远超它实际承受能力的 Pod——这就是超卖(overcommit):按 requests 算节点还很空(所以 Allocated 显示才六七成),但这些 Pod 真实的内存需求之和早已超过了节点的物理内存。
平时低峰,大家都只用一点点,相安无事;可一到高峰,所有 Pod 同时上量、用到各自的真实内存需求,加起来瞬间超过节点物理内存,节点真的没内存了——于是 kubelet 检测到节点内存压力,启动节点级驱逐(eviction)来自保。我先用命令把这个"申报值 vs 实际用量"的鸿沟看了个清楚:
# 看节点按 requests 算的"已分配"——这是调度器的视角(会显示没满)
kubectl describe node <node> | grep -A8 "Allocated resources"
# Resource Requests Limits
# memory 11Gi (68%) 28Gi (175%) <- requests 才 68%, 但 limits 已 175% 严重超卖!
# 看节点"实际"内存用量——这是物理现实的视角(高峰会爆)
kubectl top node <node>
# NAME MEMORY(bytes) MEMORY%
# node-1 15200Mi 94% <- 实际已 94%, 和 requests 的 68% 完全是两码事
# 看被驱逐 Pod 的真凭实据
kubectl describe pod <pod> | grep -A3 -i evict
# Status: Evicted
# Reason: The node was low on resource: memory.
数据一摆出来,真相再清楚不过:调度器视角(requests 之和)的 68% 和物理现实视角(实际用量)的 94%,差的那一大截,正是我把 requests 谎报低了、骗调度器超卖出来的窟窿。平时这个窟窿被低负载盖着看不出来,高峰负载一压上来,它就塌了——而塌下来砸到的,恰恰是我这种"requests 配得低"的 Pod。至于"为什么总是我的 Pod 被驱逐",答案藏在 QoS 等级里,这是后面要细说的。
第二件事:正解——按真实用量诚实申报 requests,并理解 QoS 决定谁先被驱逐
根因是"谎报 requests 导致超卖",那正解的核心就一句话:requests 要按容器的真实需求(尤其是高峰需求)诚实申报,而不是按低峰水位往低了配来骗密度。具体我做了这么几件事:
# 正解:requests 按真实需求(覆盖高峰)诚实申报
# 内存这种不可压缩资源, requests 和 limits 建议设成相等(Guaranteed 等级最稳)
resources:
requests:
memory: "2Gi" # 按压测/监控得到的高峰真实用量申报, 别再谎报低值
cpu: "500m"
limits:
memory: "2Gi" # 内存 requests=limits → Guaranteed, 最后才被驱逐
cpu: "1000m" # CPU 可压缩, limits 可略高于 requests 允许突发
这里有个关键点容易被忽略:requests 该设多少,不能拍脑袋,要靠数据。我的做法是先用监控(kubectl top、Prometheus)看这个服务在真实高峰下的资源用量,或者做一轮压测看上量后的峰值,再据此设 requests——让申报值覆盖真实的高峰需求,而不是平时的低水位。这样调度器按真实需求预留,就不会再把节点超卖到高峰扛不住。
然后是那个"为什么总是我的 Pod 被驱逐"的谜底——QoS(服务质量等级)。Kubernetes 按 requests/limits 的配置,把 Pod 分成三个等级,节点内存不足要驱逐时,按等级从低到高的顺序开刀:
- Guaranteed(最高):每个容器的 memory/cpu 都设了 requests 且 requests == limits。最后才被驱逐,最稳。
- Burstable(中):设了 requests 但 requests < limits(或只设了部分)。节点内存压力时,实际用量超出自己 requests 越多的,越先被驱逐。
- BestEffort(最低):requests 和 limits 都没设。最先被驱逐,毫无保障。
我之前 requests 配得低、limits 配得高,Pod 落在了 Burstable,而且高峰时实际用量远超我申报的那点 requests——正好撞在 Burstable 驱逐规则的枪口上(超出 requests 越多越先死)。把 requests 按真实需求设、并让内存 requests==limits 升到 Guaranteed,我的关键服务就从"高峰必被驱逐"变成了"最后才被动它"。诚实申报 requests,既治好了超卖,又顺带把 QoS 提了上去,一举两得。
第三件事:同一类"谎报申报值骗过规划者、真实负载下崩盘"的坑,我后来又撞见好几个
这次踩坑让我看清了一个更普遍的模式:很多系统会让你申报一个值,它据此提前规划/分配资源;如果你为了眼前的好处谎报这个值,规划就会失真,平时看不出来,一到真实负载就崩。这种坑到处都是:
- 连接池/线程池大小配过小:为了"省"把池子配得很小,平时够用,高峰并发一上来,大量请求排队等不到资源、超时雪崩。
- JVM 堆 -Xmx 设得超过容器 limits:向 JVM 申报的堆比容器实际给的还大,JVM 以为有那么多内存放心用,结果容器层先 OOMKill。
- 磁盘/配额申请按低值报:为了过审把存储配额报小,上线后数据一涨就写满,触发只读或服务异常。
- 限流阈值按理想值配:把下游能承受的 QPS 申报得过高,真打到那个量下游就被压垮。
- 容量评估按平均值而非峰值:做容量规划时用平均负载而非峰值负载估算,结果峰值一来资源全线告急。
它们的内核是同一个:当一个系统依据你的"申报值"来做资源规划时,这个申报值就不是一个随便填的数字,而是一份承诺——系统会信任它、据它分配、并在它之上叠加别的东西;你为了眼前好处(省机器、过审、提密度)往低/往高了谎报,等于让整个规划建立在一个失真的前提上,平时低负载时这个谎言被掩盖,一旦真实负载(尤其是峰值)到来,失真就会以驱逐、OOM、雪崩、写满的形式集中爆发。所以申报值必须诚实、且按真实的峰值需求来定,这不是抠门省下来的,而是为真实负载预留的安全垫。我把这套判断画成了一张图(见后文)。
| 场景 | 谎报的申报值 | 真实负载下的崩盘方式 |
|---|---|---|
| K8s Pod requests | 按低峰往低配 | 节点超卖、高峰驱逐 Evicted |
| 连接池/线程池 | 为省往小配 | 高峰排队、超时雪崩 |
| JVM -Xmx | 报得比容器 limits 大 | 容器层 OOMKill |
| 存储配额 | 为过审往小报 | 数据涨满、只读/异常 |
| 限流阈值 | 按理想往高配 | 下游被打垮 |
第四件事:requests vs limits vs 实际用量——一张对照表
这次事故逼我把 K8s 资源模型里几个老混淆的概念彻底摆清楚。它们分属不同视角、各管各的事:
| 维度 | requests(请求) | limits(限制) | 实际用量 |
|---|---|---|---|
| 是什么 | 向集群申报的预留量 | 运行时能用的上限 | 容器真正用了多少 |
| 谁看它 | 调度器(决定放哪个节点) | kubelet/cgroup(运行时封顶) | 监控/物理现实 |
| 影响什么 | 调度、节点占位、QoS、驱逐顺序 | OOMKill、CPU 限流 | 节点是否真的内存压力 |
| 设太低的后果 | 超卖、高峰被驱逐 | 容易被 OOMKill/限流 | — |
| 调度器看不看 | 看(唯一依据) | 不看 | 不看 |
看清这张表,我的错就一目了然:我以为把 requests 配低只是"少占点位"无伤大雅,却没意识到 requests 是调度器唯一的判断依据——它不看 limits、不看实际用量,只信 requests;我谎报低了 requests,就等于让调度器在一个失真的前提下做了所有调度决策,把节点超卖了。requests 不是"随便占个位",它是调度和驱逐的命脉。
第五件事:我曾经对 K8s 资源配置想当然的几个误区
这场驱逐风波,也把我对 requests/limits 的一堆想当然照得清清楚楚:
| 我以为 | 实际上 |
|---|---|
| requests 配低只是少占点位、没坏处 | 它是调度依据、配低=骗调度器超卖节点 |
| 调度器会看实际用量决定放哪 | 调度器只看 requests 之和、不看实际用量 |
| 节点 requests 没满就一定安全 | 实际用量可能早超物理内存(超卖) |
| requests 按平时低水位配最省 | 要按高峰真实需求配、否则高峰崩 |
| Pod 被驱逐是随机的运气不好 | 按 QoS 等级有序驱逐、配置低的先死 |
| 只设 limits 不设 requests 也行 | 那是 BestEffort、最先被驱逐毫无保障 |
这些误区的根子是同一个:我把 requests 当成了一个无关紧要、可以为了省资源随便往低填的数字,完全没意识到它是调度器据以规划整个节点资源分配的、被系统当真的"申报/承诺"。正因为我没把它当成一份会被系统信任、并据此分配的承诺,才会心安理得地谎报它、以为占了便宜,直到高峰用真实负载把这份谎报的账一次性算清。把一个"会被系统当真、并据以做规划"的申报值,当成一个"无所谓、可随意填"的数字,是这类超卖崩盘的共同根源。
第六件事:配 K8s 资源、排查 Pod 被驱逐时,我现在的自检习惯
现在每当我配 Pod 资源、或排查"Pod 莫名被 Evicted / OOMKilled",我都会先按 QoS 和驱逐这条线问自己。先看清节点内存压力时的驱逐顺序:
然后按这张自检图决定 requests/limits 怎么配:
配套地,我还在命名空间层面加了护栏,防止再有人(包括我自己)随手谎报或漏配 requests:
# LimitRange: 给没配资源的容器兜底默认值, 并限制 requests/limits 的范围
apiVersion: v1
kind: LimitRange
metadata:
name: mem-defaults
spec:
limits:
- type: Container
default: # 没写 limits 时的默认
memory: "1Gi"
defaultRequest: # 没写 requests 时的默认(避免 BestEffort)
memory: "512Mi"
max: # 单容器 requests/limits 上限
memory: "4Gi"
排查一个已经在被驱逐的 Pod 时,我固定先确认它的 QoS 等级和真实用量:
# 看 Pod 的 QoS 等级(BestEffort/Burstable/Guaranteed)
kubectl get pod <pod> -o jsonpath='{.status.qosClass}'
# 对比它申报的 requests 和它高峰真实用量, 看是不是谎报太低
kubectl get pod <pod> -o jsonpath='{.spec.containers[*].resources}'
kubectl top pod <pod> --containers
# 若实际用量 >> requests, 就是申报太低, 调高 requests 按真实需求来
这套习惯的精髓,是"requests 按真实高峰需求诚实申报、内存 requests=limits 升 Guaranteed、用 LimitRange 兜底、被驱逐先看 QoS 和实际用量对比 requests"。它让我从"requests 随便往低填省密度",变成了"按真实需求诚实申报、把它当一份会被系统当真的承诺"——核心始终是:Kubernetes 里 requests 和 limits 是两个作用完全不同的值,requests(请求量)是调度器做调度决策的唯一依据,调度器把 Pod 放到哪个节点,看的是该节点上所有 Pod 的 requests 之和加上新 Pod 的 requests 是否超过节点可分配资源,它既不看 limits 也不看容器的实际用量;limits(限制量)是容器运行时的资源上限,由 kubelet/cgroup 在运行时强制执行,内存超 limits 被 OOMKill、CPU 超 limits 被限流;所以把 requests 按平时低水位往低了配(为了提高部署密度省机器)等于向调度器谎报了一个远低于真实需求的申报值,调度器据此把远超节点实际承受能力的 Pod 塞进同一节点造成超卖,平时低负载相安无事,一到高峰所有 Pod 同时用到各自真实需求、加起来超过节点物理内存、节点内存压力触发 kubelet 节点级驱逐 eviction;而驱逐按 QoS 服务质量等级从低到高进行——BestEffort(没设 requests 和 limits)最先被驱逐、Burstable(设了 requests 但 requests 小于 limits)其次且实际用量超出自己 requests 越多越先被驱逐、Guaranteed(每个容器 requests 等于 limits)最后才被驱逐;所以正解是 requests 必须按容器的真实需求尤其是高峰需求(通过监控或压测得到)诚实申报而不是按低峰水位谎报来骗密度、内存这种不可压缩资源建议把 requests 设成等于 limits 以获得 Guaranteed 最高等级最稳、并用 LimitRange/ResourceQuota 在命名空间层面兜底默认值和限制范围防止漏配或乱配;更一般地,凡是系统让你申报一个值并据此提前规划分配资源的场景(K8s requests、连接池线程池大小、JVM 堆 Xmx、存储配额、限流阈值、容量评估),这个申报值都不是一个可随意填的数字而是一份会被系统信任并据以分配规划的承诺,为了眼前好处谎报它会让整个规划建立在失真前提上、平时被低负载掩盖、一到真实峰值负载就以驱逐 OOM 雪崩写满等形式集中爆发,所以申报值必须诚实且按真实的峰值需求来定,这不是抠门省下来的而是为真实负载预留的必要安全垫。
我立下的几条规矩
这场"高峰 Pod 必被驱逐"的事故,换来了我配 K8s 资源时,刻进骨子里的几条铁律:
- requests 是调度器的唯一依据,它不看 limits、不看实际用量。
- requests 按低峰谎报 = 骗调度器超卖节点,高峰必崩。
- requests 按真实高峰需求(监控/压测得来)诚实申报。
- 内存这种不可压缩资源,requests=limits 升到 Guaranteed 最稳。
- 驱逐按 QoS 从低到高:BestEffort→Burstable→Guaranteed。
- 用 LimitRange/ResourceQuota 兜底,防漏配 BestEffort 和乱配。
- 通用:会被系统当真的申报值,要诚实按真实峰值需求来定。
附:一段可直接照抄的 requests 校准与超卖排查清单
最后留一段我自己校准 requests、排查节点超卖与驱逐时照着跑的命令清单:
# === 1. 看节点"调度视角"(requests 之和)和"物理现实"(实际用量)的差距 ===
# 这两个数差得越大, 超卖越严重
kubectl describe node | grep -A8 "Allocated resources" # requests/limits 占比
kubectl top node # 实际用量(高峰时跑)
# === 2. 揪出谎报 requests 的 Pod:实际用量远超 requests 的 ===
# 对比每个 Pod 的 requests 和它的真实用量, 实际 >> requests 就是申报太低
kubectl top pod -A --containers | sort -k4 -h # 按内存实际用量排序
kubectl get pod -o jsonpath='{.spec.containers[*].resources.requests}'
# === 3. 看每个 Pod 的 QoS 等级, BestEffort/低 requests 的 Burstable 是驱逐高危 ===
kubectl get pods -A -o custom-columns=\
'NS:.metadata.namespace,NAME:.metadata.name,QOS:.status.qosClass'
# === 4. 查最近发生过的驱逐事件, 确认是不是内存压力驱逐 ===
kubectl get events -A --field-selector reason=Evicted
kubectl describe node | grep -iA5 "MemoryPressure"
# === 5. 校准: 按高峰真实用量设 requests, 内存令 requests=limits 升 Guaranteed ===
# 改完滚动更新, 再在下一个高峰盯第 1、4 步确认不再驱逐
这段清单的顺序就是结论本身:先用"调度视角 vs 物理现实"的差距坐实超卖 → 揪出实际用量远超 requests 的谎报 Pod → 看 QoS 等级定位驱逐高危 → 按高峰真实用量重设 requests(内存 requests=limits 升 Guaranteed)。把"凭感觉往低配"换成"按数据诚实申报",高峰那批 Evicted 就再也不会找上门了。
写在最后
回头看,这场由"requests 谎报太低"引发的"高峰驱逐"事故,真正教给我的,远不止"把 requests 调高、设成等于 limits"这一个技巧。它让我对"当一个系统让你申报一个值、并据此为你规划和分配资源时,这个申报值就不再是一个可以随便填的数字,而是一份会被当真的承诺",有了一次刻骨的体会。我栽跟头,是因为我把 requests 当成了一个"无关紧要、为了省资源可以随意往低填"的数字,完全没意识到它是调度器据以规划整个节点资源分配的命脉;我以为我只是"少占了点位、提高了密度",占了个聪明的便宜,殊不知我是在对集群撒谎——我告诉调度器"我只需要这么一点",于是它信了我,把节点排得满满当当;这个谎言在平时低负载下被掩盖得严严实实,直到高峰用真实的负载,把我谎报的那笔账,连本带利、以 Pod 被驱逐的形式,一次性算了个清楚。这让我领悟到一个关于"申报与规划"的深刻认知:在任何一个需要"预先规划"的系统里,规划者(调度器、容量规划、资源分配)都没法直接知道每个部分真实需要多少,它只能依赖各部分自己申报的需求来做决策;于是每一个申报值,都成了规划的地基——规划的合理性,完全建立在这些申报值的真实性之上;当你为了局部的、眼前的好处(省一台机器、提一点密度、过一次审批)而谎报自己的申报值,你不是在占系统的便宜,而是在污染整个规划的地基:系统会信任你的谎言、据它分配、并在它之上叠加别人的需求,于是一个失真的申报值,会让整个规划在不知不觉中变得脆弱而过载;而这种脆弱平时是隐形的——低负载像一层厚厚的缓冲,把所有谎报的窟窿都盖住了,让一切看起来岁月静好;可一旦真实的峰值负载到来,缓冲被榨干,所有被谎报掩盖的过载会同时显形,以驱逐、OOM、雪崩、写满的方式集中爆发,而且往往专挑那个谎报得最厉害的部分开刀。这给了我一种看待"一切'需要我申报一个值、系统据此规划'之事"时的清醒与诚实:每当我要填一个会被系统用来做资源规划的值(requests、池大小、配额、堆大小、限流阈值),我都提醒自己"这不是一个可以为了省而往低填的数字,这是我对系统许下的、会被当真的承诺;它必须诚实,而且要按真实的峰值需求来定,而不是按平均、按低峰、按我希望的样子"——诚实申报、按真实峰值预留,别用谎报的低值去骗一时的密度;"把申报值当承诺、按真实峰值诚实申报",是配好 K8s 资源、也是做好一切容量与资源规划的关键。认清 requests 是调度依据、谎报低就是超卖、要按真实高峰诚实申报、QoS 决定驱逐顺序——这,是我用一次"一到高峰我的 Pod 就被赶得满集群跑"的事故,换来的、关于 Kubernetes、也关于如何在系统里诚实地申报与规划的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次填 requests 时,先想一句"这是承诺不是占位,该按真实高峰来,而不是按平时省着配",那我对着那一串 Evicted 状态熬的那些高峰夜,就值了。
—— 别看了 · 2026