Skip to content

Kubernetes 弹性扩缩容:HPA 与 VPA

一份面向公开读者的学习笔记。目标是讲清三件事:HPA 和 VPA 各自改的是什么、 怎么配、以及和 GitOps(Argo CD)一起用时会在哪里打架。 文中带 实测 标记的数字来自一次真实实验(k8s 1.32 单节点 + Argo CD + VPA 1.3.1), 不是从文档里抄的。


0. 先建立心智模型:它们改的不是同一个东西

这是理解后面一切分歧的起点:

控制器 改什么 改在哪个对象上 效果
HPA(Horizontal,水平) 副本数 spec.replicas Deployment 增减 Pod 的数量
VPA(Vertical,垂直) 容器的 requests / limits Pod(不动 Deployment) 调整单个 Pod 的规格

记住这一条:HPA 动 Deployment,VPA 动 Pod。

后面"会不会和 Argo CD 抢字段"、"Git 里能不能看到真实值",全部由这个差别推出来 —— HPA 直接和 Argo 争 Deployment 的所有权,VPA 根本不碰 Argo 管的那个对象。


1. HPA(水平扩缩容)

1.1 它是怎么工作的

  • 它是一个控制循环(默认约 15 秒一轮):读指标 → 算期望副本数 → 写回 Deployment 的 spec.replicas。
  • 期望副本数大致是 当前副本数 × (当前指标值 / 目标指标值),再经过扩缩容速率策略(behavior)平滑,避免抖动。
  • 指标来源默认是 metrics.k8s.io(由 metrics-server 提供);也可以接自定义/外部指标 (Prometheus Adapter、KEDA 等)。

1.2 三个硬依赖(少一个它就是个摆设)

  1. metrics-server(或自定义指标适配器)。没有它,kubectl get hpa 的 TARGETS 会一直是 <unknown>,副本数永远不动。
  2. 容器必须声明 resources.requests.cpu。HPA 按「实际用量 ÷ requests」算利用率, 没有 requests 就没有分母(改用 AverageValue 这类绝对目标可以绕开)。
  3. 目标必须是可扩缩的工作负载 —— Deployment / StatefulSet / ReplicaSet,裸 Pod 不行。

1.3 最小可用配置

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: web
  namespace: default
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: web          # 不要带 UID:Deployment 重建后 UID 变了会失效
  minReplicas: 1
  maxReplicas: 5
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70     # 目标:平均利用率 70%
  behavior:                          # 可选:控制扩缩容激进度
    scaleDown:
      stabilizationWindowSeconds: 300

1.4 和 Argo CD 的冲突(最容易踩的一个坑)

现象:HPA 把副本扩到 4,几秒后掉回 1,接着又被扩到 4……Application 永远 OutOfSync。

原因:HPA 改的是 Deployment 的 spec.replicas,而 Argo CD 的 selfHeal 也在管这个字段 (按 Git 里的 replicaCount)。两个控制器抢同一样东西。

解法:给这个 Application 加 ignoreDifferences 忽略 replicas,并且加 RespectIgnoreDifferences=true:

spec:
  ignoreDifferences:
    - group: apps
      kind: Deployment
      jsonPointers:
        - /spec/replicas
  syncPolicy:
    syncOptions:
      - RespectIgnoreDifferences=true     # ← 少这行等于白配

⚠️ 只写 ignoreDifferences 不够。 它只让 Argo 在 diff / UI 里忽略这个字段, 自动同步(含 selfHeal)时照样把 Git 的值写回去。必须配 RespectIgnoreDifferences=true 才会在同步时保留集群实时值。这个 syncOption 需要 Argo CD ≥ 2.3。 实测:两项配齐后,Deployment 稳定停在 HPA 扩出来的副本数,Application 显示 Synced。

⚠️ 注意 ignoreDifferences 的作用范围。 如果你用 ApplicationSet 批量生成 Application, 它 template 里的配置是整个环境共用的 —— 把 ignoreDifferences 写在那儿,会让该环境下 所有服务都忽略 replicas,而不只是你那个上 HPA 的服务。想让忽略只作用于一个服务, 就把它从 ApplicationSet 里拿出来、单独定义成 Application。 实测:一开始写在 AppSet 的 template 上,结果连没上 HPA 的 troubleshoot 也一起被忽略了。

1.5 一个副作用:Git 里的 replicaCount 降级成"初值"

忽略 replicas 之后,Git 里那行 replicaCount: 1 就只在 Deployment 第一次创建时起作用了。 以后把它改成 3 也不会生效 —— 副本数从此归 HPA 管。建议就在那个字段旁边写一行注释, 否则过几个月你自己或同事都会在这个上面绕半天。


2. VPA(垂直扩缩容)

2.1 它是怎么工作的

VPA 不是 k8s 内置,要单独安装。它由三个组件构成:

组件 职责
recommender 依据历史用量算出每个容器的推荐区间(Lower / Target / Upper Bound)
updater 在 Auto 模式下驱逐"规格不合理"的 Pod,让它按新规格重建
admission-controller 一个 mutating webhook:Pod 创建时把推荐值注入 requests / limits

最关键的一点:VPA 从不修改 Deployment 的 spec.template。它只作用于正在运行的 Pod (靠"驱逐重建 + 创建时注入"这套组合)。

2.2 三种 updateMode

模式 行为 Git 里能看到真实值吗
Off 只算建议,什么都不改(用 kubectl describe vpa 看) ✅ 能,没人动过
Initial 只在 Pod 新建时注入一次建议,不主动驱逐 大体能(新建的 Pod 可能与模板有差)
Auto 主动驱逐重建,持续把 Pod 拉向建议值 ❌ 不能,真实值只在 Pod 上

"只让它算、不让它改"就是 Off。 这时 VPA 是个"顾问":你按建议把 resources 写回 Git, 由 Argo CD 正常同步 —— 这是唯一和 GitOps 完全自洽的用法,代价是它是人工闭环。

2.3 最小配置

apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: web
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: web
  updatePolicy:
    updateMode: "Off"        # 先只看建议
  resourcePolicy:            # 强烈建议配上,见 2.4
    containerPolicies:
      - containerName: '*'   # 匹配所有容器(含注入的 sidecar)
        controlledValues: RequestsOnly
        minAllowed:
          cpu: 10m
          memory: 32Mi
        maxAllowed:
          cpu: 500m
          memory: 256Mi

2.4 两个必配的"护栏"

① controlledValues: RequestsOnly

VPA 默认是 RequestsAndLimits —— requests 和 limits 它都改。但 limits 通常是你有明确 意图的上限(用来保护节点),不该交给算法。

实测:不加约束时,VPA 把一个 CPU 死循环容器的 limits 调到了 11680m(11.68 核) —— 而那台机器总共才 12 核。设成 RequestsOnly 之后,limits 稳定停在你 YAML 里写的值, VPA 只动 requests。

② minAllowed / maxAllowed

给建议设上下限,防止它给出离谱的值。约束关系要注意:

maxAllowed 不要超过 resources.limits,否则会出现 requests > limits 的非法组合, VPA 把 requests 调上去时 Pod 会创建失败。正确顺序是先放宽 limits,再设 maxAllowed。

实测:设 maxAllowed.cpu: 500m 后,VPA 想往上要更多也被顶在 500m —— 护栏生效。

2.5 一个容易困惑的点:单副本时 VPA 不动手

VPA 的 updater 有个保护:副本数少于 2 的 Deployment,它拒绝驱逐 Pod(避免把单副本服务 搞中断)。1 副本时你会看到建议正常算出来(PROVIDED=True),但 Pod 一直不动, updater 日志里只有这一句:

"Too few replicas" kind="ReplicaSet" object="default/xxx" livePods=1 requiredPods=2 globalMinReplicas=2

想让它动手,给 2 个以上副本(或调整 updater 的 --min-replicas)。


3. 对照与选型

3.1 一表看清区别

维度 HPA VPA
扩缩维度 横向:Pod 数量 纵向:Pod 规格
改的对象 Deployment(spec.replicas) Pod(requests / limits)
会重启 Pod 吗 扩容新建、缩容删除 Auto 下会驱逐重建
依赖 metrics-server(或指标适配器) VPA 三组件(CRD + recommender/updater/admission-controller)
适用场景 无状态、请求可分摊的服务 不好水平扩展、或规格难预估的服务(单实例组件、内存型应用等)
需要提前声明 requests ✅ 必须(否则算不出利用率) ❌ 不需要(这正是它要算的东西)
和 Argo CD 冲突 ✅ 抢 Deployment 的 replicas,要 ignoreDifferences ❌ 不动 Deployment;但真实值不进 Git
k8s 原生吗 ✅ 内置 API(autoscaling/v2) ❌ 需要额外安装

3.2 怎么选

  • 能水平扩展(无状态、负载可分片)→ 优先 HPA。
  • 不能水平扩展,但规格老是估不准 → 用 VPA 的 Off 模式当顾问。
  • 两个都要用?绝对不要让它们看同一个指标。 如果 HPA 按 CPU 扩副本、VPA 也按 CPU 调 requests,那么 VPA 抬高 requests 会改变 HPA 算出的利用率,两边互相拉扯。 要么分工(HPA 管 CPU 副本数、VPA 只管 memory),要么只留一个。

3.3 一个反模式

给同一个 Deployment 同时上 HPA(按 CPU)和 VPA(Auto、也管 CPU): 你会得到一个永远在抖动的副本数,加一个永远在重建的 Pod。 这不是"双保险",是互相拆台。


4. 和 GitOps(Argo CD)一起用的完整结论

你的诉求 怎么做
HPA 扩出来的副本数别被 Argo 拉回 Application 加 ignoreDifferences: /spec/replicas + RespectIgnoreDifferences=true,且只加给该服务(别加在 ApplicationSet 的 template 上)
VPA 的调整不脱离 Git updateMode: Off,把建议人工写回 Git 的 resources,由 Argo 同步
VPA 自动调、但不想让它失控 updateMode: Auto + controlledValues: RequestsOnly + min/maxAllowed
想在 Git 里查"现在到底是多少" 做不到 —— HPA/VPA 生效的那部分字段,真实值只在集群运行态里(kubectl get -o jsonpath)。这是这类控制器的固有代价

5. 快速验证清单

以命名空间 default、服务名 web 为例:

# 1. 指标通了没(HPA 的前提)
kubectl top nodes
kubectl get apiservice v1beta1.metrics.k8s.io          # AVAILABLE 应为 True

# 2. HPA 是否真的在扩
kubectl get hpa web                                     # TARGETS 不该是 <unknown>
kubectl get deploy web -o jsonpath='{.spec.replicas}'    # 应等于 HPA 给出的副本数

# 3. HPA 和 Argo 还打架吗(应 Synced,说明 ignoreDifferences 生效)
kubectl -n argocd get application <app-name>

# 4. VPA 有没有在算(Off 模式也能看)
kubectl describe vpa web | sed -n '/Recommendation/,$p'

# 5. VPA 有没有真的注入(Auto 模式)
kubectl get pods -l app=web -o jsonpath='{.items[*].spec.containers[*].resources}'

# 6. VPA 为什么没动手
kubectl -n kube-system logs deploy/vpa-updater | grep -iE "too few|evict"

6. 参考

  • Kubernetes 官方文档:Horizontal Pod Autoscaling、Resource metrics pipeline
  • kubernetes/autoscaler 仓库的 vertical-pod-autoscaler/(含 deploy/ 清单与 gencerts.sh; admission-controller 需要一份 TLS 证书,CN/SAN 为 vpa-webhook.kube-system.svc)
  • Argo CD 文档:ignoreDifferences 与 RespectIgnoreDifferences syncOption
  • 组件镜像:registry.k8s.io/metrics-server/metrics-server、 registry.k8s.io/autoscaling/vpa-{recommender,updater,admission-controller}。 内网/无外网环境可换国内代理前缀,但要注意代理里不是每个 tag 都有 (实测:华为云 ddn-k8s 代理里 VPA 1.2.2 取不到、1.3.1 才有), 升级版本前先确认代理里有对应 tag。
  • metrics-server 装在 kubeadm 集群上要加 --kubelet-insecure-tls(kubelet 是自签证书), 否则会"装成功但指标一直 <unknown>"。

附:一句话总结

HPA 改 Deployment 的副本数,所以要跟 Argo CD 明确"这个字段不归你管"; VPA 改 Pod 的规格,Argo CD 不跟它抢,但它的调整也就留在了集群里、不在 Git 中 —— 要么用 Off 把它当顾问、人工写回 Git,要么接受"运行态自适应"并给它配好护栏。