Kubernetes 基础体系 · 第 59/83 篇。示例基于 Kubernetes 当前稳定 API;弃用、版本偏差、云厂商差异和生产风险会明确说明。

Kubernetes HPA 与 VPA:指标、算法、稳定窗口、冲突和容量

Kubernetes 的自动扩缩容不是一个单独的“把资源调大或调小”的功能,而是多个控制器根据观测指标持续修改 Kubernetes 对象:

  • HPA(Horizontal Pod Autoscaler,水平 Pod 自动扩缩容):主要修改工作负载的副本数。
  • VPA(Vertical Pod Autoscaler,垂直 Pod 自动扩缩容):主要调整 Pod 容器的 CPU、内存等资源请求与限制。
  • 节点自动扩缩容器:通常由云厂商或 Cluster Autoscaler 修改节点组规模,使 Pending Pod 获得可调度的节点,或删除空闲节点。

HPA 和 VPA 都是控制回路。它们读取当前状态,计算目标状态,再通过 API Server 修改对象。理解它们的关键,不是记住几个 YAML 字段,而是弄清楚:

  1. 指标从哪里来,代表什么;
  2. 目标值如何转换为副本数或资源请求;
  3. 控制器如何避免频繁抖动;
  4. 多个控制器同时修改同一系统时,反馈回路如何相互影响;
  5. Pod 的资源请求变化最终如何影响调度、节点容量和成本。

本文以当前稳定的 Kubernetes API 为范围。HPA 的主流稳定 API 是 autoscaling/v2;VPA 不是 Kubernetes 核心组件内置的标准控制器,而是 Kubernetes Autoscaler 项目的独立组件,通常安装其 CRD 和控制器后使用 autoscaling.k8s.io/v1。VPA 的具体推荐算法、默认参数和安装方式可能随项目版本变化,不能把它们都视为 Kubernetes API 的规范保证。


一、先建立统一模型:自动扩缩容到底在控制什么

一个 Kubernetes 工作负载至少涉及三种不同的容量:

1. Pod 副本容量

副本数记为:

NN

HPA 控制的主要就是 NN。例如,Deployment 当前有 3 个副本,HPA 可能将其改成 6 个。

副本数增加通常意味着:

  • 更多并行处理能力;
  • 更多连接、缓存和后台任务实例;
  • 更高的总资源请求;
  • 可能需要更多节点。

但副本数并不等于吞吐量。应用可能受数据库连接数、分区数、锁竞争或下游服务限制,增加副本后并不会线性提高吞吐。

2. 单个容器的资源请求和限制

对某个容器,CPU 和内存通常分别有:

  • requests.cpurequests.memory:调度和资源计量使用的请求值;
  • limits.cpulimits.memory:运行时资源上限。

请求值不是“应用一定会用掉的资源”,而是 Kubernetes 为调度和资源保证采用的声明值。对于节点上的可调度容量,最重要的是请求值:

Pod requests节点可分配容量\sum \text{Pod requests} \leq \text{节点可分配容量}

VPA 主要修改容器的请求和限制,从而改变每个 Pod 的资源画像。

3. 节点容量

节点数量记为:

MM

节点自动扩缩容器根据 Pending Pod 的请求、节点组约束和可调度性决定是否增加节点;缩容则需要判断节点上的 Pod 能否被驱逐并重新调度。

因此三个控制层次可以表示为:

flowchart LR
    A[应用负载] --> B[指标采集]
    B --> C[HPA]
    B --> D[VPA Recommender]

    C --> E[修改 Deployment 副本数]
    D --> F[VPA Recommendation]
    F --> G[VPA Admission / Updater]

    E --> H[创建或删除 Pod]
    G --> I[创建新 Pod 时注入资源]
    I --> J[调度器根据 requests 调度]
    H --> J

    J --> K{是否有可调度节点}
    K -- 是 --> L[Pod Running]
    K -- 否 --> M[Pending Pod]
    M --> N[节点自动扩缩容器]
    N --> O[增加或删除节点]
    O --> J

这张图中有一个容易被忽略的事实:HPA 和 VPA 通常不直接修改同一个字段,但会通过指标和调度间接影响彼此。HPA 修改副本数,VPA 修改资源请求;而资源请求又可能是 HPA 计算利用率时的分母。


二、HPA 的指标、数据流和控制对象

2.1 HPA 不负责采集指标

HPA Controller 运行在 kube-controller-manager 中,它通过 Kubernetes API 读取指标,并通过目标资源的 Scale 子资源修改副本数。

常见指标来源包括:

指标类型 典型来源 适合表达
Resource metrics Metrics Server CPU、内存使用量或利用率
Custom metrics Custom Metrics API 适配器 Pod 或其他 Kubernetes 对象的业务指标
External metrics External Metrics API 适配器 Kubernetes 集群外部指标,例如队列长度

仅安装 HPA 对象不会自动产生指标。使用 CPU、内存指标时,集群通常需要 Metrics Server;使用 Prometheus 等系统时,通常还需要适配器把指标暴露为 Kubernetes Custom Metrics API 或 External Metrics API。

先检查资源指标是否可用:

kubectl top pods -n demo
kubectl top nodes
kubectl get --raw "/apis/metrics.k8s.io/v1beta1/nodes" | head

可能的结果:

error: Metrics API not available

这表示 HPA 依赖的资源指标 API 不可用,而不是“当前没有负载”。此时 HPA 通常无法得到完整的当前指标,扩缩容会停止或进入错误状态。

检查 HPA:

kubectl get hpa -n demo
kubectl describe hpa web -n demo

重点查看:

  • AbleToScale
  • ScalingActive
  • ScalingLimited
  • Conditions
  • Events
  • 当前指标和目标指标

2.2 HPA 的稳定 API 示例

下面的 Deployment 使用 CPU 请求作为 HPA 利用率的分母:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  namespace: demo
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: web
          image: nginx:1.27
          ports:
            - containerPort: 80
          resources:
            requests:
              cpu: 250m
              memory: 128Mi
            limits:
              cpu: "1"
              memory: 256Mi
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: web
  namespace: demo
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: web
  minReplicas: 2
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 60
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 0
      policies:
        - type: Percent
          value: 100
          periodSeconds: 60
        - type: Pods
          value: 4
          periodSeconds: 60
      selectPolicy: Max
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
        - type: Percent
          value: 25
          periodSeconds: 60
      selectPolicy: Max

前置条件:

kubectl create namespace demo
kubectl apply -f web-hpa.yaml
kubectl get deployment,hpa -n demo

resources.requests.cpu: 250m 必须存在。因为这里使用了 averageUtilization,HPA 需要知道每个容器的 CPU 请求,否则无法计算该 Pod 的 CPU 利用率。250m 表示 0.25 个 CPU 核,而不是 250% CPU。


三、HPA 的核心算法:从利用率推导副本数

3.1 单指标的基本公式

对于资源利用率指标,HPA 的常见目标公式是:

Ndesired=Ncurrent×VcurrentVtargetN_{\text{desired}} = \left\lceil N_{\text{current}} \times \frac{V_{\text{current}}}{V_{\text{target}}} \right\rceil

其中:

  • NcurrentN_{\text{current}}:当前副本数;
  • VcurrentV_{\text{current}}:当前平均指标值;
  • VtargetV_{\text{target}}:HPA 配置的目标值;
  • x\lceil x \rceil:向上取整;
  • NdesiredN_{\text{desired}}:理论上希望的副本数。

当使用 CPU 利用率时:

Vcurrent=iCPU usageiiCPU requesti×100%V_{\text{current}} = \frac{ \sum_i \text{CPU usage}_i }{ \sum_i \text{CPU request}_i } \times 100\%

如果每个 Pod 的请求相同,也可以理解为先计算每个 Pod 的利用率,再求平均;在请求不同时,按总使用量除以总请求量更准确。

完整数值例子

当前有 3 个 Pod,每个 Pod 的 CPU 请求为 500m

Pod CPU 使用量 CPU 请求 利用率
A 400m 500m 80%
B 300m 500m 60%
C 500m 500m 100%

总使用量为:

400+300+500=1200m400+300+500=1200m

总请求量为:

500+500+500=1500m500+500+500=1500m

当前利用率:

Vcurrent=12001500=80%V_{\text{current}}=\frac{1200}{1500}=80\%

目标利用率为 60%,因此:

Ndesired=3×8060=4=4N_{\text{desired}} = \left\lceil 3\times \frac{80}{60} \right\rceil = \lceil 4\rceil = 4

HPA 会把期望副本数计算为 4,然后还要经过最小副本数、最大副本数、稳定窗口和缩放策略限制。

3.2 为什么“利用率”不是“CPU 使用率”

如果一个 Pod 的 CPU 请求是 100m,实际使用 80m,则利用率为:

80/100=80%80/100=80\%

如果把请求改成 500m,实际仍使用 80m,利用率变成:

80/500=16%80/500=16\%

应用实际使用的 CPU 没有变化,但 HPA 看到的利用率大幅下降。这就是 VPA 与基于资源利用率的 HPA 可能产生反馈冲突的根本原因。

3.3 AverageValueUtilization 的区别

autoscaling/v2 中,资源指标可以使用不同目标类型。

Utilization

target:
  type: Utilization
  averageUtilization: 60

它表示相对于资源请求的百分比。它要求相关容器有对应的资源请求。

AverageValue

target:
  type: AverageValue
  averageValue: 500m

它表示每个 Pod 的平均绝对值,例如每 Pod 平均 500m CPU:

metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: AverageValue
        averageValue: 500m

此时目标不是“请求的 60%”,而是每个 Pod 的平均 CPU 使用量约为 500m。它仍然依赖指标采集,但不会使用 CPU 请求作为目标计算的分母。

3.4 多指标如何合并

HPA 可以配置多个指标,例如 CPU、内存和队列长度。对每个指标分别计算一个期望副本数:

Ncpu,Nmemory,NqueueN_{\text{cpu}},\quad N_{\text{memory}},\quad N_{\text{queue}}

HPA 通常选择它们的最大值:

Ndesired=max(Ncpu,Nmemory,Nqueue)N_{\text{desired}}= \max(N_{\text{cpu}},N_{\text{memory}},N_{\text{queue}})

这是保守策略:任一指标表明容量不足,都不能被另一个“较低”的指标抵消。

例如:

  • CPU 推导 4 个副本;
  • 内存推导 3 个副本;
  • 队列长度推导 8 个副本;

最终期望副本数是 8,而不是平均值 5。

如果某个指标无法获取,且其他指标明确要求缩容,HPA 对缩容通常会更加谨慎;具体行为还取决于指标错误、当前副本状态和控制器实现。生产诊断时应以 kubectl describe hpa 中的条件和事件为准,不能只观察 desiredReplicas


四、HPA 的容忍度、稳定窗口和缩放策略

4.1 容忍度:避免无意义的整数变化

如果当前指标和目标指标非常接近,直接按比例计算会导致副本数在边界附近频繁上下变化。因此 HPA 使用容忍度判断是否需要缩放。

常见实现中,默认容忍度约为 10%,但这是控制器参数和实现行为,不应当当作 HPA API 对所有发行版的绝对保证。

设偏差为:

r=VcurrentVtargetr=\frac{V_{\text{current}}}{V_{\text{target}}}

当:

r1<τ|r-1|<\tau

其中 τ\tau 是容忍度,控制器可以保持当前副本数。

例如当前利用率为 63%,目标为 60%,比例为:

63/60=1.0563/60=1.05

如果容忍度为 10%,5% 的偏差不足以触发扩容。

这不是把目标改成 66%,而是控制器决定暂时不采取动作。目标值仍然是 60%。

4.2 稳定窗口不是采样窗口

两个概念经常被混淆:

  • 同步周期:控制器多久重新计算一次,常见默认值约为 15 秒,但由控制器参数决定;
  • 稳定窗口:在一段时间内观察多个历史期望值,避免选择导致振荡的值。

scaleDown.stabilizationWindowSeconds: 300 并不表示“每 300 秒才检查一次”。它通常表示:过去 300 秒内出现过较高的缩容期望值时,先保留较高值,避免刚扩容就立即缩回去。

可以把缩容稳定窗口抽象成:

Ndown=max{Ndesired(t)t[TW,T]}N_{\text{down}} = \max\{ N_{\text{desired}}(t) \mid t\in [T-W,T] \}

其中:

  • TT:当前时刻;
  • WW:缩容稳定窗口;
  • Ndesired(t)N_{\text{desired}}(t):历史计算出的期望副本数。

时间线例子

假设当前副本数为 10,缩容稳定窗口为 300 秒:

时间 当前指标 原始期望副本数
T240sT-240s 高负载 10
T180sT-180s 低负载 6
T120sT-120s 低负载 5
T60sT-60s 低负载 4
TT 低负载 4

过去 300 秒中的最大期望值是 10,因此稳定窗口会阻止立即缩到 4。只有旧的高值逐渐离开窗口后,副本数才会下降。

扩容通常不使用同样长的默认稳定窗口,因为容量不足的代价可能是请求延迟和错误率上升。实际是否立即扩容,还会受到扩容策略、指标采集延迟、Deployment 创建 Pod 的速度和调度能力影响。

4.3 behavior 的作用

behavior 控制的是扩缩容动作,而不是指标采集。

例如:

behavior:
  scaleDown:
    stabilizationWindowSeconds: 300
    policies:
      - type: Percent
        value: 25
        periodSeconds: 60
    selectPolicy: Max

含义是:

  • 最近 300 秒的缩容期望值用于稳定处理;
  • 每 60 秒最多按某个策略缩容;
  • Percent: 25 表示最多减少当前副本数的 25%;
  • 若配置多个策略,selectPolicy: Max 选择允许动作更大的策略。

PodsPercent 的区别:

policies:
  - type: Pods
    value: 2
    periodSeconds: 60
  - type: Percent
    value: 50
    periodSeconds: 60

当当前有 10 个副本时:

  • Pods 允许减少 2 个;
  • Percent 允许减少 5 个;
  • Max 选择减少 5 个;
  • Min 则选择减少 2 个。

这些限制不能突破 minReplicasmaxReplicas

4.4 HPA 修改的是 Scale 子资源

HPA 不直接把 Deployment 的完整对象重写一遍,而是通过目标资源的 Scale 子资源修改期望副本数。Deployment Controller 再根据 spec.replicas 创建或删除 ReplicaSet/Pod。

这意味着:

  • HPA 管的是目标工作负载的副本数;
  • Deployment 的滚动发布控制器仍然负责发布;
  • 如果另一个系统不断修改 Deployment 的 spec.replicas,就会和 HPA 争夺控制权。

常见错误是同时使用:

kubectl scale deployment web --replicas=3

和 HPA 自动控制副本数。手工设置可能很快被 HPA 覆盖;如果某个 GitOps 系统持续把 spec.replicas: 3 同步回来,也会与 HPA 形成写入竞争。


五、VPA 的组成、生命周期和资源推荐

5.1 VPA 不是 Kubernetes 核心内置控制器

VPA 通常由以下组件组成:

  1. Recommender:根据历史资源使用情况生成推荐;
  2. Updater:寻找资源明显不足或明显过量的 Pod,并决定是否驱逐;
  3. Admission Controller:Pod 创建时根据推荐修改容器资源配置;
  4. VPA CRD:保存 VPA 对象和推荐结果。

因此,安装 Kubernetes 集群并不会自动获得 VPA。需要按照 VPA 项目的版本说明安装其 CRD、RBAC、Webhook 和控制器。VPA 对指标历史、Webhook、证书和驱逐策略都有额外运维要求。

5.2 VPA 的输入与输出

典型数据流如下:

flowchart LR
    A[Pod 容器实际 CPU/内存使用] --> B[指标 API]
    B --> C[VPA Recommender]
    C --> D[VPARecommendation]
    D --> E[Admission Controller]
    D --> F[VPA Updater]

    E --> G[新建 Pod]
    G --> H[注入 requests/limits]

    F --> I[选择需调整的旧 Pod]
    I --> J[遵守 PDB 后驱逐]
    J --> K[工作负载重建 Pod]
    K --> E

VPA 的最终效果通常发生在 Pod 创建时:

  • Admission Controller 拦截 Pod 创建请求;
  • 根据 VPA 推荐修改容器的 resources.requests 和可能的 resources.limits
  • 调度器看到修改后的请求;
  • Pod 按新资源配置运行。

对于已经运行的 Pod,仅仅更新 VPA 推荐值并不一定会修改该 Pod 的 cgroup 资源。是否重建或驱逐 Pod,取决于 updatePolicy.updateMode 和 VPA 版本实现。

5.3 VPA 示例

apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: web
  namespace: demo
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: web
  updatePolicy:
    updateMode: "Off"
  resourcePolicy:
    containerPolicies:
      - containerName: web
        controlledResources:
          - cpu
          - memory
        minAllowed:
          cpu: 100m
          memory: 64Mi
        maxAllowed:
          cpu: "2"
          memory: 2Gi

Off 模式适合先观察推荐值:

kubectl apply -f web-vpa.yaml
kubectl get vpa web -n demo -o yaml

典型状态中会看到类似以下结构:

status:
  recommendation:
    containerRecommendations:
      - containerName: web
        target:
          cpu: 300m
          memory: 256Mi
        lowerBound:
          cpu: 200m
          memory: 192Mi
        upperBound:
          cpu: 600m
          memory: 512Mi

字段含义:

  • target:VPA 认为较合适的目标请求;
  • lowerBound:低于该范围可能资源不足;
  • upperBound:高于该范围可能资源过量;
  • 这些推荐不是实时保证,也不是应用必须使用的精确容量。

推荐结果可能因历史窗口、样本数量、异常流量、启动阶段和资源上下限而变化。VPA 的具体算法并不是 Kubernetes API 规范的一部分。

5.4 VPA 的更新模式

常见模式包括:

  • Off:只生成推荐,不自动修改 Pod;
  • Initial:只在新 Pod 创建时注入推荐资源,不主动更新已有 Pod;
  • Recreate:需要时驱逐旧 Pod,让新 Pod 使用推荐值;
  • Auto:历史上用于自动更新,当前实现中通常等价于重新创建式更新;其语义和未来状态应以所安装的 VPA 版本为准。

生产环境不能只看“推荐值变好了”,还要评估重建影响。驱逐一个 Pod 可能触发:

  • 连接断开;
  • 缓存丢失;
  • 消费者重新均衡;
  • PDB 阻塞;
  • 节点容量不足导致新 Pod Pending。

VPA Updater 通常会考虑 PodDisruptionBudget,但 PDB 只限制自愿中断,并不保证更新一定成功。如果 PDB 要求至少保留全部副本,VPA 可能无法驱逐任何 Pod。


六、VPA 推荐算法:能推导什么,不能过度承诺什么

VPA 的推荐不是简单的:

request=最近一次使用量\text{request}=\text{最近一次使用量}

如果这样做,短时尖峰会导致请求过大,低谷又会导致请求过小。VPA 通常会基于历史样本构建使用量分布,并结合目标分位数、波动性、启动阶段、上下限和安全余量生成推荐。

对某个容器的 CPU 使用样本:

x1,x2,,xnx_1,x_2,\ldots,x_n

可以先按时间窗口收集样本,再形成直方图或分位数估计。若目标分位数为 qq,则目标资源可抽象为:

RtargetQq(x1,,xn)+SR_{\text{target}} \approx Q_q(x_1,\ldots,x_n) + S

其中:

  • QqQ_q:样本的第 qq 分位数;
  • SS:安全余量或算法调整项;
  • RtargetR_{\text{target}}:建议的资源请求。

但这里的 qq、时间权重、直方图实现和安全余量并不是 VPA API 保证的固定公式。不同 VPA 版本可能调整实现。工程上应把 target 看作“基于历史观测的推荐”,而不是容量规划的数学真值。

一个反例:只看平均值会掩盖尖峰

假设一个容器 10 分钟内的 CPU 使用量多数为 100m,但每分钟有一次 900m 的短时尖峰:

100m, 100m, 100m, 900m, 100m, 100m, 100m, 900m, ...

平均值可能并不高,但如果尖峰对应请求延迟或超时,单纯按平均值设置请求会造成:

  • CPU throttling;
  • 调度时请求不足;
  • HPA 利用率异常升高;
  • 负载上升时扩容晚于预期。

另一方面,如果尖峰只是没有用户影响的后台任务,按高分位数配置又可能造成节点资源浪费。资源推荐必须结合 SLO、延迟、错误率和业务负载,而不能只看一个 CPU 数字。

内存尤其不能按短时平均值处理

CPU 使用量在短期内可以下降,内存中的缓存、堆、碎片和泄漏则可能长期保留。若容器有一个持续增长的内存泄漏:

512Mi -> 600Mi -> 700Mi -> 800Mi -> ...

历史分位数可能仍然低于未来峰值。VPA 可能推荐不足,最终由内核触发 OOMKill。内存请求和限制还涉及节点可调度容量,因此内存异常通常需要配合:

kubectl describe pod <pod-name> -n demo
kubectl get pod <pod-name> -n demo \
  -o jsonpath='{.status.containerStatuses[*].lastState.terminated.reason}'

七、HPA 与 VPA 的冲突:同一资源上的反馈回路

7.1 CPU 利用率 HPA 与 VPA 的直接冲突

考虑如下配置:

  • 当前 4 个 Pod;
  • 每个 Pod CPU 请求 250m
  • 每个 Pod 实际使用 200m
  • HPA 目标 CPU 利用率 60%。

当前利用率:

Vcurrent=200250=80%V_{\text{current}}=\frac{200}{250}=80\%

理论副本数:

Ndesired=4×8060=6N_{\text{desired}} = \left\lceil 4\times \frac{80}{60} \right\rceil = 6

HPA 倾向于扩容到 6 个。

此时 VPA 观察到容器长期使用约 200m,把请求推荐为 500m。新 Pod 使用 200m 时:

Vcurrent=200500=40%V_{\text{current}}=\frac{200}{500}=40\%

HPA 的理论副本数变为:

6×4060=4\left\lceil 6\times \frac{40}{60} \right\rceil = 4

于是可能出现:

  1. 请求小,利用率高,HPA 扩容;
  2. VPA 提高请求;
  3. 利用率下降,HPA 缩容;
  4. 负载升高,HPA 再扩容。

这不是 HPA 或 VPA 单独“算错了”,而是二者共同改变了被测系统:VPA 改变了 HPA 利用率的分母

7.2 资源使用量 HPA 能否避免该冲突

如果 HPA 使用 CPU 的 AverageValue

metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: AverageValue
        averageValue: 200m

则目标是每个 Pod 的绝对 CPU 使用量,VPA 修改 CPU 请求不会直接改变这个目标的分母。

但这并不意味着两者完全独立:

  • VPA 仍然会改变调度请求;
  • 请求改变后可能导致 Pending;
  • CPU 限制改变后可能影响 throttling;
  • Pod 重建会改变短期指标;
  • HPA 副本数改变会改变每 Pod 分摊的负载;
  • 应用的吞吐和延迟可能随资源配置变化。

因此,AverageValue 只减少了一个特定冲突,不是通用的冲突消除方案。

7.3 更适合 HPA 的业务指标

对于 Web 服务,HPA 经常更适合使用业务负载指标,例如:

  • 每 Pod 请求数;
  • 活跃连接数;
  • 消息队列长度;
  • 每秒待处理任务数;
  • 请求延迟或错误率的派生指标。

例如队列长度可以抽象为:

Ndesired=QtotalQper-pod-targetN_{\text{desired}} = \left\lceil \frac{Q_{\text{total}}}{Q_{\text{per-pod-target}}} \right\rceil

其中:

  • QtotalQ_{\text{total}}:当前队列中待处理任务总数;
  • Qper-pod-targetQ_{\text{per-pod-target}}:每个 Pod 期望承担的任务数。

这种指标直接描述“需要多少副本”,通常比 CPU 利用率更接近扩容意图。但业务指标采集链路更长,可能包括 Prometheus、适配器、权限和指标名称映射,故障点也更多。

7.4 HPA 与 VPA 的边界建议

以下组合通常风险较高:

HPA:CPU Utilization
VPA:自动修改 CPU requests

因为二者共同改变利用率计算关系。

相对清晰的组合包括:

HPA:队列长度、请求率等业务指标
VPA:根据 CPU/内存历史调整 requests

或者:

HPA:CPU/内存 AverageValue
VPA:调整 requests

但仍需验证重建、调度和节点容量行为。

对于同一个 Deployment,不建议同时让多个独立系统管理 spec.replicas。例如 HPA、GitOps、发布脚本和手工运维都写副本数,会导致声明状态与实际控制状态不断互相覆盖。


八、VPA 如何影响调度、Pending Pod 和节点自动扩缩容

8.1 请求变大不等于节点会自动增加

假设集群节点的可分配 CPU 为 4,当前已有两个 Pod:

Pod A request: 1500m
Pod B request: 1500m
剩余可分配: 1000m

VPA 将另一个 Pod 的 CPU 请求从 500m 调整为 1500m。如果没有任何节点能容纳它,该 Pod 会进入 Pending:

kubectl get pods -n demo
kubectl describe pod <pending-pod> -n demo

describe 中可能看到:

0/3 nodes are available:
  2 Insufficient cpu,
  1 node(s) had untolerated taint

只有当节点自动扩缩容器识别到这个 Pending Pod,并且 Node Group 有可扩容空间时,才可能增加节点。VPA 不负责创建节点。

8.2 节点自动扩缩容器看的是请求和可调度性

节点扩容通常关注:

  • Pending Pod 的资源请求;
  • 节点标签、污点、亲和性和拓扑约束;
  • Node Group 的最小、最大节点数;
  • Pod 是否属于某个节点组可以承载的范围。

它不应根据“节点当前 CPU 使用率很低”就判断 Pending Pod 一定可以调度。调度器首先依据 requests 和约束检查可行性。

因此,以下组合可能形成延迟链:

VPA 更新推荐
  -> 驱逐旧 Pod
  -> 新 Pod requests 变大
  -> 新 Pod Pending
  -> 节点自动扩容
  -> 新节点加入并完成调度

如果节点扩容耗时较长,VPA 的自动更新可能在短期内降低可用容量,而不是提高可用容量。

8.3 请求过大也会造成碎片

节点容量规划不能只做总量加法。假设节点剩余 CPU 总量为 2000m,但分散在两个节点:

节点 1 剩余 1000m
节点 2 剩余 1000m

一个 VPA 调整后的 Pod 请求为 1500m,它仍然无法调度,尽管集群总剩余量是 2000m。这就是资源碎片。

所以必须同时观察:

  • 总请求与总可分配容量;
  • 单节点剩余容量;
  • Pod 的单实例最大请求;
  • 拓扑、亲和性、污点和 DaemonSet 占用;
  • 故障余量。

VPA 推荐出的单 Pod 请求越大,对节点类型和碎片越敏感。


九、容量计算:HPA、VPA 和故障余量如何合并

9.1 基本容量公式

设:

  • NN:副本数;
  • rr:每个 Pod 的资源请求;
  • MM:节点数;
  • CC:每个节点的可分配资源;
  • DD:DaemonSet、系统组件等固定占用;
  • FF:故障余量。

工作负载请求约为:

Rworkload=N×rR_{\text{workload}}=N\times r

节点侧可用于工作负载的容量约为:

Ravailable=M×CDR_{\text{available}}=M\times C-D

若要求保留故障余量,则需要满足:

N×r+FM×CDN\times r + F \leq M\times C-D

如果还要求一个节点故障后仍能容纳工作负载,则对于节点数为 MM 的集群,至少要检查:

N×r(M1)×CDN\times r \leq (M-1)\times C-D'

其中 DD' 表示剩余节点上的系统占用。实际故障余量还可能按多个节点、可用区和 Pod 分布计算,不能只套一个固定百分比。

9.2 完整算例

假设:

  • HPA 最大副本数 Nmax=20N_{\max}=20
  • VPA 推荐每 Pod 请求为 700m CPU;
  • 每个节点可分配 CPU 为 3500m
  • 每个节点系统和 DaemonSet 已占 500m,所以工作负载可用约 3000m
  • 要允许一个节点故障。

最大工作负载请求:

20×700m=14000m20\times 700m=14000m

单节点可容纳的完整 Pod 数:

3000700=4\left\lfloor \frac{3000}{700}\right\rfloor=4

仅从总量看,至少需要:

140003000=5\left\lceil\frac{14000}{3000}\right\rceil=5

但 5 个节点在一个节点故障后只剩 4 个节点,最多约能提供:

4×3000m=12000m4\times 3000m=12000m

无法承载 14000m 的请求。因此至少需要 6 个节点:

  • 正常状态:6 × 3000m = 18000m
  • 一个节点故障后:5 × 3000m = 15000m
  • 可以容纳 14000m,但余量只有 1000m,还要验证 Pod 分布和碎片。

这说明 HPA 的 maxReplicas 与 VPA 的最大请求共同决定节点容量上界。只配置 HPA 的最大副本数而不计算 VPA 可能推荐的最大请求,会使“理论上允许扩容到 20 个副本”变成大量 Pending Pod。

9.3 requests 与 limits 的容量含义不同

调度器主要依据 requests 调度;limits 更像运行时上限。一个 Pod 配置:

resources:
  requests:
    cpu: 100m
  limits:
    cpu: "2"

可以被调度到只剩 100m 可分配 CPU 的节点,但运行时可能争用到更高 CPU,具体取决于节点负载和 CFS 限制。

这会带来两个边界:

  • 请求过低:调度过于乐观,节点容易超卖,HPA 利用率可能很高;
  • 请求过高:调度保守,Pending 和节点成本增加。

VPA 不是用来替代容量压测的。请求应表达调度和稳定运行所需的资源,而不是随意把 limits 的上限复制成 requests。


十、常见误解和对应的失败表现

误解一:HPA 目标 CPU 60% 表示节点 CPU 保持 60%

不对。averageUtilization: 60 通常表示 Pod 容器 CPU 使用量相对于 CPU request 的平均比例,不是节点整体利用率。

节点可能只有 40% 利用率,但某个 Deployment 的 Pod 已达到 90% request;HPA 仍可能扩容。反过来,节点整体很忙,也不代表每个目标 Pod 都需要扩容。

误解二:HPA 一定会立即扩容

HPA 计算出新副本数后,还要经过:

  1. 指标采集延迟;
  2. 控制器同步周期;
  3. 容忍度;
  4. behavior 策略;
  5. Deployment 创建 Pod;
  6. 调度器找到节点;
  7. 容器启动并通过就绪探针;
  8. 服务发现和负载均衡更新。

任何一个环节慢,用户看到的扩容效果都会延后。

误解三:VPA 推荐值就是正确的资源规格

VPA 只根据它能观察到的历史和算法生成推荐。以下情况可能使推荐失真:

  • 工作负载刚部署,历史样本不足;
  • 夜间低负载覆盖了白天峰值;
  • 指标采集缺失;
  • 容器频繁重启,使用量样本不完整;
  • 内存泄漏尚未达到历史高点;
  • VPA 的上下限人为截断了推荐;
  • 业务指标而不是 CPU 才是实际瓶颈。

误解四:修改 VPA YAML 后,运行中的 Pod 会立即变大

updateMode: Off 不会主动修改运行中的 Pod。Initial 也只影响新建 Pod。自动更新模式可能通过驱逐并重建 Pod 生效,而不是在线修改所有容器资源。

验证方式:

kubectl get vpa web -n demo -o yaml
kubectl get pod -n demo -o custom-columns=NAME:.metadata.name,CPU_REQ:.spec.containers[*].resources.requests.cpu,MEM_REQ:.spec.containers[*].resources.requests.memory

要区分:

  • VPA 的 status.recommendation
  • Pod 实际 spec.containers[].resources
  • 容器当前运行状态。

误解五:Pod Pending 就是节点 CPU 使用率太高

Pending 的真实原因可能是:

  • CPU 或内存请求无法满足;
  • 污点和容忍度不匹配;
  • 节点选择器或亲和性无匹配节点;
  • 拓扑分布约束无法满足;
  • PVC 无法绑定;
  • 镜像拉取或权限问题;
  • Pod 数量上限或云厂商配额限制。

应先执行:

kubectl describe pod <pending-pod> -n demo
kubectl get nodes --show-labels
kubectl describe node <node-name>

不要只看:

kubectl top nodes

kubectl top 反映的是观测到的使用量,而调度失败首先通常与 requests 和调度约束有关。


十一、诊断一条完整的扩缩容链路

11.1 HPA 不扩容

依次检查:

kubectl get hpa web -n demo -o yaml
kubectl describe hpa web -n demo
kubectl top pods -n demo
kubectl get --raw "/apis/metrics.k8s.io/v1beta1/namespaces/demo/pods"

重点判断:

  1. HPA 的 scaleTargetRef 是否指向正确 Deployment;
  2. Deployment 是否存在并有可用的 Scale 子资源;
  3. Pod 是否声明了使用 Utilization 所需的 resource request;
  4. Metrics API 是否有数据;
  5. 当前指标是否只是落在容忍度内;
  6. maxReplicas 是否已经达到;
  7. 是否存在指标解析或权限错误;
  8. Pod 是否已经扩容但仍未 Ready。

如果 HPA 显示:

desiredReplicas: 10
currentReplicas: 10

不能立刻推断“扩容没有生效”。可能是 maxReplicas: 10 限制了它,也可能 Deployment 已经创建了 10 个 Pod,但其中部分仍处于 Pending 或未通过就绪探针。

11.2 HPA 频繁抖动

观察:

kubectl get hpa web -n demo -w
kubectl get deployment web -n demo -w
kubectl get events -n demo --sort-by=.lastTimestamp

如果副本数在两个值之间变化,检查:

  • 指标是否在目标边界附近波动;
  • 缩容稳定窗口是否过短;
  • 扩缩容策略是否过于激进;
  • Pod 启动时间是否长于指标反馈周期;
  • VPA 是否修改了资源请求;
  • 新 Pod 是否因冷缓存导致 CPU 暂时升高;
  • 业务负载是否本身具有周期性。

只增加稳定窗口不一定能解决问题。如果根因是 Pod 启动后资源请求变化、应用预热或 HPA 指标不适合,稳定窗口只能隐藏一部分症状。

11.3 VPA 不生成推荐

检查:

kubectl get vpa -n demo
kubectl describe vpa web -n demo
kubectl get pods -A | grep -i vpa
kubectl logs -n kube-system deploy/vpa-recommender

实际命名空间和 Deployment 名称取决于安装清单。需要确认:

  • VPA CRD 是否存在;
  • Recommender 是否运行;
  • Recommender 是否能访问指标 API;
  • targetRef 是否匹配目标工作负载;
  • 目标 Pod 是否确实存在;
  • 指标历史是否足够;
  • resource policy 是否把目标资源排除了;
  • VPA 版本与 CRD 字段是否匹配。

11.4 VPA 更新后 Pod Pending

执行:

kubectl get pods -n demo -o wide
kubectl describe pod <pending-pod> -n demo
kubectl get nodes
kubectl get events -n demo --sort-by=.lastTimestamp

然后把 Pending Pod 的请求与节点可分配容量对比:

kubectl get pod <pending-pod> -n demo \
  -o jsonpath='{.spec.containers[*].resources.requests}'
kubectl describe node <node-name>

恢复路径通常包括:

  • 降低 VPA 的 maxAllowed
  • 暂时切换为 OffInitial
  • 增加或放宽节点组容量;
  • 修正节点标签、污点和拓扑约束;
  • 回滚工作负载资源配置;
  • 等待旧 Pod 恢复,或在确认服务容量后手动调整副本数。

如果更新模式会持续驱逐 Pod,直接修改 VPA 配置后还应观察是否有新的驱逐动作和 Pod 重建。


十二、生产取舍:如何把两个控制器放进同一容量模型

12.1 先明确控制目标

一个可靠的设计通常先回答两个问题:

  1. 副本数由什么决定?

    • 请求率;
    • 队列长度;
    • 并发连接;
    • CPU 或内存;
    • 其他业务负载指标。
  2. 单个 Pod 需要多少资源?

    • 稳态 CPU;
    • 启动和预热峰值;
    • 内存工作集;
    • Sidecar 和 Agent 开销;
    • 故障后重新分配负载时的余量。

如果 HPA 通过业务指标控制副本数,VPA 通过资源历史调整请求,两个控制目标较为清晰。如果 HPA 通过 CPU 利用率控制副本数,同时 VPA 频繁改变 CPU request,就必须把分母变化纳入测试。

12.2 使用 VPA 前先用 Off

先部署:

updatePolicy:
  updateMode: "Off"

持续观察:

kubectl get vpa web -n demo -o yaml
kubectl top pod -n demo
kubectl get pod -n demo -o yaml

把推荐值与以下数据比较:

  • 压测时的 P95/P99 CPU;
  • 内存峰值和 OOM 记录;
  • Pod 启动时长;
  • 节点可分配容量;
  • HPA 在不同请求值下的副本曲线;
  • 一个或多个节点故障后的可用容量。

只有推荐值经过压测和容量校验,才考虑 Initial 或自动更新模式。

12.3 用上限保护节点调度

VPA 的 minAllowedmaxAllowed 可以限制推荐范围,但它们不是容量规划的替代品。上限过低可能导致 OOM 或 CPU throttling,上限过高可能让单个 Pod 无法调度。

例如:

resourcePolicy:
  containerPolicies:
    - containerName: web
      minAllowed:
        cpu: 100m
        memory: 128Mi
      maxAllowed:
        cpu: "1"
        memory: 1Gi

这段配置的作用是约束 VPA 推荐,不代表 Pod 一定能在当前节点上运行,也不代表 1Gi 内存足以覆盖所有业务峰值。

12.4 用 PDB 和副本数控制 VPA 的中断边界

自动更新模式下,VPA 可能需要重建 Pod。PDB 可以限制自愿中断,但它不会创造容量,也不会保证所有更新都能完成。

例如,3 个副本的服务可以定义:

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: web
  namespace: demo
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: web

它表示自愿中断时至少保留 2 个匹配 Pod。若只有 1 个副本,配置 minAvailable: 1 会使驱逐基本无法进行;这不是 PDB 故障,而是策略本身禁止中断。


十三、API、版本和实现边界

HPA

  • 推荐使用 autoscaling/v2
  • 旧的 autoscaling/v1 能力较少,通常只适合简单 CPU 利用率场景;
  • autoscaling/v2 支持多指标和 behavior
  • HPA 的控制循环周期、默认容忍度、部分错误处理行为受控制器参数和 Kubernetes 版本影响;
  • Metrics Server、Custom Metrics API 适配器和 External Metrics API 适配器不是 HPA API 本身。

VPA

  • VPA 是独立项目,不是 Kubernetes 核心内置能力;
  • 使用前必须确认安装的 VPA 版本、CRD 版本、Admission Webhook 和组件参数;
  • 推荐算法不是 Kubernetes API 规范保证;
  • Auto 的历史语义存在版本演进,生产环境应查看所安装版本的文档和源码配置;
  • 自动更新通常意味着 Pod 驱逐和重建,而不是对所有运行中容器进行无中断资源修改。

云厂商和节点自动扩缩容

节点自动扩缩容的行为取决于:

  • 云厂商节点组;
  • Cluster Autoscaler 或其他节点控制器;
  • 可用区、实例类型和配额;
  • 节点启动时间;
  • 云厂商弹性伸缩限制;
  • 预留实例、Spot/抢占式实例和成本策略。

因此,“HPA 扩容后节点会自动增加”只能算常见部署组合,不是 Kubernetes HPA 的规范保证。


结语:把 HPA、VPA 和节点容量看成一个闭环

HPA 控制的是副本数:

NN

VPA 控制的是单 Pod 资源请求:

rr

节点自动扩缩容器控制的是节点容量:

M×CM\times C

工作负载能否稳定运行,至少要满足:

N×r可调度节点容量N\times r \leq \text{可调度节点容量}

而当 HPA 使用资源利用率时,利用率又依赖:

利用率=实际使用量资源请求\text{利用率} = \frac{\text{实际使用量}}{\text{资源请求}}

这使得 VPA 修改 rr 后会改变 HPA 的观测结果。若再加上 Pod 启动延迟、指标延迟、稳定窗口、PDB、节点碎片和故障余量,自动扩缩容就不再是一个简单的比例控制问题。

正确的容量验证应同时测试:

  • 低负载到高负载的 HPA 扩容;
  • 高负载回落后的稳定缩容;
  • VPA 推荐值在稳态和峰值下的差异;
  • VPA 重建期间的服务可用性;
  • 新请求导致的 Pending Pod;
  • 节点自动扩容是否能识别并消化 Pending Pod;
  • 一个节点或一个可用区故障后的剩余容量;
  • HPA 和 VPA 同时启用时的反馈是否振荡。

只有把指标含义、算法分母、稳定窗口、资源请求和节点容量放在同一模型中,HPA 与 VPA 才能从两个独立的自动化功能,变成可验证的交付与运维系统。


系列导航与关联阅读

官方资料

本文依据 Kubernetes、CNCF 与相关项目官方文档重新梳理;正文和生产清单由 WR BLOG 编写。