Requests、Limits、QoS 与资源基线
一次促销发布后,API 的平均 CPU 只有 38%,节点看起来也还有余量,接口尾延迟却突然拉长。应用没有报错,Pod 也没有重启,值班人员先扩副本、再扩节点,延迟仍然呈锯齿。最后沿容器 cgroup 的 cpu.stat 才发现:容器的 CPU limit 只有 200m,每个调度周期都在积累 throttled_usec。监控中的节点空闲并不能越过容器配额,扩出来的新节点也不会改变这个 Pod 的 CPU 上限。
同一个集群的夜间批任务则表现相反。Pod 先被调度到内存看似充足的节点,运行一段时间后以 OOMKilled 重启;随后节点进入 MemoryPressure,另一些低优先级 Pod 被驱逐。团队把两类现象都叫作“内存不够”,于是同时调高 limits、requests 和节点规格,既放大了成本,又没有解释谁被内核杀死、谁被 kubelet 驱逐、调度器当初为何允许它们落在一起。资源声明只有沿 API、调度器、kubelet、内核和控制器逐层取证,才会从 YAML 数字变成可治理的容量合同。
先把四个容易混淆的数字拆开
usage 是已经发生的消耗,request 是调度器和多个容量控制器采用的声明基线,limit 是节点对容器施加的执行上限,节点 allocatable 是可分给 Pod 的资源总盘子。它们彼此相关,但绝不是同一个量。调度器放置 Pod 时主要比较待调度 Pod 的 requests 与节点尚未被 requests 占用的 allocatable,不会因为某个旧 Pod 此刻 CPU 很闲,就把它已经声明的 request 临时送给新 Pod 记账。
CPU request 通常转化为竞争时的相对权重,CPU limit 通常转化为 CFS 带宽配额;容器只要在配额窗口内用完额度,即使宿主机还有空闲 CPU,也可能被节流。内存 request 参与调度和节点压力判断,内存 limit 则由内存 cgroup 约束;超限时通常由内核 OOM 机制终止容器进程,而不是像 CPU 那样等待下一个配额窗口继续执行。Kubernetes 的容器资源管理说明给出了这两种资源的不同执行语义。
limits 也不等于容量预留。一个 request: 256Mi, limit: 1Gi 的容器只为调度声明了 256Mi 基线,却允许进程在节点可用时增长到 1Gi。大量工作负载采用同样比例,会形成内存过量承诺:平时提高装箱率,峰值同时到来时却可能触发节点压力和 OOM。反过来,把 request 直接写成历史峰值会降低风险,却可能让调度器认为集群已满,制造 Pending Pod、空闲碎片和不必要的节点扩容。
资源字段如何穿过调度器与节点内核
Pod 进入 API Server 后,准入链可能先由 LimitRange 补默认值或检查单容器上下界,再由 ResourceQuota 检查 namespace 总量。调度器读取最终 PodSpec,计算 Pod 级调度需求,在 Filter 阶段淘汰剩余 allocatable 不足的节点,在 Score 阶段参与打分。绑定后,目标节点的 kubelet 交给容器运行时创建 cgroup,Linux 内核才真正执行 CPU 权重、CPU 配额和内存上限。
这条链解释了两个常见误判。第一,修改 Deployment 模板不会原地改写正在运行的 Pod;新 ReplicaSet 创建的新 Pod 才携带新资源声明。第二,调度成功只证明 requests 能装入节点,不证明应用峰值低于 limits,也不证明同节点所有容器同时冲高时仍有足够内存。
对普通 Pod,调度需求不是简单取某一个容器。常规应用容器的 requests 会累加,init container 的峰值需求还要按其执行模型参与计算,Pod overhead 也可能加入总量。排查 Insufficient cpu 或 Insufficient memory 时,应直接查看 Pod 的最终 spec、scheduler Event 和节点 allocatable,不要只看主容器的一行配置。
kubectl get pod <pod-name> -n <namespace> -o yaml
kubectl describe pod <pod-name> -n <namespace>
kubectl get node <node-name> -o jsonpath='{.status.allocatable}'
kubectl describe node <node-name>预期在 Pending Pod 的 Event 中看到类似 Insufficient cpu、Insufficient memory 或其他调度约束;这属于声明容量不足。已经 Running 的 Pod 出现延迟、OOM 或驱逐,则应转向 cgroup、容器状态与节点条件,不应继续把 scheduler Event 当作运行时证据。
用 LimitRange 与 ResourceQuota 建立 namespace 基线
资源字段是 Kubernetes 原生能力,不需要安装额外控制器。团队真正需要“启用”的通常是 namespace 级默认值和总量约束。下面建立一个隔离实验空间:LimitRange 为未声明资源的容器补默认 request/limit,并限制单容器最大值;ResourceQuota 则限制所有 Pod 的声明总量。
apiVersion: v1
kind: Namespace
metadata:
name: resource-baseline-lab
---
apiVersion: v1
kind: LimitRange
metadata:
name: container-baseline
namespace: resource-baseline-lab
spec:
limits:
- type: Container
defaultRequest:
cpu: 100m
memory: 64Mi
default:
cpu: 500m
memory: 256Mi
min:
cpu: 25m
memory: 16Mi
max:
cpu: "2"
memory: 2Gi
---
apiVersion: v1
kind: ResourceQuota
metadata:
name: declared-capacity
namespace: resource-baseline-lab
spec:
hard:
requests.cpu: "4"
requests.memory: 8Gi
limits.cpu: "8"
limits.memory: 16Gi
pods: "30"defaultRequest 决定省略 request 时注入什么,default 决定省略 limit 时注入什么;min、max 检查单个容器的边界。Quota 的 requests.* 约束调度承诺总量,limits.* 约束允许声明的峰值总量,pods 防止对象数量无限增长。这里的数值只服务隔离实验,生产值要由工作负载基线、namespace 预算、故障域和扩容时延计算。
kubectl apply -f resource-baseline.yaml
kubectl -n resource-baseline-lab get limitrange,resourcequota
kubectl -n resource-baseline-lab describe resourcequota declared-capacityLimitRange 默认值只在创建 Pod 时由准入逻辑写入对象,之后修改策略不会追溯更新旧 Pod。上线前可用服务端 dry-run 检查最终能否通过准入,再创建一个无资源字段的测试 Pod,读取 API 中已被补齐的值:
kubectl -n resource-baseline-lab run defaults-check \
--image=busybox:1.36.1 --restart=Never --dry-run=server -o yaml \
-- sh -c 'sleep 3600'
kubectl -n resource-baseline-lab run defaults-check \
--image=busybox:1.36.1 --restart=Never -- sh -c 'sleep 3600'
kubectl -n resource-baseline-lab get pod defaults-check \
-o jsonpath='{.spec.containers[0].resources}{"\n"}'应看到 request 被补为 100m/64Mi,limit 被补为 500m/256Mi。如果对象被拒绝,Event 或 API 错误会指出违反 LimitRange 或 ResourceQuota 的字段。团队应修正模板或预算,不要通过删除 quota 让未知工作负载直接进入共享节点。字段行为可对照 LimitRange 与 ResourceQuota 的官方说明。
QoS 是结果分类,不是独立优先级按钮
Kubernetes 根据最终资源声明给 Pod 计算 Guaranteed、Burstable 或 BestEffort。一个 Pod 要成为 Guaranteed,每个容器都必须同时具有 CPU 和内存 request、limit,且对应 request 等于 limit;只要不满足完整条件,但至少有一个容器声明了 CPU 或内存 request/limit,通常就是 Burstable;所有容器都没有这些声明时才是 BestEffort。可从 status.qosClass 读取结果,而不是靠肉眼猜模板。
kubectl -n resource-baseline-lab get pod \
-o custom-columns='NAME:.metadata.name,QOS:.status.qosClass,PHASE:.status.phase,NODE:.spec.nodeName'QoS 会影响节点资源紧张时的相对处境和 Linux OOM 分值,但它不是业务优先级的替代品,也不是绝对免死牌。Guaranteed Pod 仍可能因为自身容器超过 memory limit 而 OOM;当系统和高优先级负载需要资源时,它也不是永远不可驱逐。业务抢占顺序还受到 PriorityClass、实际用量相对 requests、节点条件、静态 Pod 和 kubelet 保留资源等因素影响。Pod QoS 官方文档适合核对分类条件,但事故判断仍要回到具体事件和容器终止状态。
Sidecar、日志代理和安全代理经常破坏团队预期的 QoS:主容器 request 等于 limit,但 sidecar 只声明了一个资源,整个 Pod 就不再是 Guaranteed。准入策略应检查 Pod 中每一个容器,而不是只检查名为 app 的容器。对突发型服务,也不必为了追求 QoS 标签机械地把 request 抬到 limit;那会牺牲装箱率。资源合同应先服务 SLO 和容量模型,QoS 是合同产生的结果。
正向实验:观察 CPU limit 产生节流
下面的 Pod 把 CPU request 与 limit 都设为 200m,因此在只有一个容器且同时声明内存时应归类为 Guaranteed。容器持续执行计算,并每隔五秒读取 cgroup v2 的 cpu.stat。实验要求 Linux 节点使用 cgroup v2;若目标集群仍暴露 cgroup v1,应改读对应的 cpu.stat 和 cpu.cfs_* 文件,不能混用字段名。
apiVersion: v1
kind: Pod
metadata:
name: cpu-throttle
namespace: resource-baseline-lab
spec:
restartPolicy: Never
containers:
- name: burner
image: busybox:1.36.1
command:
- sh
- -c
- |
while :; do :; done &
while :; do
echo "--- cpu.stat ---"
cat /sys/fs/cgroup/cpu.stat
sleep 5
done
resources:
requests:
cpu: 200m
memory: 32Mi
limits:
cpu: 200m
memory: 32Mikubectl apply -f cpu-throttle.yaml
kubectl -n resource-baseline-lab wait --for=condition=Ready pod/cpu-throttle --timeout=120s
kubectl -n resource-baseline-lab get pod cpu-throttle \
-o jsonpath='{.status.qosClass}{"\n"}'
kubectl -n resource-baseline-lab logs cpu-throttle --tail=80预期 QoS 为 Guaranteed。在 cgroup v2 输出中,持续负载下 nr_throttled 和 throttled_usec 应随采样增长;字段实际是否增长取决于节点内核、运行时与负载是否真正消耗完配额,因此要以目标环境输出为准。这个实验的关键不在 CPU 百分比,而在同一容器的配额证据随时间单调增加。若 kubectl top pod 可用,它可以作为利用率旁证,但不能替代 cgroup 节流计数。
把 limit 从 200m 提高到 1 需要创建新 Pod。对 Deployment,应修改模板并观察新 ReplicaSet;对这个一次性 Pod,先删除再用新文件重建。比较相同计算负载、相同采样窗口下的 throttled_usec 增量和业务耗时,才能判断 limit 是否是瓶颈。只看一次瞬时 top 很容易错过配额窗口。
反向实验:制造可判定的容器 OOM
内存反例使用固定 32Mi limit,让 Python 进程申请并触碰 96Mi 内存。触碰页面很重要:只保留虚拟地址不一定形成实际驻留内存。restartPolicy: Never 能保留一次终止状态,避免重启循环淹没第一证据。
apiVersion: v1
kind: Pod
metadata:
name: memory-oom
namespace: resource-baseline-lab
spec:
restartPolicy: Never
containers:
- name: allocator
image: python:3.12.4-alpine3.20
command:
- python
- -c
- |
import time
blocks = []
for _ in range(96):
block = bytearray(1024 * 1024)
block[0] = 1
blocks.append(block)
time.sleep(0.03)
time.sleep(3600)
resources:
requests:
cpu: 25m
memory: 16Mi
limits:
cpu: 250m
memory: 32Mikubectl apply -f memory-oom.yaml
kubectl -n resource-baseline-lab wait \
--for=jsonpath='{.status.phase}'=Failed pod/memory-oom --timeout=180s
kubectl -n resource-baseline-lab get pod memory-oom \
-o jsonpath='{.status.containerStatuses[0].state.terminated.reason}{" exit="}{.status.containerStatuses[0].state.terminated.exitCode}{"\n"}'
kubectl -n resource-baseline-lab describe pod memory-oom预期终止原因是 OOMKilled,常见退出码为 137。具体退出码和 Event 文本由运行时呈现,权威证据是容器 state.terminated.reason、前一次状态和节点内核/运行时日志的相互印证。如果镜像拉取失败、准入策略拒绝公共镜像或 Pod 无法调度,实验没有进入内存机制,必须先处理对应 Event,不能把 Pending 或 ImagePullBackOff 解释成 OOM。
把 memory limit 改大后重新创建 Pod,如果进程不再终止,只能证明新上限容纳了这段固定申请。生产 request 和 limit 仍需用真实流量的工作集、GC、页缓存、native memory、sidecar 以及发布重叠窗口测量,不能直接照搬 96Mi 这个实验数值。
容器 OOM、系统 OOM 与节点压力驱逐要分型
容器超过自身 memory limit 时,第一证据通常在该容器的终止状态,Pod 可能按重启策略继续留在原节点。节点压力驱逐则由 kubelet 根据 memory.available、nodefs.available、imagefs.available、inode 等 eviction signal 判断,驱逐 Pod 以回收资源;第一证据通常是 Pod phase、reason、Event 和 Node condition。系统级 OOM 可能在节点内核日志中出现,并不保证 Kubernetes Event 能完整解释受害进程。
kubectl get nodes -o custom-columns='NAME:.metadata.name,MEMORY_PRESSURE:.status.conditions[?(@.type=="MemoryPressure")].status,DISK_PRESSURE:.status.conditions[?(@.type=="DiskPressure")].status'
kubectl describe node <node-name>
kubectl get events -A --sort-by=.lastTimestamp
kubectl get pod <pod-name> -n <namespace> -o yaml节点压力下,kubelet 不只是按 QoS 名称机械排序。它会考虑 Pod 是否超出 requests、Priority 和超出量等因素;不可回收资源和节点保留配置也会改变结果。硬驱逐阈值可能立即动作,软阈值还伴随宽限期。排障时应保存 kubelet 的生效参数、Node condition 时间线、被驱逐 Pod 的 request/usage/Priority,以及同节点其他工作负载变化。节点压力驱逐文档给出了信号、阈值与选择逻辑。
磁盘压力也不能用调高 memory limit 修复。emptyDir、容器可写层、镜像垃圾、日志和 inode 都可能触发不同 signal。若 emptyDir.medium: Memory 使用 tmpfs,其页也会计入容器内存;没有设置 sizeLimit 时还可能让工作负载用掉远超预期的内存。资源基线应同时审查 CPU、内存、临时存储和对象数量,而不是只给主容器填两个字段。
requests 还会改变 HPA、VPA 与节点伸缩
资源型 HPA 的 CPU 利用率通常以当前用量除以 request 计算。相同的实际 CPU 使用量下,把 request 从 100m 改成 500m,利用率会从约 200% 变成约 40%,可能让 HPA 从扩容转为不动作。缺少对应 request 时,HPA 无法为该容器计算这种利用率。于是 request 既是调度承诺,也是控制器算法的分母,不能由应用团队和平台团队各自修改而不通知对方。
VPA 从历史和实时样本形成推荐时也会结合资源声明与策略;节点自动伸缩器通常根据不可调度 Pod 的 requests 模拟新增节点是否能容纳它,而不是根据 Pod 未来可能达到的 limits 申请节点。过大的 request 会过早扩节点,过小的 request 会让 Pod 先被塞入节点,运行峰值再通过节流、OOM 或驱逐暴露风险。
因此,资源变更评审至少要同时展示四组差异:调度 requests 总量、limits 过量承诺比例、HPA 目标利用率分母变化、节点池可容纳 Pod 数变化。只提交一段 YAML diff,无法让审核者看见控制环后果。
从工作集和并发反推容量成本
CPU 基线应区分稳态、突发和可延迟工作。对延迟敏感服务,可先用压测得到满足 SLO 的单 Pod 并发与 CPU,再用故障域冗余计算副本;对批任务,可允许更强节流,但要把完成时间纳入容量目标。内存则要观察稳定工作集、GC 后基线、启动峰值、缓存增长、native/堆外内存和 sidecar,而不是把 RSS 的一次峰值直接乘安全系数。
scheduled_request = sum(pod_requests) + system_reserved + kube_reserved
memory_peak = app_working_set + runtime_native + page_cache + sidecars + rollout_overlap
node_headroom = allocatable - scheduled_request - failure_domain_reserve生产阈值没有跨集群通用答案。一个合理的不变量是:在 N+1 节点故障或发布双版本重叠时,关键工作负载仍能调度;稳定负载窗口内,CPU 节流增量与业务尾延迟不存在持续同向恶化;内存工作集不会随轮次单调增长;节点压力驱逐不被当作常态调度器。成本评估同时看 request 导致的节点数量、limit 带来的峰值风险、空闲碎片、扩容时延和业务损失,而不是只追求最高平均利用率。
RBAC、敏感容量事实与字段所有权
开发者要读取 Pod 资源声明、状态和 Event,至少需要对应 namespace 的 get/list/watch;创建实验 Pod、LimitRange 或 ResourceQuota 需要各自资源的写权限。修改 namespace 配额会影响所有租户,通常应由平台角色审批,而不是把 cluster-admin kubeconfig 发给应用团队。读取 Node 详情和全局事件会暴露节点名称、实例形态、镜像、拓扑和容量水位,也应按运维敏感数据控制。
示例 YAML 不应包含真实镜像仓库凭证、节点名、租户名或业务标签。kubectl describe 与 Event 可能带镜像地址、调度标签和准入拒绝原因;对外发工单前保留时间、reason、字段和值的必要片段,并脱敏身份和拓扑。资源声明本身不是 Secret,但它能暴露服务规模和容量模型。
字段 owner 需要写进交付合同:应用团队负责基于性能证据提出 request/limit,平台团队维护 LimitRange、ResourceQuota、节点保留和驱逐策略,弹性控制器只能修改明确授权的副本或资源字段,GitOps 对控制器拥有的动态字段必须采用协作规则。任何机器人自动调整 requests 前,都应保存旧值、推荐窗口、变更原因和回退阈值。
变更、升级与安全退出
资源字段属于稳定核心 API,但节点内核、cgroup 版本、容器运行时和 Kubernetes 发行线会改变可见文件、指标与执行细节。升级节点前,在候选节点重复 CPU 节流与 OOM 小实验,比较 QoS、cgroup 文件、Event、终止状态和监控采集;不要只验证 Pod 能启动。若采用 Pod 级资源等新能力,还要先核对目标发行线的 feature gate、运行时支持和相邻控制器兼容性。
回滚 LimitRange 或 ResourceQuota 时,先恢复策略对象,再决定哪些工作负载需要滚动重建。删除 LimitRange 不会移除已写进旧 Pod 的默认值,调低 quota 也可能让后续滚动更新因总量不足而卡住。资源调整可先在一个低风险 Deployment 上执行,观察新旧 ReplicaSet 重叠期的 requests、Pending、节流、OOM、HPA 和节点扩容时间线,再扩大范围。
实验清理按对象依赖执行:
kubectl delete namespace resource-baseline-lab --wait=true
kubectl get namespace resource-baseline-lab删除 namespace 会清理本文创建的 Pod、LimitRange 和 ResourceQuota,但不会替团队撤销外部监控中的历史标签、工单附件或临时提权。若实验使用了专用节点池、临时镜像拉取凭证或云资源,还要分别核销。团队最终应能从一次延迟或驱逐事件追到 Pod 模板、准入默认值、调度记账、节点 cgroup 和容量 owner;只有这条链闭合,requests 与 limits 才不再是凭经验填写的数字。
