requests 决定 Pod 能否被调度,也影响节点的装箱密度和资源抢占时的保障。它写进 PodSpec 后通常不会自行变化,工作负载却会随着业务迭代、流量起伏、缓存预热和依赖延迟不断改变。完全靠人工维护一组固定值,很快就会出现偏差,进而影响集群的资源利用率和稳定性。
如果团队内无预算控制~ 那么恭喜你,已经解决了这个问题 🎉
VPA(Vertical Pod Autoscaler)会持续观察工作负载,为容器生成 CPU 和内存 recommendation,并根据策略决定是否应用到 Pod。本文关注 recommendation 怎样产生并作用到 Pod,以及生产接入前需要确认哪些限制。常用配置放在前面,便于后文说明各组件的行为。
版本说明
本文基于 VPA
vertical-pod-autoscaler-1.7.0,对应源码 commit6a616ea0c5ea0cb6111240073a9273b3467c064e。字段、默认值、更新模式和源码路径均以该版本为准。
1. VPA 解决什么问题#
Kubernetes 调度器根据 Pod 的 requests 判断节点是否有足够资源,它不会预测容器之后的真实用量。requests 因此会影响调度位置、节点装箱密度,以及资源抢占时的保障。
requests 过低时,节点可能被调度进过多 Pod,CPU 抢占和内存压力会在运行时才暴露;requests 过高时,Pod 会占用过多可调度容量,造成节点利用率下降、Pod Pending,甚至触发不必要的节点扩容。固定值还会随工作负载变化逐渐偏离实际需求。
假设一个订单服务最初为每个 Pod 配置了 200m CPU / 512Mi 的 requests。当时这个值能够覆盖日常用量;后来版本升级并启用了本地缓存,单副本的稳定用量升到约 600m CPU / 900Mi,requests 却没有同步调整。调度器仍按旧值为 Pod 选择节点,多个副本被放到同一节点后,实际总用量可能超过节点能够提供的资源,最终出现 CPU 争用和节点内存压力。配置本身没有变化,但它已经不再代表应用的真实需求。
如果单个 Pod 的 CPU、内存需求经常变化,人工又很难长期维护准确的 requests,就可以考虑 VPA。Recommender 根据历史用量和策略限制计算建议;Updater 与 Admission Controller 再按 updateMode 处理存量或新建 Pod。
| 组件 | 调整对象 | 回答的问题 |
|---|---|---|
| VPA | 单个 Pod 中容器的 CPU / 内存 | 一个副本应该申请多少资源 |
| HPA | 工作负载副本数 | 当前负载需要多少个副本 |
| Cluster Autoscaler | 集群节点数 | 集群是否需要增减节点 |
三者可以组合起来使用,但要注意避免控制环互相影响。例如 HPA 按 CPU 利用率扩缩容时,利用率的分母与 CPU request 有关;VPA 同时修改这个 request,就会改变 HPA 看到的信号。实践中通常让 VPA 和 HPA 控制不同资源(根据 cpu -> HPA, mem -> VPA),或者让 HPA 使用业务中的自定义指标。
需要强调的是,VPA 不了解业务延迟目标,不提供集群的承载力保证,也不能保证 recommendation 一定能被当前节点、ResourceQuota 或其他资源约束接受。一定不要完全的依赖 VPA 来进行调控,一定需要人工进行审查调优。
2. 常用配置与 recommendation#
VPA v1.7.0 的完整字段可以查阅官方 VPA API 文档。日常配置不需要把所有可选项都写进去。下面这个对象只用于观察 recommendation,适合作为接入起点:
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: checkout
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: checkout
updatePolicy:
updateMode: "Off"
resourcePolicy:
containerPolicies:
- containerName: app
minAllowed:
cpu: 100m
memory: 128Mi
maxAllowed:
cpu: "2"
memory: 2Gi
controlledResources: ["cpu", "memory"]
controlledValues: RequestsOnly
- containerName: sidecar
mode: "Off"targetRef 指向被管理的工作负载,VPA 通过目标 selector 匹配 Pod,但不会修改副本数。updateMode: Off 让它只生成 recommendation,暂时不应用普通资源更新。
minAllowed 和 maxAllowed 限制推荐范围,取值需要结合应用的最小可运行规格、成本约束和最大节点容量。controlledResources 决定要管理 CPU、内存中的哪些资源。
这里显式使用 controlledValues: RequestsOnly,所以只修改 requests。默认值 RequestsAndLimits 还会按原有 request/limit 比例调整 limits。sidecar 通过 mode: Off 排除;容器很多时,可以再用 containerName: "*" 补一条兜底策略。
2.1 updateMode#
updateMode 只控制 recommendation 怎样作用到 Pod。即使设为 Off,Recommender 也会继续计算并更新 Status。
| 模式 | 新建 Pod | 存量 Pod | 关键边界 |
|---|---|---|---|
Off |
不应用普通 recommendation | 不更新 | Recommender 仍写入 Status |
Initial |
CREATE 时应用 | 不做普通存量更新 | 适合随自然发布逐步生效 |
Recreate |
CREATE 时应用 | Updater 驱逐并触发重建 | 省略 updateMode 时的默认值 |
InPlaceOrRecreate |
CREATE 时应用 | 先尝试 /resize,确认无法完成后可回退驱逐 |
依赖集群 Pod resize 能力 |
InPlace |
CREATE 时应用 | 只尝试 /resize |
还需要 VPA InPlace feature gate;失败不驱逐 |
Auto 在 v1.7.0 已弃用,执行上等价于 Recreate。生产配置最好写明具体模式。省略 updateMode 时会使用 Recreate,这个默认行为可以从公共辅助函数中确认:
// pkg/utils/vpa/api.go
func GetUpdateMode(vpa *vpa_types.VerticalPodAutoscaler) vpa_types.UpdateMode {
if vpa.Spec.UpdatePolicy == nil ||
vpa.Spec.UpdatePolicy.UpdateMode == nil ||
*vpa.Spec.UpdatePolicy.UpdateMode == "" {
return vpa_types.UpdateModeRecreate
}
return *vpa.Spec.UpdatePolicy.UpdateMode
}完整实现见固定版本的 API helper。
2.2 recommendation 怎么读#
Recommender 把结果写入 status.recommendation.containerRecommendations:
status:
recommendation:
containerRecommendations:
- containerName: app
lowerBound:
cpu: 120m
memory: 256Mi
target:
cpu: 300m
memory: 512Mi
upperBound:
cpu: 800m
memory: 1Gi
uncappedTarget:
cpu: 340m
memory: 560Mitarget经过 ResourcePolicy 裁剪,是准备应用到 Pod 的目标值。[lowerBound, upperBound]是推荐区间。当前 requests 落到区间外时,Updater 会把它视为更新信号;这个区间不代表性能承诺。uncappedTarget保留策略裁剪前的 target,适合用来判断 min/max 是否长期限制推荐。它已经经过分位数估算、margin 和格式化,不能当作原始用量。
这些值来自同一份聚合历史,差别出现在估算和策略处理阶段。下一节先看 CPU、内存样本怎样进入 Recommender,以及 Checkpoint 保存了哪部分历史。
3. VPA 的控制闭环#
VPA 由三个独立运行的组件组成。它们通过 API Server 中的 VPA Status 传递 recommendation,Checkpoint 则让 Recommender 重启后仍能接上之前的聚合历史。
Recommender 读取 VPA、Pod、指标和 OOM 事件,聚合历史后写入 status.recommendation。默认配置下,它也负责保存和恢复 Checkpoint,但不会修改 Pod。
Updater 面向已经运行的 Pod。它读取 recommendation,再结合更新模式、预期收益和可用性限制,决定保持现状、请求 /resize,还是发起 eviction。
Admission Controller 只处理 Pod CREATE。它在准入阶段读取已有 recommendation,返回修改 requests/limits 的 JSON Patch;recommendation 的计算和 Pod 的持久化都不在这里完成。
走 recreate 路径时,Updater 只负责驱逐旧 Pod。Deployment、StatefulSet 等工作负载控制器补足副本,新 Pod 的 CREATE 请求经过 Admission Controller 后,recommendation 才会进入 PodSpec。
注意:VPA 不会修改 workload template,也就是 Deployment、StatefulSet、CronJob 等中的 PodTemplateSpec。
4. Recommender 与 Checkpoint#
Recommender 以同一 VPA 下、同名容器的历史分布为计算对象,单个瞬时指标只是一条样本。每轮循环会刷新 VPA 与 Pod 的对应关系,加载最新指标并计算 recommendation,最后维护 Checkpoint:
// pkg/recommender/routines/recommender.go
func (r *recommender) RunOnce() {
ctx := context.Background()
r.clusterStateFeeder.LoadVPAs(ctx)
r.clusterStateFeeder.LoadPods()
r.clusterStateFeeder.LoadRealTimeMetrics(ctx)
r.UpdateVPAs()
stepCtx, cancelFunc := context.WithDeadline(
ctx, time.Now().Add(r.checkpointsWriteTimeout))
defer cancelFunc()
r.MaintainCheckpoints(stepCtx)
// Aggregate state garbage collection omitted.
}完整入口见 Recommender 周期循环。当 --storage=prometheus 时,启动历史改从 Prometheus History Provider 获取;它与默认 Checkpoint 存储是替代关系,不会叠加两份历史。
4.1 从样本到 recommendation#
CPU 和内存面对的问题不同,采样方式也不一样:
- CPU 消耗适合用分布描述,每个有效 usage sample 都会进入 CPU 直方图。
- 内存侧更关注峰值和 OOM。同一聚合区间内只保留内存峰值,默认区间为 24 小时。OOM 事件还会补一条高于当前 request 或近期峰值的人工样本,避免推荐继续停在已经证实不足的位置。
聚合入口把两类样本写入各自的衰减直方图:
// pkg/recommender/model/aggregate_container_state.go
func (a *AggregateContainerState) AddSample(sample *ContainerUsageSample) {
switch sample.Resource {
case ResourceCPU:
a.addCPUSample(sample)
case ResourceMemory:
a.AggregateMemoryPeaks.AddSample(
BytesFromMemoryAmount(sample.Usage), 1.0, sample.MeasureStart)
}
}CPU 与内存默认都使用 24 小时的衰减半衰期。旧样本不会到期后一次性删除,它们的影响力(权重)会随时间降低。样本入口和状态字段可以从 AggregateContainerState 继续追踪。
v1.7.0 默认用 90 分位生成 target,再增加 15% 安全冗余。lower/upper bound 分别取 50/95 分位,并根据历史充分程度调整;历史较短时区间会更宽,少量样本因此不容易触发更新。估算完成后,controlledResources 先过滤不由 VPA 管理的资源,minAllowed / maxAllowed 再裁剪结果。
这些默认值可以通过 Recommender 参数调整。Estimator 组合 中可以看到分位数、margin 和 confidence 的包装顺序,其他入口列在文末源码索引中。
4.2 Checkpoint 保存什么#
默认存储模式下,CheckpointWriter 会为每个 VPA、每个聚合后的容器名维护一个 VerticalPodAutoscalerCheckpoint。正常循环写入衰减后的 CPU/内存直方图和样本元数据。Recommender 重启后,InitFromCheckpoints 先恢复 AggregateContainerState,新样本再接着写入。
序列化结构列出了实际保存的字段:
// pkg/recommender/model/aggregate_container_state.go
return &vpa_types.VerticalPodAutoscalerCheckpointStatus{
LastUpdateTime: metav1.NewTime(time.Now()),
FirstSampleStart: metav1.NewTime(a.FirstSampleStart),
LastSampleStart: metav1.NewTime(a.LastSampleStart),
TotalSamplesCount: a.TotalSamplesCount,
MemoryHistogram: *memory,
CPUHistogram: *cpu,
Version: SupportedCheckpointVersion,
}, nil一份实际的 Checkpoint Status 如下:
status:
cpuHistogram:
bucketWeights:
"0": 10000
"1": 473
referenceTimestamp: "2026-07-27T00:00:00Z"
totalWeight: 654.46878060585
firstSampleStart: "2026-07-27T03:34:07Z"
lastSampleStart: "2026-07-28T10:10:21Z"
lastUpdateTime: "2026-07-28T10:10:30Z"
memoryHistogram:
bucketWeights:
"10": 10000
referenceTimestamp: "2026-07-28T00:00:00Z"
totalWeight: 4.4441318936248315
totalSamplesCount: 3674
version: v3| 字段 | 含义 |
|---|---|
cpuHistogram / memoryHistogram |
CPU usage sample 与内存区间峰值各自形成的衰减直方图。两个直方图独立保存。 |
bucketWeights |
键是内部指数直方图的桶编号,值是量化后的相对权重。保存时最重的桶会缩放为 10000,所以这里的整数不是样本数。 |
totalWeight |
直方图中所有样本经过时间衰减后的浮点总权重。恢复时用它把整数桶权重还原为近似的浮点权重。 |
referenceTimestamp |
计算衰减权重时使用的时间基准。它不表示第一条样本的时间,CPU 与内存的基准也可以不同。 |
firstSampleStart / lastSampleStart |
最早和最近的 CPU sample 时间。v1.7.0 只在写入 CPU sample 时更新这两个字段。 |
totalSamplesCount |
累计接收的 CPU sample 数量,同样不包含内存峰值样本。 |
lastUpdateTime |
本次 Checkpoint Status 完成序列化的时间。 |
version |
序列化格式版本。v1.7.0 写入 v3,加载其他版本时会报错。 |
样例中,CPU 权重落在桶 0 和 1,二者的整数权重是 10000:473,约占保留权重的 95.5% 和 4.5%,不是 10000 条与 473 条 CPU sample。内存权重全部落在桶 10。按 v1.7.0 的默认分桶,CPU 桶 0、1 分别覆盖 [0, 0.01) 和 [0.01, 0.0205) core,内存桶 10 约覆盖 [125.8 MB, 142.1 MB)。
totalSamplesCount: 3674 记录的是 CPU sample 数,而 totalWeight: 654.468... 是 CPU 样本经过衰减后的总权重,两者不能直接比较。内存侧的 totalWeight: 4.444... 也不是内存样本数。保存时会丢弃量化后为零的小权重桶,恢复过程只能近似重建原直方图。
Checkpoint 只保存聚合状态,逐条原始指标和用户配置都不在其中。它让 Recommender 重启后可以延续已有历史。观察期如果没有覆盖高峰、发布或批处理周期,留下来的仍是一段不完整历史,recommendation 也就不够准确,因此实践中往往从 Off 模式开始,让观察期覆盖一个完整的业务周期,然后再切换到 Initial 模式(或直接用 InPlace)。
保存逻辑见 Checkpoint Writer 直方图的量化与恢复见 Histogram 衰减时间基准见 Decaying Histogram 对象结构见 Checkpoint CRD。
5. Recommendation 如何作用于 Pod#
Recommender 写入 Status 后,recommendation 才成为更新输入。存量 Pod 由 Updater 评估;新建或 eviction 后重建的 Pod 则由 Admission Controller 在 CREATE 时处理。
5.1 Updater:处理存量 Pod#
有了 recommendation,Updater 也不会立刻更新 Pod。出现以下任一信号后,Pod 才会进入候选队列:
- 至少一个受控容器的当前 request 落在
[lowerBound, upperBound]之外。 - request 仍在区间内,但 Pod 已运行超过配置的生命周期阈值,且 target 与当前资源差异达到更新阈值;v1.7.0 默认分别为 12 小时和 10%。
- 受 VPA 管理的容器发生 quick OOM,而且 recommendation 确实会改变资源。
进入候选队列还不够。Pod 随后要通过策略方向、副本可用性和限速检查。minReplicas 默认是 2,所以单副本工作负载即使已有 recommendation,通常也不会被 Updater 主动处理。Updater 默认还会检查 Admission Controller 的健康状态,避免 eviction 后补出的 Pod 拿不到推荐资源。
主循环按 updateMode 选择更新路径:
// pkg/updater/logic/updater.go
if updateMode == vpa_types.UpdateModeOff ||
updateMode == vpa_types.UpdateModeInitial {
continue
}
if updateMode == vpa_types.UpdateModeInPlaceOrRecreate ||
(updateMode == vpa_types.UpdateModeInPlace && inPlaceFeatureEnabled) {
podsForInPlace = u.getPodsUpdateOrder(
filterNonInPlaceUpdatablePods(
podsAvailableForUpdate, inPlaceLimiter, vpa, u.infeasibleAttempts),
vpa)
} else {
if updateMode == vpa_types.UpdateModeInPlace {
continue
}
podsForEviction = u.getPodsUpdateOrder(
filterNonEvictablePods(podsAvailableForUpdate, evictionLimiter), vpa)
}InPlace 失败时不会改走 eviction;InPlaceOrRecreate 只有在 resize 被确认无法完成后才可能回退。原地更新仍可能带来扰动,容器的 resizePolicy 可能要求重启,节点余量不足也会让 resize 长期无法完成。
recreate 路径调用 Kubernetes Eviction API,因此会受到 PDB 约束;/resize 不经过 PDB,但仍受 VPA 自身的副本可用性和限速控制。候选和两类限制的入口列在文末 Updater 源码索引中。
5.2 Admission Controller:处理新 Pod#
Admission Controller 接收 core/v1 Pod 的 CREATE 请求。它根据 Pod 的上级控制器和目标 selector,在同一 namespace 中查找 controlling VPA。没有匹配项,请求会原样放行,不添加 VPA 资源 patch。matcher 最终只会选择一个 controlling VPA,因此生产环境要避免多个 VPA 的 selector 重叠。
匹配成功后,provider 读取已有的 status.recommendation,再应用 ResourcePolicy 和 namespace LimitRange。此时如果还没有 recommendation,普通资源 patch 就保持为空;Admission Controller 不会等待 Recommender,也不会现场估算。controlledResources 决定修改哪些资源,controlledValues 则决定只改 requests,还是让已有 limits 随 requests 按比例变化。
handler 先匹配 VPA,再交给 calculators 生成 patch:
// pkg/admission-controller/resource/pod/handler.go
controllingVpa := h.vpaMatcher.GetMatchingVPA(ctx, &pod)
if controllingVpa == nil {
return []resource_admission.PatchRecord{}, nil
}
for _, c := range h.patchCalculators {
partialPatches, err := c.CalculatePatches(&pod, controllingVpa)
if err != nil {
return []resource_admission.PatchRecord{}, field.ErrorList{
field.InternalError(field.NewPath("."), err)}
}
patches = append(patches, partialPatches...)
}calculators 的结果会组成 AdmissionResponse 中的 JSON Patch。API Server 应用 patch 后,再校验并持久化 Pod。Admission Controller 不修改 Deployment/StatefulSet template,也不回写 recommendation;matcher 和 provider 的入口列在文末源码索引中。
recreate 因此依赖 Admission Controller 保持健康。即使 eviction 已经成功、工作负载控制器也补出了 Pod,只要准入 patch 失效,新 Pod 仍会沿用 workload template 中的旧 requests。
6. 生产接入与边界#
6.1 从 Off 开始#
如果要把 VPA 接入生产,我会显式从 Off 开始,而不是创建对象后依赖默认的 Recreate。
- 先检查 VPA Condition、Recommender 日志,确认所有目标容器都有 recommendation。
- 观察高低峰、工作日/周末、发布、缓存预热和批处理。业务周期是否覆盖,比“已经运行 N 天”更有参考价值。
- 把 target 与 CPU throttling、OOM、延迟、错误率、Pod Pending 和节点 allocatable 一起看。recommendation 稳定,并不能说明它满足 SLO。
- 根据应用的最小可运行规格和最大节点容量设置 min/max。若目标只是调整调度申请量,优先显式使用
RequestsOnly。 - 先在无状态、多副本、启动快且 readiness probe 可靠的服务上灰度,再评估 PDB、节点余量和回滚路径。
- 把基线资源留在 workload template 中。切回
Off只会停止后续普通更新,已经改变的 Pod 仍要通过正常发布回滚。
观察期常用命令如下:
kubectl get vpa checkout -n shop -o yaml
kubectl describe vpa checkout -n shop
kubectl get pod -n shop -l app=checkout \
-o custom-columns='NAME:.metadata.name,CPU:.spec.containers[0].resources.requests.cpu,MEM:.spec.containers[0].resources.requests.memory'target 和 uncappedTarget 需要放在一起看。二者长期不相等,说明策略限制一直在裁剪推荐。此时要检查护栏是否合理,以及 workload 是否已经超出原设计容量。切换更新模式后,还要核对实际 Pod requests、Events 和 Admission annotations;Status 中出现 recommendation,只能证明推荐已经生成,不能证明它成功作用到了 Pod。
排查 Checkpoint 时,先确认对象对应的 VPA 和容器,再看 lastUpdateTime 是否推进、CPU 样本的起止时间和数量是否符合预期。然后检查指标链路,以及 Recommender 的版本、RBAC 和写入错误。Checkpoint 不是原始指标审计记录,其中的直方图状态也不适合手工修正。
6.2 生产边界#
| 边界 | 实践建议 |
|---|---|
| VPA 与 HPA | 不要让两者同时控制同一个 CPU/内存信号。可让 VPA 只管内存、HPA 按 CPU 扩副本,或让 HPA 使用业务指标。 |
| 节点容量 | recommendation 可能超过节点 allocatable、ResourceQuota 或 Pod 级约束。maxAllowed 应考虑 Pod 内所有容器之和和最大节点规格。 |
| 更新可用性 | Recreate 同时受 PDB、VPA minReplicas、副本状态和启动时长影响。单副本服务通常不适合自动驱逐。 |
| in-place | 它减少而非消除扰动。容器可能按 resizePolicy 重启;节点余量和内存缩容限制也可能让 resize 无法完成。 |
| 控制器与模板 | Recreate 依赖上级控制器补 Pod。VPA 不修改 workload template,GitOps 中仍需维护清晰的资源基线。 |
| 多 VPA 匹配 | v1.7.0 将重叠匹配视为未定义行为。一个 workload 应保持一个 controlling VPA,并审查 selector。 |
| Admission 与策略 | 其他 mutating webhook、LimitRange 和 ResourceQuota 可能与 VPA patch 交互或冲突;v1.7.0 也明确不兼容 Pod-level resources。启用更新前应验证最终 PodSpec。 |
| 指标与 Checkpoint | Checkpoint 只能延续已经采集的聚合历史,不能修复缺指标、错误 selector 或没有覆盖代表性负载的问题。 |
7. 总结#
Recommender 产出 recommendation,Checkpoint 延续采样历史。需要改变 Pod 时,Updater 处理存量实例,Admission Controller 则覆盖新建路径。
| 组件 | 输入 | 输出 / 动作 | 不负责什么 |
|---|---|---|---|
| Recommender | VPA、Pod、指标、OOM、历史状态 | status.recommendation |
不修改 Pod |
| Checkpoint | 聚合后的 CPU/内存直方图状态 | 跨 Recommender 重启恢复历史 | 不保存原始指标,不保证样本有代表性 |
| Updater | recommendation、存量 Pod、更新策略与限制 | /resize 或 Eviction API |
不修改 workload template,不创建替代 Pod |
| Admission Controller | Pod CREATE、VPA、recommendation、ResourcePolicy、LimitRange | requests/limits 与 annotations JSON Patch | 不计算 recommendation,不更新 VPA Status |
Off 阶段先验证数据链路和样本代表性,再用业务与基础设施信号审查 recommendation。确认护栏有效后,才逐步允许 VPA 改变 Pod。
参考资料#
官方文档:
- VPA v1.7.0 Release
- VPA v1.7.0 固定源码快照
- VPA README
- VPA API
- VPA FAQ
- VPA Known Limitations
- Resource Management for Pods and Containers
- Resize CPU and Memory Resources assigned to Containers
- Disruptions and Pod Disruption Budgets
- Horizontal Pod Autoscaling
关键源码索引:
- Recommender:周期循环、样本聚合、Estimator 组合、策略裁剪
- Checkpoint:Writer、序列化状态、CRD
- Updater:主循环、候选与优先级、In-place restriction、Eviction restriction
- Admission Controller:Pod handler、VPA matcher、Recommendation provider