VPA 推荐不准?不是 VPA 不行——是你 Pod 重启过

场景:VPA 上线后推荐 CPU 1.2 核,Pod 实际只用 300m 路径:坐标 → 分层 → 路径 → 定位 → 标点

以下排查基于 K8s v1.25,VPA v1.x(autoscaling.k8s.io/v1 API)。


上篇讲了 HPA 扩缩容不灵敏的 requests 分母陷阱——HPA 的 utilization 计算依赖 Pod 的 requests 做分母,requests 设大了利用率就偏低,导致该扩不扩。这篇我们来看 K8s 自动化的另一个难题:VPA 推荐值到底有多「健忘」

VPA 上线跑了一天,开发过来说推荐不准: kubectl get vpa 一看 target 1200m,kubectl top pod 一查只用了 300m——差了 4 倍。 不是 VPA 不准——是你的 Pod 重启过,VPA 的历史窗口清零了。

坐标

4 倍差距:target 1200m vs 实际 300m

kubectl get vpa notification-service -o yaml 的输出暴露了第一条线索:

target 显示 CPU 1200m,但同一时间 kubectl top pod 只显示 300m:

kubectl get vpa -o yaml Recommendation 段

差距不是 10% 也不是 20%,是 4 倍。这不是微调能解决的问题。

kubectl top pod 看的是当前使用率,而 VPA Recommender 看的是历史分布——这两个时间窗口不一样。要搞清楚 VPA 到底看了哪些数据才算出 1200m,得从 VPA 的三层架构逐层排查。

分层

VPA 不是一个单独的 Pod,它由三个组件协作构成: - VPA Recommender:读 metrics-server 的历史数据,算推荐值 - VPA Updater:决定什么时候触发 Pod 重建以应用新推荐 - VPA Admission Controller:Pod 创建时通过 Webhook 注入推荐值

不搞清这三层的边界,你查 VPA 的问题就会一直在 Pod 层打转。

VPA 三层架构图

排查必须走完三层:先看 Recommender 怎么算的 → 再看 Updater 为什么不执行 → 最后检查集群层 HPA 是否冲突。任何一个环节都可能让推荐值和预期差 4 倍。

Layer 1 Pod 层:VPA Recommender 的 histogram 窗口

VPA Recommender 的核心数据结构是 histogram——频率分布直方图,VPA 按预设的 bucket 区间(如 100m~200m、200m~300m)统计 CPU/内存使用出现的频次分布,用于计算 P95 百分位推荐值。它不取"当前值",而是取过去一段时间的分布百分位(默认 P95)。

kubectl get vpa notification-service -o yamlstatus.recommendation.containerRecommendations[0].cpu 字段包含了计算细节,但更关键的数据藏在推荐器的内部状态里:

VPA Recommender histogram 排查命令

要看 Recommender 实际看到了多少历史数据,两条路:

# 路径 A:log 级别提到 5 看计算过程
kubectl logs -n kube-system deployment/vpa-recommender --tail=200 | grep histogram

# 路径 B:Prometheus 查 VPA 的 metrics(如果配了监控)
vpa_recommender_histogram_bucket{container="main",pod="notification-service"}

如果日志里只有 2 行 histogram 记录,说明 Recommender 只看了一两个小时的数据。默认 8 天的观测窗口里只有 2 小时,为什么?

——因为 Pod 刚重启过。

Pod 重启后,Recommender 对新 Pod 没有历史数据积累。它的保护逻辑是:数据不够 → 先给一个保守估计,防止再次 OOM。这个保守估计就是 OOMProtectionFactor。

OOMProtectionFactor 机制:VPA 会在 histgoram 中记录 OOM 事件。一旦 Pod 的某个容器发生过 OOMKill,Recommender 会在推荐值上乘以一个保护系数(默认 1.2 + buffer),确保新推荐值显著高于 OOM 发生时的使用量。

你的 Pod 没有真的 OOM,但 Pod 重启了——Recommender 的初始推荐策略和 OOM 保护策略本质上是同一个逻辑分支:历史数据不足以判断安全边界时,给出高于实际使用量的 recommendation,防止生产环境因为 requests 太小被 OOMKill

C2 环节的关键问题回答清楚了:VPA 的哪个组件在什么条件下触发这个行为? → VPA Recommender 的 pod_memory_oom_patching 逻辑,在容器级历史窗口不足 8 天时启动保守推荐模式。

Layer 2 控制面层:VPA Updater 为什么不执行

还有一个令人困惑的现象——既然推荐了 1200m,为什么 Pod 没被更新?

kubectl describe vpa notification-service 看看 Update 状态:

VPA Updater ConfigMismatching 状态

ConfigMismatching 为 True——VPA updater 发现当前 Pod 的 resources 和 VPA 配置中的 updateMode 不匹配。如果你的 updateMode 是 Initial(只在 Pod 创建时注入),updater 不会主动重建 Pod。

VPA 有三种 updateMode

Mode 行为 适用场景
Auto 自动更新 + 自动重建 Pod 开发/测试环境
Initial 只在 Pod 创建时注入 生产环境,希望手动控制变更时机
Off 只推荐不执行 只看 VPA 建议,手动调整

C3:UpdateMode —— VPA Updater 根据这个配置决定是否主动 Evict Pod 来应用新推荐值。Auto 下 Updater 会因 recommendation 变化而驱逐旧 Pod 创建新 Pod。

大多数生产团队用的模式是 Off——只参考 VPA 的推荐值,不改 Pod。如果你的 mode 是 Off,VPA 推荐了 1200m,Pod 不会自动更新,但开发看到的画面是"VPA 推荐了 1.2 核→其实根本没动"。

Layer 3 集群层:HPA + VPA 共享 metric 时的互斥问题

如果你同时开了 HPA(HorizontalPodAutoscaler——基于 CPU/内存使用率自动调整 Pod 副本数的控制器),那还有一个更大的陷阱。

上篇我们花了整篇文章讲 HPA 的 requests 分母问题。VPA 正好反过来——它会改 requests。VPA 改 requests → HPA 的 utilization 分母改变 → HPA 判断"利用率低了" → 缩容 → VPA 发现资源变少又建议扩容 → 震荡。

VPA + HPA 互斥震荡图

这不是理论推演,是一个真实线上案例:团队同时装了 VPA Auto 和 HPA,VPA 把 requests 从 500m 改到 1.2 核后,HPA 的 CPU 利用率从 65% 掉到 27%,HPA 缩容了 2 个 Pod。VPA 检测到 Pod 变少 → 认为 CPU 压力大 → 又建议更大的 requests——无限循环。

解决方式:

  • 方案 A(推荐):HPA 不用 resource.cpu,改用 custom metrics(QPS / 延迟)。这样 VPA 改 requests 不影响 HPA 的扩缩容判断
  • 方案 B(长短期分离):VPA 只用 Off 模式做参考,HPA 用 resource.cpu。定期查看 VPA 推荐,人工调整对应的 Deployment requests
  • 方案 C(不推荐):同时开 VPA Auto + HPA resource.cpu——除非你写的 HPA target 是 custom metric

VPA 排查顺序表

排查内容 关键命令
Pod 数据层 Recommender histogram 窗口是否充足 kubectl logs -n kube-system deployment/vpa-recommender \| grep histogram
控制面更新层 Updater 的 updateMode 和执行状态 kubectl describe vpa <name> → Conditions
集群协调层 HPA 是否与 VPA 共享 resource.cpu kubectl get hpa <name> -o yaml \| yq '.spec.metrics'

路径

五步排查顺序

下次遇到"VPA 推荐值不对劲",按截图中的顺序查:

VPA 五步排查命令

定位

最常见的误判

"VPA 推荐了 1.2 核——不准啊,关掉吧"

大多数人遇到 VPA 推荐偏差后的第一反应是"关掉"。他们认为 VPA 是个自动调参工具,推出来的值经不起推敲。

VPA 推荐是一个参考区间,不是最终答案

VPA 的推荐值取决于三个因素,都排查清楚再做判断:

  1. 历史窗口:Pod 运行多久了?<3 天的窗口,VPA 会保守推荐。用 kubectl logs 看 histogram 条目数
  2. OOMProtectionFactor:发生过 OOMKill 吗?如果有,VPA 在保护你。没有 OOM 但窗口不够,也会触发类似逻辑
  3. minAllowed / maxAllowed:VPA 配置里有没有设边界?推荐值和边界重叠时,实际用的是边界

VPA 推荐不是「处方」——是「参考区间」。理解它的保守倾向,比关掉它更有用。开篇的对手不是"VPA 不准",而是"VPA 不可靠"这个结论本身。

误判对比:关掉 VPA vs 查 histogram 窗口

标点

修复方案

修复 1:给 VPA 加约束边界

apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: notification-service-vpa
spec:
  targetRef:
    apiVersion: "apps/v1"
    kind: Deployment
    name: notification-service
  updatePolicy:
    updateMode: "Off"   # 只用推荐,不自动更新
  resourcePolicy:
    containerPolicies:
    - containerName: "*"
      minAllowed:
        cpu: 200m
        memory: 256Mi
      maxAllowed:
        cpu: "1"
        memory: 1Gi

minAllowedmaxAllowed 是 VPA 的护栏——推荐值不会低于 min、不会高于 max。哪怕 OOMProtectionFactor 把推荐推到 1.2 核,maxAllowed: 1 核能把它截住。

修复 2:mode=Off + 手动参考周期

生产环境建议用 updateMode: Off,每个月看一次 VPA 推荐,决定是否需要调整 Deployment 的 requests:

kubectl get vpa notification-service -o jsonpath='{.status.recommendation.containerRecommendations[0].target}'

这里有一条可以直接拿来用的排查清单——

Check-list

每条对应一个 kubectl 命令:

# □ 看 VPA 推荐值
kubectl describe vpa <name>

# □ 看 Recommender 日志(判断历史窗口是否充足)
kubectl logs -n kube-system deployment/vpa-recommender --tail=100 | grep histogram

# □ 看当前 Pod 实际使用量(对比检查)
kubectl top pod -l app=<app>

# □ 看 VPA 的 updateMode 和 minAllowed/maxAllowed
kubectl get vpa <name> -o yaml | yq '.spec.updatePolicy, .spec.resourcePolicy'

# □ 检查 HPA metrics(看是不是和 VPA 共享了 resource.cpu)
kubectl get hpa <name> -o yaml | yq '.spec.metrics'

C6:下篇我们聊 PodDisruptionBudget——一个设太严会导致节点维护完全卡死的"静默"配置。

Pod 重启一次,VPA 失忆一周。理解它的保守倾向,才能用好它的推荐。

"故障排查的终点不是修好了——是把排查路径写成 check-list。"


附:完整命令清单

# VPA 推荐值查看
kubectl describe vpa <name>
kubectl get vpa <name> -o yaml | yq '.status.recommendation'

# VPA Recommender 日志(窗口判断)
kubectl logs -n kube-system deployment/vpa-recommender --tail=200 | grep -E "histogram|OOM|recommendation"

# Pod 实际使用量
kubectl top pod -l app=<app>

# VPA 配置
kubectl get vpa <name> -o yaml | yq '.spec.updatePolicy'
kubectl get vpa <name> -o yaml | yq '.spec.resourcePolicy'

# HPA 指标配置(检查 VPA+HPA 冲突)
kubectl get hpa <name> -o yaml | yq '.spec.metrics'

# VPA Updater 状态
kubectl logs -n kube-system deployment/vpa-updater --tail=100

📺 公众号「Ai拆代码的曹操」 🌟 知识星球「Ai拆代码的曹操」