Kubernetes VPA 的设计和实现

requests 决定 Pod 能否被调度,也影响节点的装箱密度和资源抢占时的保障。它写进 PodSpec 后通常不会自行变化,工作负载却会随着业务迭代、流量起伏、缓存预热和依赖延迟不断改变。完全靠人工维护一组固定值,很快就会出现偏差,进而影响集群的资源利用率和稳定性。

如果团队内无预算控制~ 那么恭喜你,已经解决了这个问题 🎉

VPA(Vertical Pod Autoscaler)会持续观察工作负载,为容器生成 CPU 和内存 recommendation,并根据策略决定是否应用到 Pod。本文关注 recommendation 怎样产生并作用到 Pod,以及生产接入前需要确认哪些限制。常用配置放在前面,便于后文说明各组件的行为。

版本说明

本文基于 VPA vertical-pod-autoscaler-1.7.0,对应源码 commit 6a616ea0c5ea0cb6111240073a9273b3467c064e。字段、默认值、更新模式和源码路径均以该版本为准。

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,暂时不应用普通资源更新。

minAllowedmaxAllowed 限制推荐范围,取值需要结合应用的最小可运行规格、成本约束和最大节点容量。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: 560Mi
  • target 经过 ResourcePolicy 裁剪,是准备应用到 Pod 的目标值。
  • [lowerBound, upperBound] 是推荐区间。当前 requests 落到区间外时,Updater 会把它视为更新信号;这个区间不代表性能承诺。
  • uncappedTarget 保留策略裁剪前的 target,适合用来判断 min/max 是否长期限制推荐。它已经经过分位数估算、margin 和格式化,不能当作原始用量。

这些值来自同一份聚合历史,差别出现在估算和策略处理阶段。下一节先看 CPU、内存样本怎样进入 Recommender,以及 Checkpoint 保存了哪部分历史。

3. VPA 的控制闭环#

VPA 由三个独立运行的组件组成。它们通过 API Server 中的 VPA Status 传递 recommendation,Checkpoint 则让 Recommender 重启后仍能接上之前的聚合历史。

VPA v1.7.0 中 API Server 管理的 VPA 与 Checkpoint 资源、三个 VPA 组件、Workload Controller 和 Kubelet 之间的端到端数据流
VPA v1.7.0 架构与端到端数据流

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 存储是替代关系,不会叠加两份历史。

VPA 从 CPU、内存和 OOM 样本经过聚合、衰减直方图、分位数估算与 ResourcePolicy 得到 recommendation,并通过 Checkpoint 保存历史
recommendation 计算与 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 权重落在桶 01,二者的整数权重是 10000:473,约占保留权重的 95.5% 和 4.5%,不是 10000 条与 473 条 CPU sample。内存权重全部落在桶 10。按 v1.7.0 的默认分桶,CPU 桶 01 分别覆盖 [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#

VPA Updater 从 recommendation 和候选筛选到原地 resize 或 eviction 重建的关键决策
Updater 对存量 Pod 的更新决策

有了 recommendation,Updater 也不会立刻更新 Pod。出现以下任一信号后,Pod 才会进入候选队列:

  1. 至少一个受控容器的当前 request 落在 [lowerBound, upperBound] 之外。
  2. request 仍在区间内,但 Pod 已运行超过配置的生命周期阈值,且 target 与当前资源差异达到更新阈值;v1.7.0 默认分别为 12 小时和 10%。
  3. 受 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#

VPA Admission Controller 从 Pod CREATE、VPA 匹配和策略处理到返回 JSON Patch 的关键流程
Admission Controller 的 Pod CREATE patch 流程

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

  1. 先检查 VPA Condition、Recommender 日志,确认所有目标容器都有 recommendation。
  2. 观察高低峰、工作日/周末、发布、缓存预热和批处理。业务周期是否覆盖,比“已经运行 N 天”更有参考价值。
  3. 把 target 与 CPU throttling、OOM、延迟、错误率、Pod Pending 和节点 allocatable 一起看。recommendation 稳定,并不能说明它满足 SLO。
  4. 根据应用的最小可运行规格和最大节点容量设置 min/max。若目标只是调整调度申请量,优先显式使用 RequestsOnly
  5. 先在无状态、多副本、启动快且 readiness probe 可靠的服务上灰度,再评估 PDB、节点余量和回滚路径。
  6. 把基线资源留在 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'

targetuncappedTarget 需要放在一起看。二者长期不相等,说明策略限制一直在裁剪推荐。此时要检查护栏是否合理,以及 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。

参考资料#

官方文档:

关键源码索引:

访问量 访客数