资源配额是让集群“看起来很稳”或“莫名其妙不稳”的最快开关之一。很多线上抖动源于 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 和节点容量。
选数值的实战方法(可落地的闭环)
- 先用保守模板上线(requests 写上;memory limit 写上;CPU limit 先不写)。
- 观察 1–2 周真实流量下的:
- 内存峰值(p95/p99)
- CPU 峰值与是否节流(如果设了 limit)
- OOMKilled、重启频率
- HPA 的扩缩容节奏(是否晚/是否抖)
- 迭代:
- 内存经常逼近上限:提高 limit 或优化内存
- 延迟尖刺:检查 CPU throttling、requests/limits 的比例
- HPA 不准:先修 requests,再谈 HPA 参数
- 引入 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 会间接改变调度结果。