Repository Reading Site
第十一课:requests / limits、QoS、OOM 与驱逐原理
前一课我们已经讲清了: 但你离“能带生产平台”还差一条特别硬的主线: 这条线要是不通,后面很多现象你都会误解: 1. 明明 `kubectl top node` 看起来还有很多空闲,为什么 Pod 还是 `Pending` 2. 为什么 CPU 超限后 Pod 没死,只是变慢 3. 为什么内存超限后 Pod 会直接 `OOMKilled` 4. 为什么有的
第十一课:requests / limits、QoS、OOM 与驱逐原理
为什么这一课必须接在探针和发布之后
前一课我们已经讲清了:
- Pod 什么时候算启动完成
- 什么时候能接流量
- 什么时候该被重启
- 什么时候该优雅退出
PDB怎样保护可用副本
但你离“能带生产平台”还差一条特别硬的主线:
Kubernetes 到底怎样理解资源,怎样做资源账本,怎样限制容器,怎样在资源紧张时决定谁先死、谁后死。
这条线要是不通,后面很多现象你都会误解:
- 明明
kubectl top node看起来还有很多空闲,为什么 Pod 还是Pending - 为什么 CPU 超限后 Pod 没死,只是变慢
- 为什么内存超限后 Pod 会直接
OOMKilled - 为什么有的 Pod 比别的 Pod 更容易在压力下被牺牲
- 为什么 HPA 一定离不开
requests
第十一课就是把这条资源主线讲透。
先建立一个最关键的认知:Kubernetes 同时维护两套“资源真相”
很多初学者会把资源理解成:
- 机器上现在用了多少
这只对了一半。
Kubernetes 里至少有两套并行存在的资源视图。
第一套:调度账本
这是 scheduler 看的世界。
它主要看:
requests.cpurequests.memory- 节点的
allocatable
这套世界回答的问题是:
这个 Pod 理论上能不能被放到某台节点上。
第二套:运行时真实消耗
这是 kubelet、容器运行时、Linux cgroup 看的世界。
它主要看:
- 容器当前真实 CPU 使用
- 当前真实内存占用
- cgroup 配额
- 节点是否出现压力
这套世界回答的问题是:
容器现在到底吃了多少,是否该被限制、节流、杀掉,或者节点是否该触发压力处理。
如果你把这两套世界混在一起,就会产生最经典的误解:
top 看起来还有资源,为什么还是 Pending?
因为:
- top 看的是实时使用
- 调度器看的是 requests 账本
它们不是同一个概念。
requests 和 limits 到底各自是什么意思
你以后必须能把这四句话脱口而出。
requests
表示:
我向集群申请、并在调度层面希望被保证的最小资源。
它的主要作用有两个:
- 调度器据此判断某个节点是否“放得下”
- 很多控制器和策略据此计算,如 HPA 的 CPU 利用率
limits
表示:
容器在运行时允许使用的上限。
但 CPU 和 memory 的上限行为完全不同。
CPU 超过 limits
不会直接杀进程。
它会:
- 通过 cgroup 配额做节流
- 让进程变慢
Memory 超过 limits
不会温柔地降速。
它通常会:
- 触发 OOM
- 进程被杀
- 容器退出
这就是为什么业内常说:
- CPU 是可压缩资源
- Memory 是不可压缩资源
我们先看真实集群的资源现状,不是纸上讲解
我先核对了当前这套集群的真实容量和资源使用情况。
节点容量与可分配量
实际看到:
cp-3:16 CPU / 16270560Ki allocatable memoryus590068728056:16 CPU / 16270468Ki allocatable memorywk-1:16 CPU / 16267584Ki allocatable memoryhk652699382121:4 CPU / 8029536Ki allocatable memory- 控制面节点也有资源,但有 taint
实时资源使用
kubectl top nodes 看到的实时使用例如:
cp-3大约397m CPU / 4142Mi memoryus590068728056大约213m CPU / 2404Mi memory
这两组数据的意义
你必须区分:
capacity / allocatable是资源上限视图top是某一刻的实时使用视图
调度是否能放下一个 Pod,优先看的是:
- allocatable 减去已承诺 requests 的剩余空间
不是:
- 此刻 top 上到底空了多少
节点上还有第三组很重要的数据:Allocated Resources
我在 us590068728056 节点上看到:
Requests: cpu 2880m (18%), memory 4266Mi (26%)
Limits: cpu 9200m (57%), memory 7392Mi (46%)
而且 kubectl describe node 还明确提示:
Total limits may be over 100 percent, i.e., overcommitted.
这句话很关键。
它说明 Kubernetes 允许:
- limits 总和超过节点物理容量
这就是所谓:
- 超卖
- overcommit
为什么能这么做?
因为:
- 不是所有容器都会同时打满 limit
- CPU 尤其适合超卖
但 requests 这本账不能乱来。
因为 scheduler 正是靠它来做可放置判断。
真实集群里的 QoS 分布,值得你好好警惕
我统计了当前集群在做本课实验前的 QoS 分布:
70 BestEffort40 Burstable0 Guaranteed
这说明什么?
说明这套集群里大量工作负载:
- 根本没配 requests / limits
- 或者只配了部分资源
这是很多真实平台都会有的情况。
风险是什么
如果大量业务都是 BestEffort:
- 调度层没有明确资源承诺
- 节点压力下更容易先被牺牲
- HPA 也更难基于 CPU request 做正确计算
这也是为什么资源治理不是“优化项”,而是平台成熟度问题。
真实组件里,资源配置是怎么体现出来的
coredns
我检查到:
- requests:
cpu = 100mmemory = 70Mi
- limits:
memory = 170Mi- 没有 CPU limit
所以它是:
Burstable
ingress-nginx-controller
我检查到:
- requests:
cpu = 100mmemory = 90Mi
- 没有配置 limits
所以它也是:
Burstable
metrics-server
我检查到:
- requests:
cpu = 100mmemory = 200Mi
- 没有配置 limits
它同样是:
Burstable
为什么很多系统喜欢配成 Burstable
因为这是一种常见折中:
- 给 scheduler 明确 request
- 但不把 CPU 完全卡死
- 允许运行时有一定 burst 空间
这在很多基础设施组件上是合理的。
QoS 不是一个标签游戏,而是资源语义分层
Kubernetes 的 QoS 有三类:
1. BestEffort
条件是:
- 没有任何 container 配 requests / limits
特点:
- 没有调度承诺
- 也没有运行时上限
- 资源压力下风险最大
2. Burstable
条件是:
- 配了部分 requests / limits
- 或者 request 和 limit 不完全相等
特点:
- 有一定资源承诺
- 可以有一定突发
- 是现实中最常见的折中类型
3. Guaranteed
条件是:
- 每个容器都同时声明了 CPU 和 memory 的 request 与 limit
- 且 request = limit
特点:
- 资源边界最明确
- 一般被认为是保护级别最高的 QoS
但你要注意:
Guaranteed不是永远不死金牌。
它只是:
- 在资源管理和压力场景里更受保护
并不意味着:
- 应用自己不会挂
- 节点绝不出问题
- 永远不会被处理
我们这次实验故意做出的三类 QoS 样本
实验目录:
manifests/11-resources
其中最基础的三种 Pod 是:
besteffort-demoburstable-demoguaranteed-demo
我观察到的真实结果
它们的 qosClass 分别是:
besteffort-demo -> BestEffortburstable-demo -> Burstableguaranteed-demo -> Guaranteed
这说明 QoS 不是你手写的字段。
而是 Kubernetes 根据资源声明自动推导出来的分类结果。
为什么 too-large-request 会 Pending,即使节点实时使用看起来不高
我故意创建了一个 Pod:
requests.cpu = 20requests.memory = 20Gi
结果它一直是:
Pending
事件里写得很明确:
4 Insufficient cpu4 Insufficient memory- 控制面节点还有 taint:
untolerated taint {node-role.kubernetes.io/control-plane:}
这一步的本质非常重要
这说明 scheduler 的思路不是:
- “全集群加起来够不够”
而是:
- “有没有某一台允许调度的单节点能完整容纳这个 Pod”
也就是说,资源不是碎片拼图。
一个 Pod 不能:
- 在节点 A 用 10 CPU
- 在节点 B 再借 10 CPU
它必须整体放进某一台节点。
这也是为什么 kubectl top nodes 会骗人
比如你看到:
- 集群总 CPU 似乎很多
但这不代表:
- 某一个 20 CPU 的 Pod 能被调度进去
调度器看的是:
- 单节点可用 request 空间
不是:
- 全集群实时空闲总和
CPU limit 的本质是 cgroup 配额,不是“超了就杀”
我们这次做了两种 CPU 燃烧 Pod:
cpu-burst-democpu-limit-demo
两者都在疯狂空转:
while true; do :; done
区别是:
cpu-burst-demo没有 CPU limitcpu-limit-demo的 CPU request 和 limit 都是100m
kubectl top 的结果
我看到:
cpu-burst-demo大约999mcpu-limit-demo大约101m
这一步已经很直观地说明:
- 没 CPU limit 的容器可以向上 burst 到接近 1 核
- 配了
100mCPU limit 的容器会被限制在大约 0.1 核
但更关键的是底层 cgroup 证据。
cpu-burst-demo 的 cgroup
我在容器内读到:
cpu.max = max 100000nr_throttled = 0throttled_usec = 0
这表示:
- 没有限额
- 没被节流
cpu-limit-demo 的 cgroup
我在容器内读到:
cpu.max = 10000 100000
这可以理解成:
- 每
100000us的周期里 - 最多只能用
10000usCPU 时间
也就是:
- 10% 一个 CPU
- 大约
100m
更关键的是 5 秒前后 cpu.stat 的变化:
nr_throttled从785增加到835throttled_usec从64579894增加到68686213
这就是铁证。
说明 CPU limit 的本质是:
- cgroup 配额
- 超额后节流
不是:
- 直接杀进程
为什么说 CPU 是“可压缩资源”
因为当 CPU 不够时,内核通常可以做的是:
- 让你少跑一点
- 慢一点
- 被限速
它不需要立刻把你杀死。
所以 CPU 资源更适合:
- overcommit
- burst
这也是为什么很多系统:
- 会给 CPU request
- 但不一定给很死的 CPU limit
当然,这不是绝对规则。
如果你非常在意:
- 单租户噪声隔离
- 防止某个容器抢满节点 CPU
那你仍然会给 CPU limit。
但你要知道其代价:
- 容器可能被持续节流
- 延迟会变大
Memory limit 的本质不是节流,而是边界
这次的 oom-demo 非常关键。
它的资源配置是:
- requests:
cpu = 50mmemory = 32Mi
- limits:
cpu = 200mmemory = 64Mi
容器内部不断申请 1Mi 字符串块。
我看到的结果
日志里一路打印到了:
allocated 58 Mi
然后 Pod 进入:
CrashLoopBackOff
describe pod 里明确写着:
Reason: OOMKilledExit Code: 137Restart Count: 2
为什么不是刚好到 64Mi 才死
这正是你必须理解的地方。
limits.memory = 64Mi 不代表:
- 日志能精确打印到 64Mi 那一刻才被杀
因为真实内存使用还包括:
- Shell 进程本身开销
- 变量管理开销
- 运行时额外内存
- 页表、缓存等底层开销
所以你经常会看到:
- 日志上的“应用自报已分配内存”小于 limit
- 但容器已经 OOMKilled
这不是 Kubernetes 错了。
而是你看到的只是应用自己记录的一部分。
Exit Code 137 到底意味着什么
你以后看到 137 要立即警觉。
因为它通常对应:
128 + 9- 即进程收到
SIGKILL
在容器资源超限场景里,这往往意味着:
- OOM 被内核或容器运行时处理掉了
所以:
Reason: OOMKilledExit Code: 137
这两个组合,是排障时非常典型的内存超限信号。
OOMKill 和 Eviction 不是一回事
这一点特别容易混。
OOMKill 是容器级、cgroup 级事件
典型触发条件:
- 容器超出自己的
limits.memory
典型表现:
- Pod 还在同一节点
- 容器重启
Reason: OOMKilledExit Code: 137
Eviction 是节点级压力事件
典型触发条件:
- 节点整体内存压力
- 磁盘压力
- PID 压力
典型表现是 kubelet 站在“整台节点”的角度说:
我必须回收一些 Pod,否则整台机器会出问题。
这时它关注的不是某个容器自己的 limit。
它关注的是:
- 节点是否进入
MemoryPressure DiskPressurePIDPressure
当前节点压力状态是什么
我检查了:
us590068728056cp-3
当前都显示:
MemoryPressure = FalseDiskPressure = FalsePIDPressure = FalseReady = True
这意味着:
- 当前没有节点级驱逐压力
所以我们这次实验看到的是:
- 纯粹的容器级 OOMKill
不是:
- kubelet 因节点压力触发的 eviction
kubelet 真实配置给了我们什么线索
我还 SSH 到节点看了:
/var/lib/kubelet/config.yaml
看到了几个关键点:
cgroupDriver: systemdmemorySwap: {}- 没有显式覆盖
evictionHard这类阈值
这至少说明:
- 这套集群的资源控制底层是沿着 systemd cgroup 驱动走的
- 没有做显式的 swap 玩法
- 驱逐阈值没有在配置文件里做明显定制
所以你看到的资源行为,基本是比较标准的 kubelet + cgroup 资源管理模型。
QoS 到底怎样影响“谁更容易死”
这里必须讲得谨慎一点。
先说结论
一般来说:
BestEffort风险最高Burstable居中Guaranteed最受保护
但这不是绝对免死顺序。
为什么不是绝对的
因为真实的 OOM 与 eviction 决策还会受到:
- 当前真实使用量
- 是否超过 requests
- Pod priority
- 节点整体压力
等因素影响。
所以更准确的说法是:
QoS 影响资源压力下的保护等级,但不是唯一变量。
你该怎么理解它
BestEffort:没有明确承诺,平台最难替你兜底Burstable:有承诺,但允许突发,是真实生产最常见类型Guaranteed:边界最明确,保护级别最高,但资源利用率可能更保守
为什么 HPA 一定离不开 requests
虽然这节课我们还没正式展开 HPA,但必须先把前置原理说清楚。
HPA 基于 CPU 做自动伸缩时,核心不是看:
- 你用了多少 m CPU
而是看:
- 当前使用量相对于 request 的比例
举例:
- request =
100m - 当前使用 =
200m
那就是:
- 200%
如果你根本没写 request:
- HPA 就没有一个稳定的分母
这也是为什么资源声明不只是“防止 OOM”,它还是 autoscaling 的数学基础。
作为架构师,你必须能讲清楚的完整链路
以后如果有人问你:
Kubernetes 到底怎样做资源管理?
你应该能完整讲出这条链:
requests决定调度账本和最小承诺。limits决定运行时上限。- CPU limit 通过 cgroup 配额实现,超了会节流。
- Memory limit 通过内存边界实现,超了会 OOMKill。
QoS是 Kubernetes 根据 requests / limits 自动推导出的资源保护等级。Pending通常是调度账本问题,不一定是实时资源耗尽。OOMKilled是容器级问题,Eviction是节点级问题,两者不能混淆。
如果这条链能讲清楚,你对 Kubernetes 资源模型就不是“会背字段”,而是真懂了。
本课你必须真正背下来的结论
requests决定调度,limits决定运行时边界。- 调度器看的是 requests 账本,不是
kubectl top的瞬时空闲。 - CPU 超限会被节流,不会像内存一样直接杀进程。
- Memory 超限通常会导致
OOMKilled,典型信号是Exit Code 137。 BestEffort / Burstable / Guaranteed是资源声明推导出的 QoS,不是手工字段。Guaranteed更受保护,但不是绝对不会被处理。OOMKill和Eviction是两条完全不同的机制链。
给你的专家化训练题
- 为什么
kubectl top node看起来空闲,不代表一个大 request 的 Pod 就能被调度成功? - 为什么
cpu-limit-demo不会被杀,但会看到nr_throttled和throttled_usec持续增长? - 为什么
oom-demo日志只打到58 Mi左右,却已经OOMKilled? - 为什么
cpu-burst-demo能跑到接近1 core,而它的 request 明明只有100m? - 为什么说 HPA 依赖 requests,而不是依赖 limits?
你把这五题讲明白,这一课才算真正学会。