Kubernetes 基础体系 · 第 21/83 篇。示例基于 Kubernetes 当前稳定 API;弃用、版本偏差、云厂商差异和生产风险会明确说明。

Kubernetes Resource 与 QoS:Request、Limit、CPU Throttle、OOM 和 Eviction

在 Kubernetes 中,容器资源配置同时影响四件事:

  1. Scheduler 是否允许 Pod 进入某个节点
  2. 节点上的运行时是否限制容器使用 CPU 或内存
  3. 节点发生资源压力时,哪些 Pod 更可能被驱逐
  4. 容器被内核 OOM Kill、被 kubelet 驱逐或仅仅被 CPU 限速时,故障表现有何不同

requestslimits、QoS、CPU throttle、OOM 和 eviction 不是互相独立的配置项。它们分别位于调度、Linux cgroup 运行时控制、内核内存回收以及 kubelet 节点治理等不同层次。要正确诊断问题,必须先区分这些层次。


一、先建立资源模型:谁读取 Request,谁执行 Limit

一个 Pod 的资源配置通常写在容器上:

apiVersion: v1
kind: Pod
metadata:
  name: resource-demo
spec:
  containers:
    - name: app
      image: nginx:1.27
      resources:
        requests:
          cpu: "250m"
          memory: "128Mi"
        limits:
          cpu: "500m"
          memory: "256Mi"

这里有四个独立的数值:

  • cpu request:调度器为该容器计算所需 CPU 的依据;
  • memory request:调度器为该容器计算所需内存的依据;
  • cpu limit:运行时允许该容器使用的 CPU 上限;
  • memory limit:运行时允许该容器使用的内存上限。

它们的关系可以概括为:

Pod 提交
  │
  ├── API Server / Admission:默认值、校验、配额
  │
  ├── Scheduler:主要使用 requests 选择节点
  │
  └── Kubelet + 容器运行时 + Linux cgroup
          ├── 执行 CPU limit
          ├── 执行 memory limit
          └── 监控节点压力并执行 eviction

因此,以下两个结论都不正确:

  • “设置了 request,容器就能稳定获得这部分内存或 CPU。”
  • “设置了 limit,调度器就一定为容器预留了这部分资源。”

更准确的表述是:

request 主要是调度和资源计算的声明;limit 主要是运行时的使用上限。二者都不是对物理资源进行绝对、无条件预留的通用语义。


二、CPU 和内存资源的单位

2.1 CPU:核心数和 millicore

CPU 的资源单位是 CPU 核心数:

requests:
  cpu: "1"

表示请求一个逻辑 CPU 单位。也可以用 millicore:

requests:
  cpu: "250m"

其中:

1 CPU = 1000m
250m = 0.25 CPU

250m 不是“固定绑定四分之一颗 CPU”。在多核节点上,它通常表示容器在调度和 CPU 时间分配中拥有相当于 0.25 CPU 的资源权重或上限,而不是绑定到某个物理核上。

CPU request 在 Linux cgroup 中通常还会影响 CPU 权重:当多个容器竞争 CPU 时,请求较大的容器通常获得更高的相对权重。但这属于运行时和 cgroup 实现提供的资源分配机制,不等于实时预留,也不保证业务延迟。

2.2 内存:字节数量,而不是 CPU 时间

内存可以使用 MiGi 等二进制单位:

memory: "256Mi"

也可以使用 MG 等十进制单位:

1 Mi = 2^20 bytes
1 M  = 10^6 bytes

256Mi256M 并不相同。

内存 request 和 limit 通常包含进程匿名内存、文件页缓存以及由 cgroup 计量的其他内存类型。具体统计范围会受 Linux 内核、cgroup v1/v2、容器运行时和 Kubernetes 版本影响,因此不能简单把 ps 输出的 RSS 当成 Kubernetes 计量值。

例如,容器使用的 emptyDir 如果介质是内存:

volumes:
  - name: cache
    emptyDir:
      medium: Memory

这部分数据会占用节点内存,并通常计入 Pod 的内存压力;它不是“免费缓存”。

2.3 其他资源

Kubernetes 还支持:

  • ephemeral-storage:容器可写层和临时卷等本地临时存储;
  • 扩展资源:例如 GPU;
  • HugePages 等特殊资源。

这些资源不完全遵循 CPU 和内存的运行时行为。例如 GPU 通常是不可超卖的整数资源,不能把 GPU 当成可通过 CPU throttle 方式共享的资源。本文重点讨论 CPU、内存和与驱逐相关的临时存储。


三、Request 如何影响调度

3.1 节点是否可调度,首先看 Pod 的 Request

设节点对某资源的可分配容量为:

ArA_r

节点上已经调度的 Pod 对该资源的 request 总和为:

pNRp,r\sum_{p \in N} R_{p,r}

新 Pod 的 request 为:

Rnew,rR_{new,r}

调度器在资源过滤阶段通常要求:

pNRp,r+Rnew,rAr\sum_{p \in N} R_{p,r} + R_{new,r} \leq A_r

这个判断是“声明式容量计算”,不是实时读取节点上当前进程实际使用量。

例如,某节点的可分配 CPU 为 4 核,已经有两个 Pod:

Pod CPU request 当前实际使用
A 2 CPU 0.5 CPU
B 1.5 CPU 0.3 CPU

虽然当前实际使用只有 0.8 CPU,但调度器已经认为该节点有:

2+1.5=3.5 CPU2 + 1.5 = 3.5 \text{ CPU}

被 request 占用,因此最多只能再调度 request 不超过 0.5 CPU 的 Pod。一个 request 为 1 CPU 的新 Pod 可能因为 Insufficient cpu 无法调度,即使节点监控看起来非常空闲。

反过来,如果现有 Pod 的 request 总和只有 2 CPU,但它们的实际使用或 limit 总和可以达到 6 CPU,而节点只有 4 CPU,调度器仍可能接受它们。这就是资源超卖的基础。

3.2 节点可分配容量不是硬件总容量

调度器使用的通常是 Node 的 allocatable,而不是机器的物理总容量:

kubectl describe node <node-name>

常见输出片段类似:

Capacity:
  cpu:                8
  memory:             32745256Ki
Allocatable:
  cpu:                7500m
  memory:             30000000Ki

两者之间的差额可能用于:

  • 操作系统;
  • kubelet;
  • 容器运行时;
  • Kubernetes 系统组件;
  • 云厂商代理;
  • 管理员配置的 systemReservedkubeReserved
  • eviction 相关的安全余量。

allocatable 的具体值取决于节点配置和发行版,不能用“物理内存减去一个固定百分比”推断。

3.3 Pod request 的计算不是简单逐项相加

对于普通容器,某资源的 Pod request 大致是所有应用容器 request 之和。但包含 init container 时,必须考虑 init container 的串行执行特性。

对某资源 rr,Pod 的有效 request 可以表示为:

Rpod,r=max(app containersRc,r,maxinit containersRi,r)R_{pod,r} = \max\left( \sum_{\text{app containers}} R_{c,r}, \max_{\text{init containers}} R_{i,r} \right)

例如:

spec:
  initContainers:
    - name: init
      image: busybox:1.36
      resources:
        requests:
          cpu: "2"
          memory: "1Gi"
  containers:
    - name: app
      image: nginx:1.27
      resources:
        requests:
          cpu: "500m"
          memory: "256Mi"

应用容器合计为 500m CPU、256Mi 内存,但 init container 需要 2 CPU、1 GiB,因此调度时该 Pod 的有效 request 至少要按 init container 的峰值计算。否则,init 阶段可能无法在节点容量内运行。

这也是一个常见反例:应用长期只需要 0.5 CPU,却因为启动迁移任务的 init container request 为 4 CPU,导致 Pod 长期难以调度。


四、Limit 如何影响运行时

4.1 CPU Limit 是可被强制执行的时间上限

CPU 是可压缩资源。所谓“可压缩”,是指系统可以减少进程获得的 CPU 时间,而不必立即杀死进程。

在常见 Linux cgroup 实现中,CPU limit 通常通过 CFS bandwidth control 实现。可以用一个简化模型理解:

  • CPU quota:一个周期内允许容器使用的 CPU 时间;
  • CFS period:CPU 配额的统计周期;
  • 当周期内的 CPU 时间耗尽,容器在剩余时间内被限制运行。

例如:

limits:
  cpu: "500m"

它通常意味着每个调度周期最多使用约半个 CPU 的计算时间。具体的 period、cgroup 文件以及 cgroup v1/v2 行为由内核、容器运行时和 kubelet 实现决定,不应依赖某个固定 cgroup 路径。

如果进程持续需要 1 CPU,而 limit 只有 500m,运行时可能让它运行一部分时间,再暂停一部分时间。结果通常是:

  • CPU 使用率接近 limit;
  • 请求延迟升高;
  • 工作队列堆积;
  • container_cpu_cfs_throttled_* 等指标升高(如果监控系统暴露这些 cAdvisor 指标)。

CPU limit 造成的 throttle 与节点是否空闲没有必然关系。即使节点还有大量空闲 CPU,容器自己的 cgroup quota 已经用完,它仍可能被限制。

4.2 CPU Request 不是 CPU Limit

下面的配置表示“调度按 250m 计算,运行时最多使用 2 CPU”:

resources:
  requests:
    cpu: "250m"
  limits:
    cpu: "2"

当节点有空闲 CPU 时,容器可能短时间使用接近 2 CPU;当节点竞争激烈时,它通常只能按 request 产生的权重参与竞争,且不能超过 2 CPU。

而下面的配置:

resources:
  requests:
    cpu: "2"
  limits:
    cpu: "2"

并不表示容器获得了物理 CPU 独占,只表示:

  • 调度器按 2 CPU 计算;
  • 容器最多使用 2 CPU;
  • 在 CPU 竞争时通常拥有更高的相对权重;
  • 是否有稳定延迟仍取决于节点超卖、内核调度、CPU manager、NUMA、频率变化和业务负载。

4.3 CPU Limit 为省略时的含义

如果容器没有 CPU limit,通常不会有该资源的 cgroup 上限,但仍受节点总容量、其他 cgroup 权重和系统负载影响。

这不等于“可以无限使用 CPU”。它最多只能使用节点上实际可获得的 CPU 时间。对于突发型批处理任务,这种配置可能允许更好地利用空闲资源;对于多租户服务,则可能增加邻居干扰和延迟抖动。

4.4 内存 Limit 是不可压缩资源的边界

内存不像 CPU 那样可以简单“暂停后继续”。当容器持续申请内存,达到 cgroup memory limit,Linux 内核通常会触发该 cgroup 范围内的内存回收或 OOM 处理。

例如:

resources:
  requests:
    memory: "128Mi"
  limits:
    memory: "256Mi"

这表示调度器至少按 128 MiB 计算,但运行时允许的内存边界约为 256 MiB。超过边界时,容器可能被内核杀死,而不是被自动降速。

内存 limit 不是严格的“应用变量大小上限”。应用还需要为:

  • 堆外内存;
  • 线程栈;
  • mmap;
  • 动态库;
  • 文件页缓存;
  • 临时文件;
  • 运行时内部结构;

预留空间。只为 Java heap 或 Python 对象设置容量,往往会低估 Kubernetes 看到的总内存。


五、QoS Class:Kubernetes 如何给 Pod 分类

Kubernetes 会根据 Pod 中容器的 CPU、内存 request 和 limit 计算 QoS Class。它不是用户直接设置的字段,而是由 Pod 资源配置推导出的结果:

kubectl get pod <pod-name> \
  -o jsonpath='{.status.qosClass}{"\n"}'

QoS 主要有三类。

5.1 Guaranteed

一个 Pod 要成为 Guaranteed,通常需要满足:

  1. Pod 中每个容器都设置了 CPU 和内存的 requests
  2. Pod 中每个容器都设置了 CPU 和内存的 limits
  3. 对每个容器而言,CPU request 等于 CPU limit;
  4. 对每个容器而言,内存 request 等于内存 limit。

示例:

apiVersion: v1
kind: Pod
metadata:
  name: guaranteed-demo
spec:
  containers:
    - name: app
      image: nginx:1.27
      resources:
        requests:
          cpu: "500m"
          memory: "512Mi"
        limits:
          cpu: "500m"
          memory: "512Mi"

Guaranteed 并不表示:

  • 永远不会被 OOM Kill;
  • 永远不会被驱逐;
  • 获得独占 CPU;
  • 节点发生故障时自动存活;
  • 业务不会因为自身超过 limit 而失败。

它主要改变调度计算、cgroup 配置以及资源压力下的相对处理方式。

5.2 BestEffort

如果 Pod 中所有容器都没有 CPU 和内存 request,也没有 CPU 和内存 limit,该 Pod 通常属于 BestEffort

apiVersion: v1
kind: Pod
metadata:
  name: besteffort-demo
spec:
  containers:
    - name: app
      image: nginx:1.27

这种 Pod 不会因为没有 request 而“不可运行”。它可以被调度到节点上,但调度器不会为它计入 CPU 或内存预算。节点压力发生时,它通常处于最不利的位置。

5.3 Burstable

不满足 Guaranteed,但至少有一个容器设置了 CPU 或内存 request/limit 的 Pod,通常属于 Burstable

apiVersion: v1
kind: Pod
metadata:
  name: burstable-demo
spec:
  containers:
    - name: app
      image: nginx:1.27
      resources:
        requests:
          cpu: "250m"
          memory: "128Mi"
        limits:
          cpu: "1"
          memory: "512Mi"

一个容器只设置 request、不设置 limit,也通常是 Burstable:

resources:
  requests:
    cpu: "250m"
    memory: "128Mi"

另一个容易忽略的反例是多容器 Pod:

spec:
  containers:
    - name: app
      image: nginx:1.27
      resources:
        requests:
          cpu: "500m"
          memory: "256Mi"
        limits:
          cpu: "500m"
          memory: "256Mi"
    - name: sidecar
      image: busybox:1.36

虽然 app 的 request 和 limit 相等,但 sidecar 没有完整配置,因此整个 Pod 不是 Guaranteed,而是 Burstable。

5.4 QoS 的判断必须以实际对象为准

Admission webhook、LimitRange 或其他默认策略可能修改 Pod 的资源字段。不要仅根据提交前的 YAML 推断 QoS,应查看 API Server 中实际保存的对象:

kubectl get pod <pod-name> -o yaml
kubectl get pod <pod-name> \
  -o jsonpath='{.status.qosClass}{"\n"}'

另外,QoS 分类主要基于 CPU 和内存资源;它不能概括所有资源治理策略。临时存储、GPU、ResourceQuota、PriorityClass 都是另外的维度。


六、CPU Throttle:被限速不等于被杀死

6.1 CPU Throttle 的故障路径

CPU throttle 的典型路径如下:

应用线程持续运行
    │
    ▼
达到容器 cgroup 的 CPU quota
    │
    ▼
Linux CFS 暂停该 cgroup 的可运行线程
    │
    ▼
请求处理变慢、队列堆积、延迟升高
    │
    └── 周期结束后恢复,不一定重启容器

它的关键特征是:

  • 容器仍然处于 Running;
  • 进程通常没有退出;
  • kubectl describe pod 不一定出现错误事件;
  • CPU 使用率可能“卡”在 limit 附近;
  • 延迟和吞吐先恶化,随后可能引发健康检查失败。

因此,下面两个现象不能混为一谈:

现象 典型原因
CPU 使用率达到 limit,throttled 时间增加 CPU limit 过低或周期性突发
CPU 使用率不高,但请求很慢 I/O、锁竞争、GC、下游依赖或 CPU 配额周期行为
进程退出,状态为 OOMKilled 内存压力或 cgroup 内存上限
Pod 被删除并重新创建 kubelet eviction、控制器重建或其他调度行为

6.2 一个可观察的 CPU 示例

下面的 Pod 会持续消耗 CPU,但 limit 只有 100m:

apiVersion: v1
kind: Pod
metadata:
  name: cpu-throttle-demo
spec:
  containers:
    - name: cpu
      image: busybox:1.36
      command:
        - sh
        - -c
        - |
          while true; do
            :;
          done
      resources:
        requests:
          cpu: "100m"
        limits:
          cpu: "100m"

创建并查看:

kubectl apply -f cpu-throttle-demo.yaml
kubectl top pod cpu-throttle-demo
kubectl describe pod cpu-throttle-demo

前置条件:

  • 集群安装了 Metrics Server,kubectl top 才能工作;
  • busybox 镜像可以从节点拉取;
  • 节点允许创建该 Pod。

预期现象是 CPU 使用量接近 100m,但容器不会因为 CPU 用满而自动退出。若监控系统暴露 cAdvisor 指标,可以进一步查看常见指标:

container_cpu_cfs_throttled_seconds_total
container_cpu_cfs_periods_total
container_cpu_cfs_throttled_periods_total

这些指标名称和采集方式属于常见实现,不是所有 Kubernetes 发行版都保证以相同方式提供。应以实际监控系统和 cgroup 版本为准。

生产环境中不能只看到 CPU limit 被使用就断定“limit 太低”。如果业务本来就需要 100m CPU,使用率高是正常的;真正需要关注的是 throttling、延迟、吞吐和队列是否同时恶化。


七、OOM:内存超限、节点 OOM 与业务自杀

OOM 是 Out Of Memory,表示系统在满足内存分配请求时无法继续提供内存。Kubernetes 环境中至少要区分三种情况。

7.1 cgroup 内存 OOM

如果某容器超过自己的 memory limit,内核可能在该 cgroup 内选择进程杀死。常见结果是:

kubectl get pod <pod-name>

看到容器重启,进一步查看:

kubectl describe pod <pod-name>

可能出现:

Last State:     Terminated
  Reason:       OOMKilled
  Exit Code:    137

137 通常对应进程收到 SIGKILL,但诊断时应优先看 Reason: OOMKilled、事件以及容器运行时日志,不能仅凭退出码判断所有情况。

下面的示例用于实验,不适合生产:

apiVersion: v1
kind: Pod
metadata:
  name: memory-oom-demo
spec:
  restartPolicy: Always
  containers:
    - name: allocator
      image: python:3.12-alpine
      command:
        - python
        - -c
        - |
          chunks = []
          while True:
              chunks.append(bytearray(10 * 1024 * 1024))
      resources:
        requests:
          memory: "32Mi"
        limits:
          memory: "64Mi"

应用会不断申请内存,接近或超过 64 MiB 后可能被杀死。由于 restartPolicy: Always,Pod 中的容器可能被重新启动,表现为 RESTARTS 持续增加:

kubectl get pod memory-oom-demo -w
kubectl describe pod memory-oom-demo

这个实验有几个边界:

  • Python 解释器自身已经占用一部分内存;
  • cgroup 统计不一定与 Python 分配的字节数完全相等;
  • 镜像、内核和 cgroup 版本会影响触发时机;
  • OOM Kill 的具体受害进程由 Linux 内核选择。

7.2 节点级 OOM

如果节点整体内存耗尽,可能发生 node-level OOM。此时内核可能杀死:

  • 普通容器进程;
  • kubelet;
  • 容器运行时;
  • 节点上的系统进程。

这比单个容器达到自己的 limit 更危险。因为 kubelet 可能来不及先执行 eviction,内核就已经开始杀进程。

因此:

Memory limit 主要提供 cgroup 层面的边界;它不能保证节点不会发生整体内存不足。

诊断节点级 OOM 时,应查看:

kubectl describe node <node-name>
kubectl get events --sort-by=.lastTimestamp

并在节点上查看系统日志,例如:

journalctl -k
journalctl -u kubelet

不同操作系统、云镜像和日志系统的命令与日志位置可能不同。

7.3 应用主动退出不一定是 OOM

应用可能因为以下原因主动退出:

  • 自己的内存保护逻辑;
  • JVM、Go runtime 或数据库的内部限制;
  • 探针失败后被重启;
  • 配置错误;
  • 收到 SIGTERM 或 SIGKILL;
  • 进程内部 panic。

这类情况不能直接归因于 Kubernetes OOM。应同时检查:

kubectl logs <pod-name> --previous
kubectl describe pod <pod-name>
kubectl get pod <pod-name> -o json

--previous 对于容器已经重启的场景尤其重要,因为当前容器日志可能已经不是发生故障的那一次。


八、QoS 与 OOM 优先级:重要但不是绝对规则

在节点内存压力下,Linux 内核需要选择进程;Kubernetes kubelet 也可能主动驱逐 Pod。QoS 会影响 Kubernetes 对 Pod 的相对保护程度,kubelet 还会通过 oom_score_adj 等机制影响内核的选择倾向。

通常可以理解为:

  • BestEffort 更容易成为受害者;
  • Burstable 中超过自身 request 的 Pod 更容易受到影响;
  • Guaranteed 通常受到更高保护;
  • 但节点级 OOM、系统进程、优先级和实际内存使用仍可能改变结果。

不能把它简化为“BestEffort 一定先死,Guaranteed 永远不会死”。内核 OOM 选择的是进程,不是 Kubernetes 抽象层中的 Pod;kubelet 也可能因节点压力、系统保留不足或特定驱逐条件采取行动。

一个关键反例是:

Burstable Pod:
  memory request = 100Mi
  memory limit   = 8Gi
  actual usage   = 2Gi

它远远超过自己的 request,因此在内存压力下不一定比另一个使用 200Mi、request 为 1Gi 的 Pod 更安全。仅看 QoS Class 不够,还要看“实际使用是否超过 request”。


九、Eviction:kubelet 主动驱逐 Pod

9.1 Eviction 与 OOM 的区别

Eviction 是 kubelet 根据节点资源压力主动采取的 Pod 淘汰行为;OOM Kill 是 Linux 内核在内存分配无法满足时杀死进程。

对比项 Eviction OOM Kill
主要执行者 kubelet Linux 内核
触发依据 节点压力信号、驱逐阈值 cgroup 或节点内存耗尽
对象 Kubernetes Pod 进程
是否有 Pod 终止流程 通常有,可执行优雅终止 可能直接 SIGKILL
常见表现 Pod 被驱逐、状态变化、事件 容器 OOMKilled、退出码常见为 137
是否一定等 kubelet 处理 是 kubelet 路径 不一定,内核可先处理

如果节点内存已经耗尽,内核 OOM 可能先于 kubelet eviction 发生。因此设置 eviction threshold 不能保证绝对避免 OOM。

9.2 kubelet 监控的压力信号

kubelet 会监控节点压力,例如:

  • memory.available
  • nodefs.available
  • imagefs.available
  • nodefs.inodesFree
  • pid.available

具体启用的信号、阈值以及文件系统布局与 kubelet 配置和发行版有关。

查看节点状态:

kubectl describe node <node-name>

可能看到:

Conditions:
  MemoryPressure   True
  DiskPressure     False
  PIDPressure      False

节点压力还可能反映在事件中:

kubectl get events -A --sort-by=.lastTimestamp

9.3 内存驱逐的判断逻辑

对内存压力,kubelet 不是单纯按照 QoS 名称排序。实际排序会综合考虑:

  1. Pod 是否超过自己的 memory request;
  2. Pod 的优先级;
  3. Pod 的实际使用量与 request 的关系;
  4. QoS 等相关信息。

因此,一个 Burstable Pod 如果实际使用没有超过 request,可能比另一个严重超过 request 的 Burstable Pod 更不容易被驱逐。PriorityClass 也会参与排序,不能只看 QoS。

Pod 被驱逐后,控制器的行为取决于工作负载类型:

  • Deployment 通常创建替代 Pod;
  • StatefulSet 通常按序维护副本;
  • Job 可能重试;
  • 独立 Pod 不一定有人重新创建。

“Pod 被重新调度了”通常不是 eviction 本身完成的,而是上层控制器观察到副本数不足后创建了新的 Pod。

9.4 磁盘压力与 ephemeral-storage

节点磁盘压力可能来自:

  • 容器可写层;
  • emptyDir
  • 容器日志;
  • 镜像层和镜像缓存;
  • inode 耗尽。

可以为容器设置临时存储 request 和 limit:

resources:
  requests:
    ephemeral-storage: "1Gi"
  limits:
    ephemeral-storage: "2Gi"

但这不等同于为 Pod 分配了一块独占磁盘。kubelet 通常依据节点文件系统信号和本地临时存储使用情况进行管理。不同节点可能使用独立的 imagefscontainerfs 或统一文件系统,统计和驱逐行为会有差异。

内存、CPU 和临时存储的故障表现也不同:

CPU 超限       → throttle,进程通常继续运行
内存超限       → OOM Kill,进程可能退出
节点内存压力   → kubelet eviction 或 node-level OOM
磁盘压力       → DiskPressure,可能驱逐使用临时存储较多的 Pod

十、一个完整资源配置示例

下面的 Deployment 展示了应用容器、sidecar、探针和资源配置之间的关系:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: app
          image: nginx:1.27
          ports:
            - containerPort: 80
          resources:
            requests:
              cpu: "250m"
              memory: "256Mi"
            limits:
              cpu: "1"
              memory: "512Mi"
              ephemeral-storage: "1Gi"
          readinessProbe:
            httpGet:
              path: /
              port: 80
            initialDelaySeconds: 2
            periodSeconds: 5

        - name: log-sidecar
          image: busybox:1.36
          command: ["sh", "-c", "while true; do sleep 3600; done"]
          resources:
            requests:
              cpu: "50m"
              memory: "32Mi"
            limits:
              cpu: "100m"
              memory: "64Mi"
              ephemeral-storage: "128Mi"

对每个 Pod:

CPU request = 250m + 50m = 300m
内存 request = 256Mi + 32Mi = 288Mi

CPU limit = 1 + 100m = 1.1 CPU
内存 limit = 512Mi + 64Mi = 576Mi

该 Pod 不是 Guaranteed,因为应用容器的 request 与 limit 不相等,且 sidecar 也不满足等值条件,所以它通常是 Burstable。

三个副本在调度时大致贡献:

CPU request    = 3 × 300m = 900m
内存 request  = 3 × 288Mi = 864Mi

调度器不会把三个 Pod 的 CPU limit 3 × 1.1 = 3.3 CPU 当作必须预留的容量,但节点上的运行时仍可能允许它们竞争最多约 3.3 CPU。如果节点可运行容量小于这种潜在峰值,就存在 CPU 超卖和 throttle 风险;如果所有容器都同时使用高内存,则存在内存压力甚至 OOM/eviction 风险。


十一、如何区分 Pending、Throttle、OOM 和 Eviction

11.1 Pod 长期 Pending:先看 Request 和调度事件

kubectl describe pod <pod-name>

重点看:

Events:
  Warning  FailedScheduling

常见原因包括:

Insufficient cpu
Insufficient memory
node(s) had untolerated taint
node(s) didn't match Pod's node affinity

如果是资源不足,应比较:

kubectl get nodes
kubectl describe node <node-name>
kubectl get pod -A \
  -o custom-columns='NAMESPACE:.metadata.namespace,NAME:.metadata.name,NODE:.spec.nodeName,CPU_REQ:.spec.containers[*].resources.requests.cpu,MEM_REQ:.spec.containers[*].resources.requests.memory'

最后一个命令对多容器结果不一定直观,生产诊断时应通过脚本逐容器求和,并结合 init container 的峰值计算。

11.2 Pod Running 但延迟高:检查 CPU throttle

kubectl top pod <pod-name>
kubectl describe pod <pod-name>

如果使用 Prometheus,可以观察:

  • CPU 使用量;
  • CPU throttled seconds;
  • throttled periods;
  • 请求延迟;
  • 运行队列和应用线程池排队。

只看 kubectl top 的 CPU 百分比不够。容器使用 100m CPU 可能是正常完成了工作,也可能是被 100m limit 持续压住;需要把使用量与 limit、throttle 和业务指标放在一起判断。

11.3 容器频繁重启:检查 OOMKilled 和上一次日志

kubectl get pod <pod-name>
kubectl describe pod <pod-name>
kubectl logs <pod-name> -c <container-name> --previous

重点关注:

Last State:
  Reason: OOMKilled

同时查看节点:

kubectl describe node <node-name>

如果多个不相关 Pod 同时出现异常,或者 kubelet、容器运行时也不稳定,应怀疑节点级内存压力,而不只是某个 Pod 的 limit 太小。

11.4 Pod 被驱逐:看 Pod 原因、节点条件和事件

kubectl get pod <pod-name> -o wide
kubectl describe pod <pod-name>
kubectl describe node <node-name>
kubectl get events -A --sort-by=.lastTimestamp

需要区分:

  • Evicted
  • OOMKilled
  • NodeLost
  • 探针失败导致重启;
  • 控制器滚动更新;
  • 用户手动删除。

这些状态可能都表现为“Pod 不见了”或“Pod 重启了”,但恢复方法完全不同。


十二、Request、Limit 与 QoS 的常见误解

误解一:Request 是应用能稳定拿到的资源

CPU request 主要参与调度和相对竞争权重;内存 request 主要参与调度和驱逐判断。它们不是物理资源预留合同。

如果一个容器 request 为 1Gi、limit 为 4Gi,它可以被调度到一个按 1Gi 预算仍然有空间的节点上,但四个 Pod 同时使用 4Gi 时,节点可能无法承受。

误解二:Limit 越大,Pod 越安全

更大的内存 limit 允许单个 Pod 使用更多内存,但也增加节点超卖和节点级 OOM 风险。更大的 CPU limit 可以容纳突发,但如果节点 CPU 竞争激烈,也不能保证低延迟。

Limit 是故障边界,不是容量规划的替代品。

误解三:Guaranteed 就不会被驱逐

Guaranteed 通常拥有更高的保护级别,但只要节点出现不可避免的资源压力,Guaranteed Pod 也可能被驱逐。例如:

  • 节点系统进程消耗过多内存;
  • 节点磁盘压力;
  • Pod 本身超过 limit 后被 OOM Kill;
  • 节点故障或被维护;
  • 资源压力超过 kubelet 可以安全处理的范围。

误解四:CPU limit 超过后会 OOM

CPU limit 超过后通常是 throttle,不是 OOM。CPU 和内存是不同的资源类型,分别由不同的 Linux cgroup 机制处理。

误解五:删除 Pod 就能解决资源问题

删除 Pod 只能暂时释放其 cgroup 资源;如果 Deployment、StatefulSet 或其他控制器仍要求该副本存在,它会立即创建替代 Pod。若根因是 request 不合理、内存泄漏、节点容量不足或滚动更新策略,单纯删除可能造成反复重建和更大流量波动。


十三、与 LimitRange 和 ResourceQuota 的关系

资源字段还可能被命名空间级策略改变或拒绝。

13.1 LimitRange:默认值和单对象约束

LimitRange 可以为容器设置默认 request/limit,也可以限制单个容器或 Pod 的资源范围。例如:

apiVersion: v1
kind: LimitRange
metadata:
  name: defaults
  namespace: team-a
spec:
  limits:
    - type: Container
      defaultRequest:
        cpu: "100m"
        memory: "128Mi"
      default:
        cpu: "500m"
        memory: "512Mi"

它可能导致一个原本没有资源字段的容器在 admission 后拥有默认值,从而改变:

  • Pod 的 QoS Class;
  • 调度器使用的 request;
  • 是否满足 ResourceQuota;
  • 节点的可调度容量。

因此,排查 QoS 或 Pending 问题时,应查看实际保存对象,而不是只看应用仓库中的原始模板。

13.2 ResourceQuota:命名空间预算

ResourceQuota 控制命名空间总体资源预算,例如:

apiVersion: v1
kind: ResourceQuota
metadata:
  name: team-quota
  namespace: team-a
spec:
  hard:
    requests.cpu: "10"
    requests.memory: 20Gi
    limits.cpu: "20"
    limits.memory: 40Gi
    pods: "50"

它的语义是:

命名空间内所有对象的 request/limit 总量不能超过配额

ResourceQuota 主要在 admission 阶段拒绝新对象或更新,不是节点运行时的实时限速器。Pod 已经运行后,quota 不会因为瞬时 CPU 使用过高而主动 throttle,也不会直接执行 eviction。

还必须注意:

  • requests.cpulimits.cpu 是两个不同预算;
  • Pod 数量配额与 CPU、内存配额互相独立;
  • quota 是否要求所有 Pod 显式设置资源,取决于具体 quota 与 LimitRange 配置;
  • 多租户隔离不能只依靠 QoS,还需要配额、节点池、优先级、污点容忍和网络策略等机制。

十四、生产容量规划中的正确连接方式

资源配置应至少形成以下闭环:

业务并发与延迟目标
        │
        ▼
压测得到的实际 CPU / 内存分布
        │
        ├── request:用于调度和容量预算
        └── limit:用于限制异常峰值和隔离故障
                │
                ▼
节点 allocatable、超卖比例、碎片和故障余量
                │
                ▼
扩容、驱逐风险和故障恢复能力

14.1 Request 应从工作负载数据推导

设某服务在目标并发下的 CPU 使用量观测为 UcpuU_{cpu},内存峰值为 UmemU_{mem}。可以把 request 设计为:

RcpuPq(Ucpu)+ScpuR_{cpu} \geq P_{q}(U_{cpu}) + S_{cpu}

RmemPq(Umem)+SmemR_{mem} \geq P_{q}(U_{mem}) + S_{mem}

其中:

  • PqP_q 是某个观测分位数,例如 P95 或 P99;
  • SS 是为 GC、流量变化、采集误差和发布过程保留的余量。

这不是 Kubernetes 规定的固定公式,而是容量规划方法。分位数越高,调度预算越大、碎片越多,但被驱逐和性能抖动的概率可能降低。

14.2 内存通常比 CPU 更适合保守设置

CPU 可以在竞争时通过 throttle 退化,内存超过边界则可能直接杀死进程。因此:

  • 内存 request 过低会导致调度器过度打包;
  • 内存 limit 过低会造成 OOM 重启;
  • 内存 limit 过高会增加节点级 OOM 风险;
  • 内存泄漏不能通过无限增大 limit 解决,只能延后故障。

14.3 过大的 Request 会产生碎片

假设节点 allocatable 为 8 CPU,有三个节点,每个节点剩余:

节点 A:600m
节点 B:600m
节点 C:600m

集群总体还有 1.8 CPU 空闲,但一个新 Pod request 为 1 CPU 时仍可能无法调度,因为没有单个节点拥有至少 1 CPU 的可用 request 空间。这就是资源碎片。

所以容量规划不仅要看集群总量,还要看:

  • Pod request 分布;
  • 节点规格;
  • 亲和性和反亲和性;
  • 拓扑分布约束;
  • DaemonSet 占用;
  • 滚动更新期间的额外副本;
  • 单节点故障后的剩余容量。

14.4 必须预留故障余量

如果所有节点都按 allocatable 的 100% 调度,任意一个节点故障都可能使剩余节点无法容纳被重新调度的副本。更实际的规划会预留:

  • 节点故障余量;
  • 发布期间的 surge 副本;
  • 突发流量;
  • 系统组件增长;
  • eviction 前的缓冲;
  • 镜像拉取和日志增长造成的临时磁盘空间。

这部分属于容量工程和运行策略,不是 QoS Class 自动完成的功能。


十五、处理问题时应先判断故障层次

可以按以下顺序诊断:

Pod 是否 Pending?
  └─ 是:看 request、allocatable、亲和性、taint 和 FailedScheduling

Pod 是否 Running 但慢?
  └─ 看 CPU limit、throttle、GC、锁、I/O 和下游延迟

容器是否频繁重启?
  └─ 看 Last State、OOMKilled、退出码、previous 日志和探针事件

Pod 是否被 Evicted?
  └─ 看节点 Pressure、驱逐事件、内存/磁盘/ inode 使用

多个 Pod 与节点组件同时异常?
  └─ 优先排查节点级资源耗尽、内核 OOM、运行时和 kubelet

最重要的诊断原则是:

不要用一个现象解释所有资源故障。Pending 是调度问题,CPU throttle 是运行时 CPU 控制问题,OOM 是内存分配失败路径,Eviction 是 kubelet 的节点治理路径;它们可能相互诱发,但不是同一个机制。

当这些机制被分开理解后,requestslimits 和 QoS 才能被正确用于容量规划、隔离故障和解释生产环境中的资源行为。


系列导航与关联阅读

官方资料

本文依据 Kubernetes、CNCF 与相关项目官方文档重新梳理;正文和生产清单由 WR BLOG 编写。