容量建模:冗余水位、发布峰值与 Rightsizing
一个服务平时只有三成 CPU 使用率,发布时新增副本却持续 Pending。节点监控显示还有大量空闲 CPU,团队因此判断调度器失灵;查看 Event 才发现,现有 Pod 的 CPU requests 已经占满 Node Allocatable,maxSurge 创建的新 Pod 没有可承诺空间。真实用量低不等于调度容量充足,发布峰值也不等于业务流量峰值。
另一个集群把所有 requests 统一下调一半,节点利用率迅速变得漂亮。流量突发时,多个容器同时超过低估的内存 request,节点进入 MemoryPressure;节点自动伸缩器仍按 requests 模拟扩容,无法从实际用量直接推断需要什么节点。节省出来的不是成本,而是把风险从账单转移到了 OOM、驱逐和恢复时间里。
容量账本必须同时保留承诺与消耗
Kubernetes 至少存在三本容易混淆的账。capacity 是节点报告的物理资源总量;allocatable 是扣除系统、kubelet 与驱逐保留后,可供 Pod 调度的上限;Pod requests 是调度器、ResourceQuota、HPA 利用率和节点自动伸缩器共同消费的声明。CPU、内存的实时 usage 则来自 kubelet、Metrics API 或监控系统,用来解释执行压力和 rightsizing,不会直接替代调度账。
调度器检查候选节点上已调度 Pod 的 requests 之和,而不是当前 usage。一个几乎空闲但 requests 已承诺完的节点会拒绝新 Pod;一个 requests 很低但 usage 很高的节点可能继续接收 Pod,随后才发生 CPU 节流、OOM 或节点压力驱逐。Kubernetes 的 容器资源管理说明明确了 requests、limits、调度和 cgroup 执行之间的关系。
容量模型因此不能只给一个“集群利用率”。至少按 CPU、memory、ephemeral-storage、Pod 数、扩展资源和关键 flavor 建立向量,并分别保存:总 capacity、Node Allocatable、系统保留、DaemonSet 固定税、工作负载 requests、实际 usage 分位数、发布附加量、故障附加量、不可放置碎片和待供给容量。任何一维先耗尽,整批 Pod 都可能无法放置。
先让采样链可用,再谈 Rightsizing
最小分析环境需要 kubectl、可读取 Node/Pod/Deployment/Event 的身份,以及可用的 Resource Metrics API。托管集群可能已提供指标链;若 kubectl top 返回 Metrics API not available,可在隔离或自管集群安装 Metrics Server 0.9.0。生产安装要先审阅固定发行资产和集群兼容性,不把远程内容直接管道给 API Server:
curl -fL -o metrics-server-components.yaml \
https://github.com/kubernetes-sigs/metrics-server/releases/download/v0.9.0/components.yaml
kubectl apply --server-side -f metrics-server-components.yaml
kubectl -n kube-system rollout status deploy/metrics-server
kubectl get apiservice v1beta1.metrics.k8s.io
kubectl top nodes
kubectl top pods -A --containers预期 APIService 为 Available=True,节点与 Pod 返回带时间窗口的 CPU、内存样本。样本缺失时检查 Metrics Server 日志、Service endpoints、聚合层 TLS、kubelet 证书和网络;不要使用 --kubelet-insecure-tls 把身份错误永久掩盖。Metrics Server 只保留近期资源快照,不提供长期分位数、业务吞吐、OOM 历史或发布阶段标记。长期建模仍需从观测平台获取一段覆盖稳态、峰值、发布和故障演练的序列。
Rightsizing 推荐可以从 VPA Off 模式或 Goldilocks 获取,但推荐值不是自动批准。VPA 的 target、lowerBound、upperBound 受到历史窗口、OOM、样本质量和策略边界影响;sidecar、初始化容器、JVM 堆外内存和周期性批任务都可能让短窗口失真。先在 Off 模式积累建议,再由业务 owner 对照延迟、吞吐和失败证据审批;自动更新模式会改变 Pod requests,必须另行评估中断与字段所有权。
用公式写出稳态、发布与故障三种需求
对资源维度 r,单工作负载稳态承诺可写成:
Steady(r) = replicas × request_per_pod(r) + init_overlap(r)
Release(r) = maxSurge × request_per_pod(r) + migration_job(r)
Failure(r) = replicas_lost_in_failure_domain × request_per_pod(r)
Required(r) = Steady(r) + max(Release(r), Failure(r), DemandBurst(r))这里没有固定的“预留百分之二十”。max() 表示先判断这些事件是否会重叠;若发布窗口允许遇到节点故障,Release 与 Failure 就不能互斥,必须相加。init_overlap 处理 init container 与 Pod 级调度请求的差异,migration_job 包括发布时临时数据库迁移、缓存预热和校验任务,DemandBurst 来自 HPA 在节点供给完成前可能创建的副本。
集群可承诺 headroom 也不是 allocatable - usage,而是:
SchedulableHeadroom(r) =
sum(NodeAllocatable(r))
- sum(ScheduledRequests(r))
- ReservedForFailure(r)
- ReservedForRelease(r)
- FragmentationLoss(r)当结果为正,也不保证某个 Pod 能放下。headroom 分散在十台各剩 200m CPU 的节点上,无法容纳请求 1 CPU 的单 Pod;内存足够的节点可能没有所需 GPU、zone 或卷拓扑。容量评审必须追加一次“代表性 Pod 能否放置”的约束求交,而不是把每台节点余量简单相加。
字段变化会沿多条控制链传播
调高 requests.cpu 会降低节点可容纳副本数,也会降低基于 CPU utilization 的 HPA 比值,因为 HPA 使用 usage / request;节点自动伸缩器会为 Pending Pod 模拟更大节点。调低 requests 会产生相反效果,但实际 CPU 使用不变时,HPA 更容易扩副本,节点层又可能因为声明太小而低估供给。一个字段同时被三个控制器消费,不能只从“单 Pod 成本”看待。
limits.cpu 主要进入 cgroup CPU 带宽执行,超过后通常表现为节流;limits.memory 超过后可能触发容器 OOM。limit 不是调度预留,只有 request 进入常规放置预算。若只写 limit 而省略 request,且 admission 没有设置默认值,Kubernetes 可能按规则复制 limit 为 request;这会把原本想表达的执行上限意外变成完整容量承诺。
Deployment 的 replicas 决定稳态副本,maxSurge 决定滚动发布可额外创建多少 Pod,maxUnavailable 决定发布期间允许损失多少旧副本。PDB 约束自愿中断,不替发布控制器保证容量;HPA 的 maxReplicas 决定需求侧上界,却不保证节点能按冷启动预算及时供给。TopologySpread、anti-affinity、taint 和 PVC zone 会把总容量切成多个不可互换的池。
节点的 kube-reserved、system-reserved 与 eviction threshold 共同影响 allocatable 和稳定水位。保留太小,系统 daemon 抢占 Pod 预算并触发节点压力;保留太大,可调度容量被静态浪费。Kubernetes 节点压力驱逐说明了 memory、磁盘和 inode 压力的执行路径,也强调应让调度容量与驱逐阈值一致,避免 Pod 一落地就跨过压力线。
正向实验:让发布峰值留在预算内
下面用 ResourceQuota 建一个确定性的发布预算。namespace 最多接受 2 CPU requests;Deployment 有 3 个副本,每个请求 400m,maxSurge: 1,因此稳态为 1.2 CPU,发布峰值为 1.6 CPU,仍在预算内。实验需要创建 namespace、quota 和 Deployment 的权限,不需要集群管理员身份。
apiVersion: v1
kind: Namespace
metadata:
name: capacity-lab
---
apiVersion: v1
kind: ResourceQuota
metadata:
name: request-budget
namespace: capacity-lab
spec:
hard:
requests.cpu: "2"
requests.memory: 1Gi
pods: "6"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: release-wave
namespace: capacity-lab
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: release-wave
template:
metadata:
labels:
app: release-wave
spec:
containers:
- name: hold
image: registry.k8s.io/pause:3.10
resources:
requests:
cpu: 400m
memory: 128Mi
limits:
cpu: 800m
memory: 256Mikubectl apply -f capacity-lab.yaml
kubectl -n capacity-lab rollout status deploy/release-wave
kubectl -n capacity-lab describe quota request-budget
kubectl -n capacity-lab patch deploy release-wave \
-p '{"spec":{"template":{"metadata":{"annotations":{"capacity.example/revision":"r2"}}}}}'
kubectl -n capacity-lab get deploy,rs,pod -w发布过程中预期短暂出现 4 个 Pod,quota 的 requests.cpu used 从 1200m 上升到 1600m,新 Pod Ready 后旧 Pod 逐个退出,used 回到 1200m。这个不变量比“发布成功”更有价值:发布附加量出现、被观察、随后归零,稳态承诺没有单调增长。若 Pod 因节点资源或镜像拉取失败而 Pending,quota 仍可能显示创建已获准,这说明 namespace 预算成立但物理供给链未成立。
kubectl -n capacity-lab get resourcequota request-budget -o yaml
kubectl -n capacity-lab get events --sort-by=.metadata.creationTimestamp
kubectl get nodes -o custom-columns=NAME:.metadata.name,CPU:.status.allocatable.cpu,MEM:.status.allocatable.memory
kubectl describe node <candidate-node>反向实验:让 maxSurge 撞上配额上限
把单 Pod CPU request 改为 600m,稳态变成 1.8 CPU,而一个 surge Pod 会把发布峰值推到 2.4 CPU。旧副本因为 maxUnavailable: 0 不能先删,新副本又被 quota 拒绝,Rollout 将稳定停住:
kubectl -n capacity-lab set resources deploy/release-wave \
--requests=cpu=600m,memory=128Mi \
--limits=cpu=900m,memory=256Mi
kubectl -n capacity-lab rollout status deploy/release-wave --timeout=90s
kubectl -n capacity-lab describe deploy release-wave
kubectl -n capacity-lab get events --sort-by=.metadata.creationTimestamp | tail -n 20
kubectl -n capacity-lab describe quota request-budget预期 rollout status 超时,ReplicaSet Event 出现 FailedCreate,错误指出 requests.cpu 超过 request-budget;旧 ReplicaSet 仍保留 3 个可用副本。这个反例稳定揭示了“稳态刚好放下,发布没有 headroom”的机制。若先看到 Insufficient cpu,说明 API 配额允许创建,但节点 requests 容量先耗尽;两种失败的修复责任不同。
可以用三种方式解除阻塞,每种都代表架构取舍。把 request 恢复到经测量的值,是 rightsizing;临时提高 quota,是在 namespace 权利层借容量;把 maxUnavailable 调为 1,是用发布期间可用性换容量。任何变更都应先保存 Deployment 和 quota,再观察新 ReplicaSet 收敛,不能同时修改三项后只记录“恢复成功”。
kubectl -n capacity-lab set resources deploy/release-wave \
--requests=cpu=400m,memory=128Mi \
--limits=cpu=800m,memory=256Mi
kubectl -n capacity-lab rollout status deploy/release-wave
kubectl -n capacity-lab describe quota request-budgetN+1 要按故障域和替代时间计算
“N+1 节点”只有节点同构、工作负载可任意迁移时才勉强成立。真实集群应按 zone、节点池、架构、GPU 型号、存储拓扑和污点分别计算。某 zone 损失一台节点后,其他 zone 是否允许违反 topology spread、PVC 是否能挂载、剩余节点是否容纳最大不可分 Pod,决定了那份 +1 是否真的可用。
故障冗余还要和替代时间绑定。节点自动伸缩器从发现 Unschedulable Pod,到云实例创建、Node 注册、CNI/CSI/DaemonSet 初始化、镜像拉取和应用 Ready,构成完整冷启动时间。若业务恢复预算短于这条链,就必须保留热 headroom;若可等待且任务可重试,可以用动态供给换成本。Kubernetes 节点自动伸缩说明明确指出,供给与整合主要按 Pod requests 和调度约束判断,不直接按实际 usage。
对 Deployment,可用故障附加量近似为失效域内副本 requests,加上重建期间 HPA 可能产生的新增副本;对批作业,还要考虑 gang 最小成员和队列 admission;对 DaemonSet,新节点每增加一台都先支付一份固定税。N+1 的验收应做真实 cordon/drain 或节点池隔离演练,观察失效到业务 Ready 的时间线,而不是只在表格里多写一台机器。
碎片率解释了为何总余量够却放不下
CPU 和内存碎片来自 Pod 资源比例与节点规格不匹配。例如节点各剩 1 CPU + 1Gi,待调度 Pod 请求 500m + 4Gi,集群 CPU 总余量再多也无效。GPU、hugepages、Pod IP、volume attachment 和可用 Pod 数都是更硬的碎片维度。只用 sum(requests) / sum(allocatable) 会把这些不可互换空间全部抹平。
可操作的碎片指标应围绕代表性 Pod 定义:对每类 workload template,统计有多少节点同时满足资源、label、taint、topology 和 volume 条件;再记录“总 headroom 为正但可行节点数为零”的持续时间。节点自动供给器选择规格时,也要检查 DaemonSet overhead、实例可用性与最小 Pod 尺寸,防止创建一个总资源足够但仍放不下目标 Pod 的节点。
整合控制器同样按 requests 模拟迁移。requests 虚高会阻止可安全整合的节点,虚低则可能把实际高用量 Pod 压到一起,造成执行期争用。rightsizing 与节点整合必须串联验证:先让请求接近可解释的业务需求,再观察 consolidation 是否减少碎片且不提高节流、OOM、驱逐和延迟。
Rightsizing 不是把 request 调到平均值
CPU 可压缩,短时超出 request 通常还能竞争空闲周期;内存不可压缩,低估更容易触发 OOM 或节点压力。CPU request 可以结合 usage 分位数、延迟拐点、节流与 HPA 行为设置;memory request 要覆盖工作集、缓存、堆外、启动峰值和 OOM 历史,并给运行时保留空间。平均值会抹掉发布预热、GC、模型加载和批处理尖峰。
推荐流程先按容器拆分主进程、sidecar 与 init container,再把 usage 时间线和业务事件对齐。检查 VPA target 是否长期越出当前 request、lowerBound 是否在低峰稳定、upperBound 是否由异常峰值拉高;随后只改一类 workload,在相同负载下比较 Ready 时间、CPU throttling、OOM、延迟、HPA 副本与节点 requests。连续多个业务周期稳定后再扩大范围。
原地 resize 可以减少重建,但不是所有集群与策略都支持。Kubernetes 1.35 起容器 CPU/内存原地 resize 稳定,实际使用仍要检查服务端版本、kubelet、runtime 和 resize policy;期望资源、已分配资源与实际生效资源可能在过渡期不同。官方 容器原地资源调整任务给出了 resize 子资源和 status 证据。使用 VPA 时还要核对其发行线与 update mode,不能仅因 Kubernetes 支持原地调整就假定 VPA 会采用同一路径。
权限、凭证和容量数据同样需要边界
容量分析的只读身份通常需要读取 Node、Pod、controller、Event、ResourceQuota、HPA、VPA 和 metrics API;修改 requests、quota、节点池或 autoscaler 则应拆成不同角色。普通业务团队不应修改 Node Allocatable、ClusterQueue 或全局默认资源;平台团队也不应在没有业务 owner 确认的情况下自动应用 VPA 推荐。
Pod 名、namespace、node label、实例类型、GPU 型号、requests 和 usage 能揭示租户规模、成本与业务峰值。导出报告时移除环境变量、Secret 引用值、内部域名和云账号;Prometheus 查询凭证使用只读短期身份,不写入文章、Git 或共享脚本。Metrics Server、VPA recommender 和成本平台各自保存不同粒度的数据,访问策略不能因为都叫“监控”就合并。
供应链上固定 Metrics Server、VPA、Goldilocks 与 autoscaler 的镜像 digest 或受控版本,记录 CRD 变更和权限增量。容量工具一旦拥有 patch workload 或管理节点池的权限,就从观察者变成执行控制器;上线评审必须重新审查爆炸半径和回滚路径。
成本模型要把延迟与浪费放在同一张账上
静态 headroom 的成本是持续空闲,动态供给的成本是冷启动和失败供给,rightsizing 过度的成本是节流、OOM、驱逐与副本膨胀,碎片的成本是买了却不可放置。节点单价只能解释其中一部分。按 workload 和故障域统计 requests 成本、实际 usage、发布附加量、空闲可行容量、Pending 时间、扩容失败、抢占重算和外部许可证,才能知道优化是否真的省钱。
不要用一个固定利用率目标管理所有集群。在线低延迟服务、可中断批处理、GPU 训练和控制面组件的恢复预算不同;同一利用率在不同 workload mix 下代表完全不同的风险。生产阈值由业务 SLO、故障演练、供应延迟和预算共同决定,示例中的 2 CPU 只证明发布波形,不是可复制的容量标准。
选型要看是谁改变容量事实
只需要看当前样本时,Metrics Server 与 kubectl top 足够,但不能做长期分位数。需要建议 requests 时,VPA Off 或 Goldilocks 提供起点;需要自动调整单 Pod 资源时,VPA 要承担字段所有权和中断治理;需要按负载改副本时选择 HPA 或 KEDA;需要把 Pending Pod 转成节点时选择 Cluster Autoscaler、Karpenter 或托管节点供给;需要控制批任务何时获得配额时使用 Kueue。
这些工具不能互相替代,也不应同时写同一字段。GitOps、人工变更和 VPA 同时管理 requests 会持续漂移;HPA 按 CPU utilization 工作时,VPA 修改 CPU request 会改变 HPA 分母;节点自动伸缩器与 rightsizing 的组合会改变供给和整合结果。选型输出必须包含字段 owner、输入指标、失败策略、最大动作幅度、冷却窗口和退出方式。
清理实验并保留可回滚证据
实验结束先保存正反两轮的 quota、Deployment、ReplicaSet 和 Event,再删除 namespace。namespace 删除会级联清理 Deployment、Pod、ResourceQuota;若长时间 Terminating,检查其中 finalizer 和 admission webhook,不要强删系统对象。
kubectl -n capacity-lab get quota,deploy,rs,pod -o yaml > capacity-lab-final.yaml
kubectl -n capacity-lab get events --sort-by=.metadata.creationTimestamp \
> capacity-lab-events.txt
kubectl delete namespace capacity-lab --wait=true
kubectl get namespace capacity-lab若 Metrics Server 是为隔离实验临时安装,使用最初保存的同一发行 manifest 删除,并确认 APIService、Deployment、Service、RBAC 和聚合发现都已消失:
kubectl delete -f metrics-server-components.yaml
kubectl get apiservice v1beta1.metrics.k8s.io
kubectl -n kube-system get deploy,service | grep metrics-server共享集群不要因为单篇实验卸载公共指标链。回滚 rightsizing 时恢复 Git 中上一版 requests、limits、HPA target 与 rollout strategy,观察新旧 ReplicaSet、业务延迟和节点调度,而不是只执行 rollout undo 后结束。
升级和退出要保住历史可比性
升级 Kubernetes、Metrics Server、VPA、节点自动伸缩器或实例规格前,导出 Node Allocatable、workload requests、usage 查询定义、VPA recommendation、HPA behavior、发布策略和节点池约束。升级可能改变默认值、稳定 API、指标字段或 resize 行为;若查询口径同时变化,升级前后的利用率和 headroom 就不能直接比较。
候选版本先在隔离节点池重放代表性稳态、发布、扩容和节点故障场景。门禁不是组件 Pod 可用,而是 requests 账一致、指标新鲜、发布附加量归零、N+1 演练在恢复预算内、碎片没有恶化、控制器没有争夺字段。失败时冻结自动建议和节点整合,恢复上一版控制器与配置,再确认遗留 CRD、webhook 和云实例是否清零。
退出自动 rightsizing 时先把推荐切回观察模式,再把 requests 的唯一所有权交还 Git 模板;退出节点自动供给时先保留静态基础容量,再停止新供给、迁移工作负载、排空动态节点并撤销云权限。直接删除控制器会留下无人管理的声明、节点或外部资源。
用责任合同维持容量模型
业务团队负责单 Pod 画像、启动峰值、SLO、并发和发布策略;平台团队负责 Node Allocatable、系统保留、节点池、调度约束与指标链;FinOps 负责单价和预算事实;值班团队负责从 Event、status、指标和日志判断失败层。任何人修改 requests、maxSurge、HPA 上界、quota、节点规格或故障域,都要同步更新容量模型并触发对应实验。
长期检查围绕可观察的不变量展开:稳态 requests 与审批基线一致;发布后附加 requests 回到稳态;失去一个承诺故障域后代表性 Pod 仍可放置或在恢复预算内获得新节点;Pending 能明确归因于配额、资源、约束或供给;rightsizing 后节流、OOM、驱逐、延迟和成本没有向坏方向迁移;退出工具后字段 owner、CRD、webhook、云资源和凭证全部归零。
容量模型的价值不在于预测一个永远准确的数字,而在于让每次变化都有因果:request 为什么是这个值,headroom 为哪种事件保留,哪一类碎片不能共享,冷启动需要多久,失败后由谁扩容、回滚和买单。只要这些问题能从同一份证据链回答,集群就不会再用“平均利用率还不高”解释所有 Pending,也不会用盲目下调 requests 冒充 Rightsizing。
