2026-09-03

Kubernetes Requests 与 Limits:调度、限流和 OOM 边界

把 CPU/内存 requests 与 limits 讲清楚:调度、限流、OOMKilled、QoS、HPA/VPA 与容量规划。

资源配额是让集群“看起来很稳”或“莫名其妙不稳”的最快开关之一。很多线上抖动源于 requests/limits 写法不合理,或者它们和 HPA/节点容量的假设不一致。

先看三个真实症状

如果你是在排查线上抖动,先不要急着改 HPA。Requests 和 limits 配错时,常见症状通常长这样:

  • Pod 一直 Pending:调度器认为节点剩余 requests 不够,即使 top 看起来还有空闲 CPU。
  • P99 突然飙升但 CPU 使用率不高:CPU limit 触发 throttling,应用看似没有吃满,实际被 CFS quota 按住了。
  • 节点内存被打穿或 Pod 频繁 OOMKilled:memory request/limit 和真实峰值不匹配,驱逐和重启开始接管现场。

我通常先把问题分成两类:调度阶段的问题看 requests,运行阶段的问题看 limits、QoS、throttling 和 OOMKilled。混在一起看,很容易把 HPA、节点池和应用性能互相甩锅。

最容易误判的地方

  • kubectl top 看到的是当前使用量,不是调度器使用的“占位量”。
  • CPU limit 不是免费保险,它可能把延迟问题伪装成“服务偶发慢”。
  • HPA 的 CPU 百分比通常以 requests 为分母,requests 乱写会让扩缩容判断失真。
  • Node Capacity 不是应用可用容量,真正能调度的是 Allocatable。

先定一个可验证的起点

  • 所有容器都要设置 requests(CPU + 内存):调度器用它来决定 Pod 放到哪里。
  • CPU limits 要谨慎:CPU 上限会触发 CFS quota,负载一上来可能被节流,延迟飙升但你还以为“CPU 不高”。
  • 内存 limits 很重要:没有内存上限,单个容器可能把节点内存吃穿,拖垮同节点其他业务;有上限则要面对 OOMKilled。
  • HPA 默认基于 requests:requests 不准,自动扩缩容就会不准。

调度器到底看什么

Kubernetes 调度(bin packing)主要基于 requests:

  • 你写的 requests.cpu、requests.memory 决定这个 Pod “占用”多少资源用于放置。
  • limits 主要影响运行时资源限制(尤其 CPU 限流、内存 OOM)。

如果某资源只写 limit、没写 request,且 LimitRange、准入策略等没有先填默认 request,Kubernetes 会把该 limit 复制成 request。只有 request 和 limit 都没有、且没有其他默认机制时,请求量才是 0。排查调度时要看 API Server 最终保存的 Pod,而不是只看提交前 YAML。

起步模板

resources:
  requests:
    cpu: "250m"
    memory: "256Mi"
  limits:
    memory: "512Mi"

这个组合背后的思路:

  • requests 让调度与容量规划可预测
  • memory limit 保护节点不被单个容器拖死
  • 先不设 CPU limit(或留足余量),避免节流导致的延迟尖刺

CPU:requests 用于放置,limits 用于节流(并可能伤害延迟)

CPU 是“可压缩资源”。当 CPU 不够时,进程通常不会崩,而是变慢。

  • requests.cpu:调度和扩容计算的基础(很多指标会以它为分母)
  • limits.cpu:运行时的 CFS quota 上限,超过就会被 throttling(节流)

没设置 CPU limit 时,容器可以使用节点上的空闲 CPU;发生竞争时,CPU request 还会影响相对 CPU share。它不是独占核心保证,除非平台另行启用 CPU Manager 等隔离策略。

一个很典型的坑:

你看到 CPU usage 似乎不高,但 p95/p99 延迟突然升高;原因可能是被节流,尤其在突发流量或 GC/加密握手等瞬时 CPU 峰值场景。

建议:

  • 一定保证 limits.cpu >= requests.cpu
  • 对突发型服务不要把 CPU limit 设得太贴近 requests;否则“短时间需要多一点 CPU”都拿不到

内存:limits 保护节点,但要接受 OOMKilled 的现实

内存不是可压缩资源。Linux 对 memory limit 的执行是反应式的:容器可能短暂超过 limit,内核检测到内存压力后才触发 OOM 处理。被终止的进程如果是容器主进程,Kubernetes 通常记录 OOMKilled,是否重启还取决于 restart policy。

排查 OOM 的第一步:

kubectl describe pod -n <ns> <pod> | rg -n "OOMKilled|Killed|Exit Code"

在 cgroup v2 节点上,可以直接查看容器的节流与内存事件:

kubectl exec -n <ns> <pod> -c <container> -- cat /sys/fs/cgroup/cpu.stat
kubectl exec -n <ns> <pod> -c <container> -- cat /sys/fs/cgroup/memory.events

cpu.stat 中 nr_throttled、throttled_usec 持续增长,才是 CPU limit 正在影响运行的直接证据;memory.events 中 oom、oom_kill 可区分容器级内存压力。cgroup v1 的文件路径和字段不同。

常见应对路径:

  • 提高 limits.memory(同时评估成本与密度)
  • 优化程序内存(缓存策略、泄漏、对象生命周期)
  • 引入 VPA 做推荐(即使不自动应用也很有价值)

requests/limits 还会影响 QoS(驱逐优先级)

Kubernetes 会按 requests/limits 计算 Pod 的 QoS:

  • Guaranteed:每个容器 CPU/内存都设置 requests 与 limits,且相等(requests == limits)
  • Burstable:设置了部分 requests/limits,但不满足 Guaranteed 条件
  • BestEffort:完全没有 requests/limits

QoS 会影响 OOM score 和节点压力下的候选范围,但驱逐不是固定的“BestEffort、Burstable、Guaranteed”队列。kubelet 还会看 Pod 是否超过 request、Priority 和超过 request 的相对幅度。Guaranteed Pod 也不是绝对免疫;准确判断要结合 request、实际用量和 PriorityClass。

快速查看某个 Pod 的 QoS:

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

HPA 的分母问题:CPU 利用率通常除以 requests

很多集群里 HPA 的 CPU utilization 算法近似是:

当前 CPU 使用量 / CPU requests

如果目标 Pod 的相关容器没有 CPU request,HPA 无法计算该容器的利用率,可能跳过指标或抑制扩缩动作。不要把“request=0”当成无限敏感的 HPA 配置。

因此:

  • requests 写太高:HPA 认为“利用率很低”,扩容会偏慢
  • requests 写太低:HPA 认为“利用率很高”,扩容会偏激进

如果你发现:

  • 峰值时扩容不及时
  • 或者平时无意义扩容/缩容抖动

第一优先级就该回头检查 requests 是否贴近“真实稳定负载”。

运行时内存上限要与容器限制对齐

如果运行时以为自己能用的内存比容器限制大,OOM 通常只是时间问题:

  • JVM:-Xmx 要低于 limits.memory,并为 native memory 留余量。
  • Node.js:对齐 --max-old-space-size。
  • Go:没有单一堆上限参数,但要关注峰值、缓存和 buffer,配合 GOMEMLIMIT 更稳。

还要考虑节点 Allocatable(不是 Capacity)

节点的物理资源(Capacity)并不等于可分配给 Pod 的资源(Allocatable)。系统预留、kubelet 预留和驱逐阈值都会吃掉一部分。DaemonSet 会参与调度,但如果它的 request 明显低于实际用量,同样会让容量模型失真。

排查“理论上能放下,实际放不下”:

kubectl describe node <node> | rg -n "Allocatable|Allocated resources|Capacity"
kubectl get events -A --sort-by=.lastTimestamp | rg -n "Evicted|MemoryPressure"

Sidecar / initContainer:别忘了隐藏成本

很多生产 Pod 带 sidecar:

  • service mesh(Envoy)
  • 日志/监控 agent
  • 安全组件

这些容器同样需要 requests/limits,并且会显著改变 Pod 的总资源画像。只给主容器配资源,常常会让你“以为 300Mi”,实际 Pod 轻松 600Mi+。

initContainers 也要注意。传统 init container 按顺序运行,Pod 调度时通常取“所有普通容器 request 之和”与“最大单个 init container request”中的较大值,再加 Pod overhead。以 restartPolicy: Always 声明的原生 sidecar 会按不同规则计入。迁移、warmup、下载等启动峰值如果没写准,Pod 会放不下或在批量发布时造成节点压力。

内存型 emptyDir 使用 tmpfs,写入内容会计入写入容器的内存用量。缓存文件、模型临时文件或大日志写进 emptyDir.medium: Memory,同样可能触发 memory limit。

用 LimitRange / ResourceQuota 做“护栏”

为了避免团队成员忘记写 requests/limits,常见做法是用:

  • LimitRange:提供默认值与 min/max,防止极端配置与“全空”
  • ResourceQuota:限制命名空间总配额,并可强制要求 requests/limits

示例(仅示意,按你们平台标准调整):

apiVersion: v1
kind: LimitRange
metadata:
  name: defaults
  namespace: app
spec:
  limits:
    - type: Container
      defaultRequest:
        cpu: 100m
        memory: 128Mi
      default:
        memory: 512Mi
      min:
        cpu: 25m
        memory: 64Mi
      max:
        cpu: "2"
        memory: 4Gi

新版本能力:Pod 级预算与原地调整

从 Kubernetes 1.34 起,PodLevelResources 进入 Beta 并默认启用,可在 Pod 层声明 CPU、内存和 hugepages 预算,让同一个 Pod 内的容器共享资源余量。它仍是 Beta 能力,使用前先核对集群版本、feature gate、监控与计费系统是否理解 Pod 级字段。

从 Kubernetes 1.35 起,In-place Pod Resize 进入 Stable,可以在不重建 Pod 的情况下调整部分容器 CPU/内存 request 和 limit。它不等于所有运行时都能无条件在线缩放;发布前仍要检查 .status.resize、.status.containerStatuses[].allocatedResources 和节点容量。

选数值的实战方法(可落地的闭环)

  1. 先用保守模板上线(requests 写上;memory limit 写上;CPU limit 先不写)。
  2. 观察 1–2 周真实流量下的:
  • 内存峰值(p95/p99)
  • CPU 峰值与是否节流(如果设了 limit)
  • OOMKilled、重启频率
  • HPA 的扩缩容节奏(是否晚/是否抖)
  1. 迭代:
  • 内存经常逼近上限:提高 limit 或优化内存
  • 延迟尖刺:检查 CPU throttling、requests/limits 的比例
  • HPA 不准:先修 requests,再谈 HPA 参数
  1. 引入 VPA 做推荐(Off/Initial 先走一段),逐步自动化。

快速决策表

目标 CPU request CPU limit Memory request Memory limit
一般 Web/API 服务 必填 可选 必填 必填
严格多租户 必填 必填 必填 必填
批处理任务 必填 可选 必填 必填

最终原则

先把 requests 写对,再把 memory limit 写上;CPU limit 只有在你能接受并量化节流影响时再加。

上线前自检

  • 主容器、sidecar 和长期运行的 init container 都有经过测量的 requests。
  • init container 的峰值、Pod overhead 和内存型 emptyDir 已计入容量。
  • memory limit 覆盖真实 P95/P99 峰值,并给运行时额外内存留余量。
  • 只有能观测并接受 CPU throttling 时才设置 CPU limit。
  • HPA 使用真实 requests,而不是复制来的占位值。
  • 容量检查使用 Allocatable,并计入 DaemonSet 与系统预留。

资源配置的三个判断

Q: requests 要等于 limits 吗? A: 只有需要强保证时才这样设置;多数服务用 Burstable 即可。

Q: 为什么会 OOMKilled? A: memory limit 太小或峰值过高,调整 limit 并结合监控。

Q: 限制会影响调度吗? A: 调度看最终 request;如果只写 limit,Kubernetes 可能先把 limit 复制成 request,因此 limit 会间接改变调度结果。

参考链接