你是否遇到过这种情况:流量突增时应用跟不上,等到扩容 Pod 跑起来,用户已经骂了半天?或者流量下去了,Pod 还堆在那里,账单看着心在滴血?
Horizontal Pod Autoscaler(HPA)和 Vertical Pod Autoscaler(VPA)就是解决这两个问题的。HPA 负责横向——流量多了加 Pod,流量少了缩 Pod;VPA 负责纵向——每天蹲在那里看你 Pod 吃多少资源,完了告诉你“你这 Request 设大了”,帮你调小资源配额。但注意,它们不是二选一的关系,而是互补的,共同构成 K8s 弹性伸缩体系。
- HPA 到底怎么工作——不是玄学
- 稳定窗口(stabilizationWindow)、容忍度(tolerance)这些容易被忽略的参数怎么调
- VPA 的 updateMode 变了——Auto 已废弃,InPlaceOrRecreate 怎么用
- HPA 和 VPA 一起跑为什么可能“打起来”(死亡螺旋),怎么避免
- 自定义指标接入(Prometheus Adapter)的生产级配置
我用的是 Go 1.21.5 + Kubernetes v1.30.6,环境在自建集群(MacOS 上测试的,Linux 应该也一样)。这个文档里的 YAML 都是真实跑过的,你需要的话可以直接复制。
前置条件:你至少得知道 Deployment、ReplicaSet、Pod 是啥,了解 K8s 的控制循环怎么玩。Metrics Server 已经在集群里跑起来,VPA 组件也装好了(VPA 不是 K8s 默认自带的,得手动装)。
一、HPA 到底怎么运行的——数据流向搞清楚了,调错才有方向
说 HPA 之前,先搞清楚数据从哪来、经过谁、最终去哪。很多 HPA 不生效的根本原因是 Metrics Server 没部署好,而不是 HPA 的 YAML 写错了。
数据流向(按时间顺序):
- cAdvisor(集成在 kubelet 里)采集每个容器的 CPU 用量、内存占用等原始数据。
- kubelet暴露
/stats/summaryAPI,把汇总后的节点和 Pod 级别的指标传出去。 - Metrics Server(默认 15 秒轮询一次)去轮询所有 kubelet 的接口,把数据聚合到集群层面,然后通过
custom.metrics.k8s.io端点注册到 API Server。 - HPA Controller(跑在 kube-controller-manager 里,默认 15 秒算一次)从 API Server 拿到当前副本数和目标指标值,算出来你要多少副本,然后写进 Scale 子资源,Deployment Controller 负责去干活。
实践 1|创建你的第一个 HPA
先验证 Metrics Server 是否正常工作:
kubectl get deployment metrics-server -n kube-system kubectl top nodes # 能看到 CPU/MEM 使用就算 OK部署官方测试应用 php-apache(它的 resources.requests.cpu 设了 200m,HPA 依赖这个才能干活):
kubectl apply -f https://k8s.io/examples/application/php-apache.yaml创建 HPA:
kubectl autoscale deployment php-apache --cpu-percent=50 --min=1 --max=10看看 HPA 状态:
kubectl get hpa # 预期输出: NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE php-apache Deployment/php-apache 0%/50% 1 10 1 5s压测一下:
kubectl run -i --tty load-generator --rm --image=busybox:1.28 --restart=Never -- /bin/sh -c "while sleep 0.01; do wget -q -O- http://php-apache; done"大概一分多钟后(HPA 响应要时间),副本数应该会涨上去。
实践 2|stabilizationWindow、tolerance 和 policies——这些参数才是 HPA 调优的关键
稳定窗口(stabilizationWindowSeconds)解决的是“刚把 Pod 缩了,流量又上来,白缩一场”的问题。缩容时不是看当前指标,而是看过去 N 秒内的最高期望副本数。扩容的稳定窗口默认是 0——指标显示要扩,HPA 马上执行。说实话,我个人建议给扩容也加个 30~60 秒的稳定窗口,防止那种突突突的短暂流量峰值引发不必要的急速扩容。
下面是完整配置示例:
apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: php-apache spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: php-apache minReplicas: 2 maxReplicas: 20 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 70 behavior: scaleUp: stabilizationWindowSeconds: 30 # 扩容也给 30 秒稳定窗口 policies: - type: Percent value: 100 periodSeconds: 60 # 每 60 秒最多翻倍 - type: Pods value: 4 periodSeconds: 60 selectPolicy: Max scaleDown: stabilizationWindowSeconds: 300 # 默认就是 300 秒 policies: - type: Percent value: 10 periodSeconds: 60还有个容易忽略的参数是 tolerance(容忍度),默认 0.1,也就是说目标利用率设 70% 的话,在 63% 到 77% 之间它不动,防止指标轻微波动就反复伸缩。
踩坑 1|Pod 总数为什么会超过 maxSurge——别问,问就是踩过
RollingUpdate 和 HPA 一起跑的时候,有时候你会突然发现 Pod 总数远超预期。这个问题的根因是两个控制器互不知情。HPA 按副本数去扩容,Deployment 的 RollingUpdate 按配置的 maxSurge 加新的 Pod,老的还开着,一下子冒出来很多。更坑爹的是,Terminating 状态的 Pod 不计入可用副本数,HPA 觉得“我算出来需要 10 个,现在只有 7 个在提供服务,那就再扩”——然后你就看到 Pod 数冒上去了。缓解方案是在 HPA 里设 maxReplicas 时留出 maxSurge 的空间,另外适当调小 Deployment 的 maxSurge。
二、HPA + VPA 为什么可能“打起来”——死亡螺旋(Death Spiral)拆解
这是一个经典的大坑。
VPA 每 24~48 小时收集数据,然后压出一条推荐值说“你的 Pod 用不了 200m CPU,Request 降到 100m 吧”。VPA 改了 Request 之后,固定用 150m CPU 的 Pod,利用率从 75% 变成了 150%。HPA 看到利用率超过 70%,觉得“天啊,我扛不住了”,立刻疯狂扩容。ReplicaSet 里 Pod 增多之后,负载分散到更多 Pod 上,每个 Pod 的利用率反而下降了。VPA 的直方图看到利用率下降,以为需求减少了,继续往下调 Request。然后 HPA 看到利用率又反弹——循环加速,整个集群的资源配置和副本数一起开始来回震荡。
这个问题极度恶心,尤其是在有状态应用上,可能造成不可恢复的抖动。
建议方案:
- 不要让 HPA 和 VPA 同时针对 CPU 和内存去做自动伸缩。让 HPA 管理副本数,VPA 只在
Off模式下跑,定期看一眼推荐值然后手动改 Request。 - 如果非要用 Auto 模式,那就把 HPA 的伸缩行为配置到其他的指标上去,比如 QPS,不要让它们用同一套指标互相干扰。
- 某厂商的托管集群里默认同时装了这两样,很多人都不知道,结果一跑起来整个环境就崩了。避坑法则:VPA 只在 Off 模式下跑,看完推荐值手动改,别让它自动动。
(实战中我见过有个哥们直接把两个都开了 Auto,第二天早上发现集群都快自己把自己锤散了,咖啡都没来得及喝就冲过去手动 rebuild。老实说,我不喜欢 VPA 的 Auto 模式,太绕了,还是 Off 跑着看推荐值最稳妥。)
踩坑 2|Pod 资源请求没设,HPA 计算不了
这个坑很小但特别常见。HPA 算 CPU/内存使用量的分母是resources.requests.xxx,没有 Request 就没法算。
验证你所有 Deployment 有没有设 Request 的快速命令:
kubectl get deployments -A -o jsonpath='{range .items[*]}{.metadata.namespace}/{.metadata.name}: requests={.spec.template.spec.containers[0].resources.requests}{"\n"}{end}'实践 3|自定义指标扩展 HPA——用 Prometheus Adapter
K8s 默认只支持 CPU 和内存的 HPA。要用 QPS、消息队列堆积深度、GPU 利用率这些业务指标来做伸缩判断,一般社区里会用 Prometheus Adapter 去打通 Prometheus 和 K8s Custom Metrics API。
用 Helm 安装 Prometheus Adapter:
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts helm install prometheus-adapter prometheus-community/prometheus-adapter \ --namespace monitoring \ --set prometheus.url=http://prometheus.monitoring.svc \ --set prometheus.port=9090注册 Custom Metrics API 后,验证一下:
kubectl get --raw /apis/custom.metrics.k8s.io/v1beta1 | jq '.resources[].name'配置 HPA 使用请求数自定义指标(http_requests_per_second这个 metric 需要在 Prometheus 里已经配置好规则):
apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: api-hpa spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: api-server minReplicas: 2 maxReplicas: 20 metrics: # 自定义指标:每 Pod 每秒请求数 - type: Pods pods: metric: name: http_requests_per_second target: type: AverageValue averageValue: "100" # 每 Pod 达到 100 req/s 就扩容 behavior: scaleUp: stabilizationWindowSeconds: 30 scaleDown: stabilizationWindowSeconds: 300此外,KEDA是另一个方向。它把触发条件从资源利用率扩展到了任意事件源(Kafka、RabbitMQ、Cron 等),做事件驱动的伸缩更方便。两者互补,Prometheus Adapter + HPA 更贴近原生体系,KEDA 做缩容到 0 和支持 ScaleToZero 的场景更强。
三、VPA 详解
VPA 通过三个组件干活:Recommender(监控资源使用,给出推荐值)、Updater(按推荐值更新 Pod 资源)、Admission Controller(拦截 Pod 创建时把推荐值写进去)。
实践 4|VPA 的 updateMode 变了——Auto 从 1.4.0 开始已废弃
VPA 1.4.0 及以后版本中,Auto模式已经被废弃,改成了Recreate。这个改动很多老司机都不知道,直接拿老的 YAML 跑会跟预期完全不一样。目前支持的 updateMode 值有四个:
模式 | 行为 |
| 只给推荐值(status.recommendation),不自动更新——我最推荐的模式 |
| 只在 Pod 创建时设一次 Request,之后不管了 |
| 推荐值和当前 Request 差别大了就驱逐 Pod 重建 |
| 先尝试原地更新(需要 K8s ≥ 1.27),不行就回退到 Recreate |
InPlaceOrRecreate这个模式很香。Kubernetes 1.27 引入了 InPlace Pod Vertical Scaling 特性,Pod 不用重启就能改资源配额。这个模式利用的就是这个能力。但是注意,原地更新的时候,内存限制缩减目前还不完全支持,而且节点要有足够的空闲资源才能就地调大配额。
apiVersion: autoscaling.k8s.io/v1 kind: VerticalPodAutoscaler metadata: name: nginx-vpa spec: targetRef: apiVersion: "apps/v1" kind: Deployment name: nginx updatePolicy: updateMode: "Off" # 先 Off 模式跑几天,看推荐值合理了再改 resourcePolicy: containerPolicies: - containerName: nginx minAllowed: cpu: "100m" memory: "256Mi" maxAllowed: cpu: "500m" memory: "1Gi"跑几天之后查看推荐值:
kubectl get vpa nginx-vpa -o yaml # 看 status.recommendation.containerRecommendations然后取target或unsappedTarget里的值手动写到 Deployment 的resources.requests里去。
踩坑 3|VPA 推荐值可能超过节点可用资源
VPA 有时候会推荐超过节点上最大可用容量的资源值。这种情况一旦发生,Pod 在重建的时候就直接 Pending 住调度不出来,永远卡在Pending状态,这是非常可怕的灾难。minAllowed和maxAllowed必须设,这是生产级别的安全底线。
实战决策|什么时候用 HPA、VPA 还是 KEDA
很多人在社区里问“这个场景我到底该用什么”,这里直接给一个判断表:
场景 | 推荐 | 理由 | 危险提示 |
无状态 Web 服务 | HPA | 根据 CPU/QPS 加 Pod,简单直接 | 别忘了设 minReplicas 和 maxReplicas |
有状态单副本应用 | VPA(Off模式) | 没法横向扩,只能改单 Pod 资源配额 | Auto 模式不要碰,原地更新第一选择 |
消息队列消费者 | KEDA + HPA | 根据队列深度弹性伸缩 KEDA 用 ScaledObject 做直接 | 缩容到 0 的冷启动延迟要预算 |
批处理任务/Timer | KEDA(Cron scaler) | 定时拉起,跑完了缩回 0 | 任务切换开销要预估好 |
大规模 GPU/ML 推理 | HPA + Prometheus Adapter | 用 DCGM 的 GPU 利用率指标做决策 | 热启动很耗时,扩缩平滑窗口要加大 |
成本优先的 Serverless | KEDA + ECI/Fargate | 支持缩容到 0,账单按秒计算 | 冷启动至少撑 2~5 秒 |
四、常见故障排查
故障 1:HPA 显示<unknown>
kubectl describe hpa看 Metrics 列显示 Unknown,大概率是 Metrics Server 挂了或者资源请求没设。
检查命令:
kubectl get pod -n kube-system -l k8s-app=metrics-server kubectl top nodes如果是自建集群(kubeadm 那种),拿 TLS 跳过的手段:
kubectl edit deploy metrics-server -n kube-system # 在 args 里加: # --kubelet-preferred-address-types=InternalIP # --kubelet-insecure-tls故障 2:HPA 已经超过 maxReplicas 了还在扩
检查是不是 RollingUpdate 的 maxSurge 设太大导致 Deployment 在 HPA 之外加 Pod。另外看看 Target 的 Pod 是不是有很多 Terminating 状态拖慢统计延迟。
故障 3:VPA 更新 Pod 导致持续重启
updateMode: Recreate会在每次资源不匹配时 Pod 重建。把它改成 Off,用推荐值手动配 Deployment。
总结与最后的建议
好了,大概就这些东西。总结几个核心要点:
- 部署顺序:
Metrics Server先搞起来(没用它 HPA 完全是空的),HPA 写 YAML 时minReplicas和maxReplicas别乱设,稳定窗口(stabilizationWindowSeconds)按场景调。需要业务指标做决策,配 Prometheus Adapter。 - 安全使用 VPA:必须设
minAllowed和maxAllowed,updateMode用Off或者Initial。VPA 1.4.0 把 Auto 改成了 Recreate,YAML 一定要改过来。能利用 InPlaceOrRecreate 和 InPlace Pod Vertical Scaling 能力用。不要在生产环境把 HPA 和 VPA 同时设成针对 CPU 的 Auto 模式,绝对不要。 - 死亡螺旋预防:HPA 和 VPA 要在两套不同指标上去做伸缩判断,不要让他们互相踩——HPA 走 QPS 或者消息队列深度,VPA 的 CPU 和内存只是静态推荐,不参与实时伸缩决策。这是最稳的组合。
你还有什么更好的办法或者踩过的坑?评论区见。如果觉得有用,欢迎分享给其他被 HPA/VPA 折磨的兄弟姐妹们。