K8s Lab 把当前仓库文档整理成一个可阅读的网页站点

Repository Reading Site

第十一课:requests / limits、QoS、OOM 与驱逐原理

前一课我们已经讲清了: 但你离“能带生产平台”还差一条特别硬的主线: 这条线要是不通,后面很多现象你都会误解: 1. 明明 `kubectl top node` 看起来还有很多空闲,为什么 Pod 还是 `Pending` 2. 为什么 CPU 超限后 Pod 没死,只是变慢 3. 为什么内存超限后 Pod 会直接 `OOMKilled` 4. 为什么有的

Markdown11-第十一课-requests-limits-QoS-OOM与驱逐原理.md2026年4月10日 04:56

第十一课:requests / limits、QoS、OOM 与驱逐原理

为什么这一课必须接在探针和发布之后

前一课我们已经讲清了:

  • Pod 什么时候算启动完成
  • 什么时候能接流量
  • 什么时候该被重启
  • 什么时候该优雅退出
  • PDB 怎样保护可用副本

但你离“能带生产平台”还差一条特别硬的主线:

Kubernetes 到底怎样理解资源,怎样做资源账本,怎样限制容器,怎样在资源紧张时决定谁先死、谁后死。

这条线要是不通,后面很多现象你都会误解:

  1. 明明 kubectl top node 看起来还有很多空闲,为什么 Pod 还是 Pending
  2. 为什么 CPU 超限后 Pod 没死,只是变慢
  3. 为什么内存超限后 Pod 会直接 OOMKilled
  4. 为什么有的 Pod 比别的 Pod 更容易在压力下被牺牲
  5. 为什么 HPA 一定离不开 requests

第十一课就是把这条资源主线讲透。


先建立一个最关键的认知:Kubernetes 同时维护两套“资源真相”

很多初学者会把资源理解成:

  • 机器上现在用了多少

这只对了一半。

Kubernetes 里至少有两套并行存在的资源视图。

第一套:调度账本

这是 scheduler 看的世界。

它主要看:

  • requests.cpu
  • requests.memory
  • 节点的 allocatable

这套世界回答的问题是:

这个 Pod 理论上能不能被放到某台节点上。

第二套:运行时真实消耗

这是 kubelet、容器运行时、Linux cgroup 看的世界。

它主要看:

  • 容器当前真实 CPU 使用
  • 当前真实内存占用
  • cgroup 配额
  • 节点是否出现压力

这套世界回答的问题是:

容器现在到底吃了多少,是否该被限制、节流、杀掉,或者节点是否该触发压力处理。

如果你把这两套世界混在一起,就会产生最经典的误解:

top 看起来还有资源,为什么还是 Pending?

因为:

  • top 看的是实时使用
  • 调度器看的是 requests 账本

它们不是同一个概念。


requestslimits 到底各自是什么意思

你以后必须能把这四句话脱口而出。

requests

表示:

我向集群申请、并在调度层面希望被保证的最小资源。

它的主要作用有两个:

  1. 调度器据此判断某个节点是否“放得下”
  2. 很多控制器和策略据此计算,如 HPA 的 CPU 利用率

limits

表示:

容器在运行时允许使用的上限。

但 CPU 和 memory 的上限行为完全不同。

CPU 超过 limits

不会直接杀进程。

它会:

  • 通过 cgroup 配额做节流
  • 让进程变慢

Memory 超过 limits

不会温柔地降速。

它通常会:

  • 触发 OOM
  • 进程被杀
  • 容器退出

这就是为什么业内常说:

  • CPU 是可压缩资源
  • Memory 是不可压缩资源

我们先看真实集群的资源现状,不是纸上讲解

我先核对了当前这套集群的真实容量和资源使用情况。

节点容量与可分配量

实际看到:

  • cp-316 CPU / 16270560Ki allocatable memory
  • us59006872805616 CPU / 16270468Ki allocatable memory
  • wk-116 CPU / 16267584Ki allocatable memory
  • hk6526993821214 CPU / 8029536Ki allocatable memory
  • 控制面节点也有资源,但有 taint

实时资源使用

kubectl top nodes 看到的实时使用例如:

  • cp-3 大约 397m CPU / 4142Mi memory
  • us590068728056 大约 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 BestEffort
  • 40 Burstable
  • 0 Guaranteed

这说明什么?

说明这套集群里大量工作负载:

  • 根本没配 requests / limits
  • 或者只配了部分资源

这是很多真实平台都会有的情况。

风险是什么

如果大量业务都是 BestEffort

  • 调度层没有明确资源承诺
  • 节点压力下更容易先被牺牲
  • HPA 也更难基于 CPU request 做正确计算

这也是为什么资源治理不是“优化项”,而是平台成熟度问题。


真实组件里,资源配置是怎么体现出来的

coredns

我检查到:

  • requests:
    • cpu = 100m
    • memory = 70Mi
  • limits:
    • memory = 170Mi
    • 没有 CPU limit

所以它是:

  • Burstable

ingress-nginx-controller

我检查到:

  • requests:
    • cpu = 100m
    • memory = 90Mi
  • 没有配置 limits

所以它也是:

  • Burstable

metrics-server

我检查到:

  • requests:
    • cpu = 100m
    • memory = 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-demo
  • burstable-demo
  • guaranteed-demo

我观察到的真实结果

它们的 qosClass 分别是:

  • besteffort-demo -> BestEffort
  • burstable-demo -> Burstable
  • guaranteed-demo -> Guaranteed

这说明 QoS 不是你手写的字段。

而是 Kubernetes 根据资源声明自动推导出来的分类结果。


为什么 too-large-request 会 Pending,即使节点实时使用看起来不高

我故意创建了一个 Pod:

  • requests.cpu = 20
  • requests.memory = 20Gi

结果它一直是:

  • Pending

事件里写得很明确:

  • 4 Insufficient cpu
  • 4 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-demo
  • cpu-limit-demo

两者都在疯狂空转:

  • while true; do :; done

区别是:

  • cpu-burst-demo 没有 CPU limit
  • cpu-limit-demo 的 CPU request 和 limit 都是 100m

kubectl top 的结果

我看到:

  • cpu-burst-demo 大约 999m
  • cpu-limit-demo 大约 101m

这一步已经很直观地说明:

  • 没 CPU limit 的容器可以向上 burst 到接近 1 核
  • 配了 100m CPU limit 的容器会被限制在大约 0.1 核

但更关键的是底层 cgroup 证据。

cpu-burst-demo 的 cgroup

我在容器内读到:

  • cpu.max = max 100000
  • nr_throttled = 0
  • throttled_usec = 0

这表示:

  • 没有限额
  • 没被节流

cpu-limit-demo 的 cgroup

我在容器内读到:

  • cpu.max = 10000 100000

这可以理解成:

  • 100000us 的周期里
  • 最多只能用 10000us CPU 时间

也就是:

  • 10% 一个 CPU
  • 大约 100m

更关键的是 5 秒前后 cpu.stat 的变化:

  • nr_throttled785 增加到 835
  • throttled_usec64579894 增加到 68686213

这就是铁证。

说明 CPU limit 的本质是:

  • cgroup 配额
  • 超额后节流

不是:

  • 直接杀进程

为什么说 CPU 是“可压缩资源”

因为当 CPU 不够时,内核通常可以做的是:

  • 让你少跑一点
  • 慢一点
  • 被限速

它不需要立刻把你杀死。

所以 CPU 资源更适合:

  • overcommit
  • burst

这也是为什么很多系统:

  • 会给 CPU request
  • 但不一定给很死的 CPU limit

当然,这不是绝对规则。

如果你非常在意:

  • 单租户噪声隔离
  • 防止某个容器抢满节点 CPU

那你仍然会给 CPU limit。

但你要知道其代价:

  • 容器可能被持续节流
  • 延迟会变大

Memory limit 的本质不是节流,而是边界

这次的 oom-demo 非常关键。

它的资源配置是:

  • requests:
    • cpu = 50m
    • memory = 32Mi
  • limits:
    • cpu = 200m
    • memory = 64Mi

容器内部不断申请 1Mi 字符串块。

我看到的结果

日志里一路打印到了:

  • allocated 58 Mi

然后 Pod 进入:

  • CrashLoopBackOff

describe pod 里明确写着:

  • Reason: OOMKilled
  • Exit Code: 137
  • Restart Count: 2

为什么不是刚好到 64Mi 才死

这正是你必须理解的地方。

limits.memory = 64Mi 不代表:

  • 日志能精确打印到 64Mi 那一刻才被杀

因为真实内存使用还包括:

  • Shell 进程本身开销
  • 变量管理开销
  • 运行时额外内存
  • 页表、缓存等底层开销

所以你经常会看到:

  • 日志上的“应用自报已分配内存”小于 limit
  • 但容器已经 OOMKilled

这不是 Kubernetes 错了。

而是你看到的只是应用自己记录的一部分。


Exit Code 137 到底意味着什么

你以后看到 137 要立即警觉。

因为它通常对应:

  • 128 + 9
  • 即进程收到 SIGKILL

在容器资源超限场景里,这往往意味着:

  • OOM 被内核或容器运行时处理掉了

所以:

  • Reason: OOMKilled
  • Exit Code: 137

这两个组合,是排障时非常典型的内存超限信号。


OOMKill 和 Eviction 不是一回事

这一点特别容易混。

OOMKill 是容器级、cgroup 级事件

典型触发条件:

  • 容器超出自己的 limits.memory

典型表现:

  • Pod 还在同一节点
  • 容器重启
  • Reason: OOMKilled
  • Exit Code: 137

Eviction 是节点级压力事件

典型触发条件:

  • 节点整体内存压力
  • 磁盘压力
  • PID 压力

典型表现是 kubelet 站在“整台节点”的角度说:

我必须回收一些 Pod,否则整台机器会出问题。

这时它关注的不是某个容器自己的 limit。

它关注的是:

  • 节点是否进入 MemoryPressure
  • DiskPressure
  • PIDPressure

当前节点压力状态是什么

我检查了:

  • us590068728056
  • cp-3

当前都显示:

  • MemoryPressure = False
  • DiskPressure = False
  • PIDPressure = False
  • Ready = True

这意味着:

  • 当前没有节点级驱逐压力

所以我们这次实验看到的是:

  • 纯粹的容器级 OOMKill

不是:

  • kubelet 因节点压力触发的 eviction

kubelet 真实配置给了我们什么线索

我还 SSH 到节点看了:

  • /var/lib/kubelet/config.yaml

看到了几个关键点:

  • cgroupDriver: systemd
  • memorySwap: {}
  • 没有显式覆盖 evictionHard 这类阈值

这至少说明:

  1. 这套集群的资源控制底层是沿着 systemd cgroup 驱动走的
  2. 没有做显式的 swap 玩法
  3. 驱逐阈值没有在配置文件里做明显定制

所以你看到的资源行为,基本是比较标准的 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 到底怎样做资源管理?

你应该能完整讲出这条链:

  1. requests 决定调度账本和最小承诺。
  2. limits 决定运行时上限。
  3. CPU limit 通过 cgroup 配额实现,超了会节流。
  4. Memory limit 通过内存边界实现,超了会 OOMKill。
  5. QoS 是 Kubernetes 根据 requests / limits 自动推导出的资源保护等级。
  6. Pending 通常是调度账本问题,不一定是实时资源耗尽。
  7. OOMKilled 是容器级问题,Eviction 是节点级问题,两者不能混淆。

如果这条链能讲清楚,你对 Kubernetes 资源模型就不是“会背字段”,而是真懂了。


本课你必须真正背下来的结论

  1. requests 决定调度,limits 决定运行时边界。
  2. 调度器看的是 requests 账本,不是 kubectl top 的瞬时空闲。
  3. CPU 超限会被节流,不会像内存一样直接杀进程。
  4. Memory 超限通常会导致 OOMKilled,典型信号是 Exit Code 137
  5. BestEffort / Burstable / Guaranteed 是资源声明推导出的 QoS,不是手工字段。
  6. Guaranteed 更受保护,但不是绝对不会被处理。
  7. OOMKillEviction 是两条完全不同的机制链。

给你的专家化训练题

  1. 为什么 kubectl top node 看起来空闲,不代表一个大 request 的 Pod 就能被调度成功?
  2. 为什么 cpu-limit-demo 不会被杀,但会看到 nr_throttledthrottled_usec 持续增长?
  3. 为什么 oom-demo 日志只打到 58 Mi 左右,却已经 OOMKilled
  4. 为什么 cpu-burst-demo 能跑到接近 1 core,而它的 request 明明只有 100m
  5. 为什么说 HPA 依赖 requests,而不是依赖 limits?

你把这五题讲明白,这一课才算真正学会。