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 三个硬依赖(少一个它就是个摆设)¶
- metrics-server(或自定义指标适配器)。没有它,
kubectl get hpa的 TARGETS 会一直是<unknown>,副本数永远不动。 - 容器必须声明
resources.requests.cpu。HPA 按「实际用量 ÷ requests」算利用率, 没有 requests 就没有分母(改用AverageValue这类绝对目标可以绕开)。 - 目标必须是可扩缩的工作负载 —— 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与RespectIgnoreDifferencessyncOption - 组件镜像:
registry.k8s.io/metrics-server/metrics-server、registry.k8s.io/autoscaling/vpa-{recommender,updater,admission-controller}。 内网/无外网环境可换国内代理前缀,但要注意代理里不是每个 tag 都有 (实测:华为云 ddn-k8s 代理里 VPA1.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,要么接受"运行态自适应"并给它配好护栏。