先说个反直觉的结论:一个AI推理服务,你给它配了20个Pod,CPU常年只用15%,但它还是会在某天半夜被用户骂上热搜。原因很简单,AI服务的流量不是匀速长河,而是脉冲式的。真正的问题是——当脉冲来的时候,你手动敲kubectl scale的速度,根本追不上它。
这就是为什么我坚持把HPA(HorizontalPodAutoscaler,水平Pod自动扩缩容)当成所有AI服务上线的标配。它本质上就是个"智能资源管家":盯着业务水位,自动帮你加人、减人,不需要半夜爬起来看告警。这篇文章不整文档式科普,就把我自己从"手动扩容累到怀疑人生"到"HPA用得像老司机"的完整路径拆给你看:原理、配置、AI场景调优、排错链路,全都有。适合刚上手Kubernetes的AI应用开发者,也适合已经配了HPA但经常不按预期工作的同学。
1. AI服务的流量脾气,决定了你绕不开HPA
1.1 一次被人流打懵的教训:扩容永远慢过流量
去年底我把一个基于大模型的图片理解服务部署到Kubernetes,上线第二周赶上一次营销活动。凌晨1点47分,告警群开始刷屏,gateway timeout率一路冲到87%。我当时的应对路径是这样的:先登录监控面板看请求量,发现是某个合作方的H5页面瞬间引入了上千并发;然后去查Deployment当前副本数,才4个Pod;接着手动执行扩容,把副本数改到12;等Kubernetes调度完、拉完镜像、加载完模型,全部Pod就绪,已经是25分钟之后。
用户不会等你25分钟。那次事故之后我把完整的操作时间线复盘了一遍:发现问题用了3分钟,决定扩容用了1分钟,但真正吞掉时间的是调度、拉镜像、容器启动、模型加载这个链条。手动扩容的致命伤不是"我反应慢",而是Kubernetes的冷启动链路天然长。想要追平瞬时流量,必须在流量刚露头的那几十秒就做出扩容动作,这是人手做不到的。
HPA的评估周期默认只有15秒。也就是说,它可以在你刚看到告警的时候就已经把副本数提上去了。扩容速度不一定快多少,但它不会睡过头,不会犹豫,不会在凌晨两点的时候因为"再等等看"而错过窗口。
1.2 推理服务的三个"不听话"特征
AI服务的负载模型和传统Web服务有本质区别,这也是为什么它特别需要HPA,同时也特别难配好。
第一个特征是请求烈度极不均匀。传统Web接口每个请求的成本基本恒定,但大模型推理请求的成本可能差出几个数量级。一个"你好"和一个"请帮我写一篇五千字的报告"放在同一个batch里,GPU算力开销完全不是一个量级。单看QPS根本推断不出真实的资源压力。
第二个特征是服务端batch机制扭曲了资源利用率指标。vLLM这类推理框架会把并发请求攒起来批量推理,GPU利用率可以长时间维持高位,但此时队列可能还在堆积;反过来,GPU利用率不高的时候,队列也可能已经堵了。CPU和内存指标在这种场景下更能严重失真。
第三个特征是单副本贵。一个GPU节点动辄几万块一年,你不可能靠"常年开着50个副本"来应对5分钟的流量峰值。成本约束逼着你必须动态扩缩容。
1.3 谁真的适合HPA?先给服务对号入座
不是所有服务都适合HPA。我见过不少团队把HPA当成万金油,给什么服务都贴一份,结果带来的麻烦比省下的事还多。这里列一个我常用的判断表:
| 业务类型 | 流量特征 | 是否适合HPA | 原因 |
|---|---|---|---|
| 多轮对话ChatBot | 早晚高峰明显,闲时很低 | 很合适 | 潮汐效应明显,低成本时段长 |
| 在线推理API | 突发性强,由外部调用方驱动 | 很合适 | 手动扩根本来不及 |
| 内部开发测试环境 | 恒定低负载,常驻2个Pod就够 | 不建议 | 缩容没有收益,反而增加抖动 |
| 批处理任务 | 由任务队列驱动,运行时长固定 | 看情况 | 需要按队列长度扩,而不是按CPU扩 |
| 离线离线处理 | 负载本身就受调用方节奏控制 | 不建议 | 计算资源可以预留 |
一句话总结:有潮汐、有突发、单副本又贵的服务,是HPA的主场。AI推理服务三个特征全占,基本属于头号适用对象。
2. HPA的决策逻辑:指标、公式、冷却窗口一锅端
2.1 指标从哪来:一条数据管线的完整链路
HPA本身不采集任何指标,它只是各种指标管线的下游消费者。理解这条管线,排错时能少走一半弯路。
Kubernetes里有三类指标能供HPA使用:
- 资源指标(Resource Metrics):CPU、内存这种内置指标,来自每个节点上的kubelet和cAdvisor,由metrics-server汇总,暴露在
metrics.k8s.io这个API下。 - 自定义指标(Custom Metrics):应用自身暴露的业务指标,比如请求队列长度、每秒推理请求数,通过聚合层暴露在
custom.metrics.k8s.io下。 - 外部指标(External Metrics):和Pod没有直接绑定关系的指标,比如消息队列积压量,暴露在
external.metrics.k8s.io下。
你可以这样理解:HPA是个只管下订单的管家,它不看锅里的菜,只看抄表员递给它的数字。架构上,所有指标请求都通过Kubernetes API服务器的聚合层转发,链路是:应用和节点产生数据 → 采集器(metrics-server、Prometheus等)汇总 → 注册为APIService → HPA控制器周期性拉取评估。
常见的误区是以为"装了Prometheus就算能自动扩缩容了"。不是的,Prometheus只是数据源,还需要一层适配器(比如prometheus-adapter)把Prometheus的查询结果翻译成HPA能读的API。KEDA这类控制器本质上也承担了这个翻译和托管工作,后面讲自定义指标时会细说。
2.2 副本数公式拆解:HPA心里那杆秤
HPA每隔15秒做一次"要不要调副本数"的计算。它算的不是直觉,是一个确定公式:
desiredReplicas = ceil[currentReplicas × (currentMetricValue / desiredMetricValue)]翻译一下:现在的副本数,乘以"实际指标值除以目标指标值",向上取整。举个具体例子你就明白了。
现在有4个Pod,CPU平均利用率50%,HPA目标利用率配的60%。代入公式:4 × (50/60) = 3.33,向上取整是4。结果不变,所以副本数保持不变。这就是为什么很多人的HPA看起来"不动"——不是坏了,而是当前负载压根没越过阈值。
换一组数字,当前5个Pod,平均CPU利用率90%,目标60%:5 × (90/60) = 7.5,向上取整是8。HPA一次评估就可能把副本从5调到8。
如果配置了多个指标,HPA会分别计算每个指标对应的期望副本数,取最大值。这是很合理的设计:任何一个维度的压力都不允许被忽视。
还有一个隐藏参数叫容忍度(tolerance),默认0.1。意思是实际指标和目标的比值落在0.9~1.1之间时,控制器不动作,防止微小波动导致副本数来回改。这就是为什么前面的例子算出来3.33取整为4,看起来明明低于目标但没缩容——除了取整,容忍度也提供了缓冲。
2.3 稳定窗口和速率控制:防过山车的两把闸
理解了公式,你会立刻想到一个问题:如果流量出现一个秒级毛刺,HPA是不是马上把副本数拉满,等毛刺过去又立刻缩回来?答案是:如果没有稳定窗口,确实会。这也是新手HPA抖动最频繁的原因。
Kubernetes在HPA里引入了stabilizationWindowSeconds和behavior两个机制。缩容默认有300秒的稳定窗口:HPA评估发现需要缩容时,不会立刻执行,而是先等5分钟,看负载是不是真的持续走低。扩容的稳定窗口默认是0秒,因为扩容通常希望快。
Behavior可以对扩容和缩容的速度分别做策略限制。我最常用的一组策略是:扩容时每60秒最多增加当前副本数的100%,缩容时每60秒最多减少2个Pod,并且缩容前保持5分钟观察。这样既保证流量陡增时能以翻倍速度拉起,又避免几分钟后的缩容把服务打回原形。
这两把闸本质上是拿"短暂的不精确"换"长期的稳定"。用生活类比就是:空调温度到了设定值不会立刻停机,而是要再吹一会儿才压压缩机;如果一过温度就停机,一超温度就开机,压缩机早废了。
3. 快速上手:把HPA配置到你的推理服务上
3.1 动手前先确认三件事
我见过太多人对着HPA文档抄一份YAML就kubectl apply,结果三天后发现TARGETS列全是<unknown>。配置前请先确认三件事,能帮你省掉大量排查时间。
第一,Kubernetes版本够不够新。HPA的autoscaling/v2API从Kubernetes 1.23开始进入稳定版。如果你的集群还是1.20、1.21,请用autoscaling/v2beta2,字段大部分兼容,但别直接抄新版配置。
第二,metrics-server是否正常。执行kubectl get apiservice | grep metrics.k8s.io,确认状态是True而不是False或Unknown。再执行kubectl top node,能返回节点用量才算通。很多云厂商的托管集群默认不带metrics-server,这一步挂了后面全白搭。
第三,Deployment容器必须声明资源requests。这是最容易踩的坑。HPA的CPU利用率是拿"当前使用量"除以"requests里声明的CPU"算出来的,如果容器没写requests,这个除法根本无从做起,HPA就显示<unknown>。注意,写limits不写requests也没用,必须显式给requests。
3.2 写第一份CPU和内存HPA
我建议所有AI服务先跑通最简单的CPU、内存HPA,再上自定义指标。第一份YAML可以直接抄:
apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: llm-inference-hpa spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: llm-inference minReplicas: 2 maxReplicas: 20 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 70 - type: Resource resource: name: memory target: type: Utilization averageUtilization: 80重点看scaleTargetRef:它告诉HPA管哪个Deployment,minReplicas和maxReplicas给副本数划了边界。CPU目标利用率70%,内存80%,任何一个超过自己阈值,HPA都会按前面说的公式去扩容。
运行kubectl apply -f hpa.yaml之后,执行kubectl get hpa -w实时观察。正常情况下TARGETS列会显示70%/70%这种实际和目标的比例,过一会儿如果流量上来,会变成95%/70%,随后副本数开始上涨。
3.3 让AI帮你"讲"一遍HPA,快速扫盲
既然标题是"AI快速掌握HPA",我分享一个我现在带新人常用的方法:利用AI对话助手把HPA的机制快速理解一遍。不需要多高级,随便一个你用得顺手的AI助手都行。
我一般会给这样的指令:
请用最通俗的语言解释Kubernetes HPA的副本计算公式。举例:当前有4个副本,CPU平均使用率50%,目标利用率60%,计算后最终副本数是多少?再举一个使用率90%的例子。最后解释为什么第一个例子计算结果虽然是3.33但取整后是4。
AI通常会把比例关系、取整逻辑讲得比较清楚,尤其适合补充那些你不好意思问人的基础问题。还可以把kubectl describe hpa的输出贴给AI,让它帮你读Events里的报错提示。
但我要提醒一点:AI生成的HPA配置不一定是当前集群的API版本。我就见过AI输出autoscaling/v2beta2的旧格式,还有把averageValue和averageUtilization混用的。AI在这里是"随身教练",不是"最终裁判"。所有它给的结论,都以kubectl api-resources | grep autoscaling和你集群实际运行情况为准。
3.4 验证:配完不等于完事
HPA配置完必须压测,不能只盯着YAML自我感动。我的验证套路分三步。
第一步,准备一个压测工具,wrk、hey都行,对着推理服务的Service发起请求:
wrk -t4 -c200 -d120s http://your-service-address/v1/chat第二步,观察HPA动作。压测开始后30秒内,kubectl get hpa -w应该能看到TARGETS百分比上升;再过1~2分钟,REPLICAS开始增长。如果压了3分钟副本纹丝不动,停掉压测回头看指标源和阈值。
第三步,验证缩容。压测结束后不要急着等缩容,因为缩容有5分钟稳定窗口。你可以趁这个时间观察日志里是否有Pod被终止、新Pod是否正常清理连接。整个过程走完,才敢说这个HPA是可用的。
4. AI推理场景的进阶调优:GPU、队列和冷启动
4.1 为什么CPU指标对GPU推理服务不灵
跑基础HPA只是及格线,真正麻烦的是AI推理服务的特点:瓶颈在GPU,而你最方便拿到的CPU指标偏偏和GPU负载不匹配。
现象很典型:vLLM服务在压测时GPU利用率已经95%,CPU占用却只有30%。这时候如果你只配了CPU的HPA,副本数纹丝不动,但用户已经在排队了。反过来,有些预处理任务密集的服务CPU很高但GPU很闲,照CPU扩容纯粹是浪费显卡。
原因在于推理框架的batch机制。GPU吃的是批量矩阵计算,CPU只负责调度、tokenize、采样这些边角活。想用指标反映真实压力,你得往业务层走。
4.2 用排队长度当扩缩容信号
对大模型推理服务,最直观的"用户可感知压力"指标不是资源利用率,而是请求排队长度。vLLM会在metrics端点暴露类似vllm:num_requests_waiting的队列指标,TGI也有对应的tgi_queue_size,具体名称跟随版本变化,但思路一致。
实现上有两条路:一是用KEDA这种控制器,直接用Prometheus查询触发扩缩容;二是通过prometheus-adapter把指标注册成external metrics,再用原生HPA引用。
我推荐优先试KEDA,因为对你屏蔽了大量指标适配细节,配置更短。一个典型的ScaledObject长这样:
apiVersion: keda.sh/v1alpha1 kind: ScaledObject metadata: name: llm-queue-scaler spec: scaleTargetRef: name: llm-inference minReplicaCount: 2 maxReplicaCount: 20 triggers: - type: prometheus metadata: serverAddress: http://prometheus.monitoring.svc.cluster.local:9090 query: sum(vllm:num_requests_waiting{namespace="default"}) threshold: "5" metricType: AverageValue注意metricType那行。AverageValue表示"所有匹配Pod平均排队数不超过5",适合判断整体是否过载;如果你更关心全局总排队量,可以换Value。KEDA控制器会为这个ScaledObject在背后生成一个HPA对象,所以你仍然可以用kubectl get hpa看到它,它的核心逻辑还是HPA那套。这里query里的PromQL语义很关键,别聚合错了维度,否则指标失真。
4.3 扩容要快、缩容要稳:behavior配置之道
默认HPA的扩容策略是没有速率限制的,看起来"爽快",但遇到突刺会拉满一堆Pod。我的线上配置一定给behavior明确写两套策略:
behavior: scaleUp: stabilizationWindowSeconds: 0 policies: - type: Percent value: 100 periodSeconds: 15 scaleDown: stabilizationWindowSeconds: 300 policies: - type: Pods value: 2 periodSeconds: 60扩容不设稳定窗口,每15秒最多翻一倍,保证突发流量能在几个评估周期内拉出足够副本。缩容设300秒稳定窗口,并且每60秒最多减少2个Pod,给业务留出退潮观察期。这个配法尤其适合模型加载慢的服务。
但就算扩容再快,你还要面对一个残酷事实:大模型服务的冷启动时间是分钟级的。一个推理镜像动辄10GB起步,模型权重可能有十几GB,新Pod光是加载模型就够一分钟。即使HPA瞬间把副本从2扩到10,新Pod也要慢慢ready。对策有几个:minReplicas不要抠得太死,留2~3个"热副本"兜底;镜像做分层缓存;配合startupProbe给足启动时间,别让readiness探针在模型加载期就把新Pod踢出服务。
4.4 一个必须破除的误解:PDB拦不住HPA缩容
很多文章会建议配PodDisruptionBudget(PDB)来防止缩容把在跑的长请求掐断。这是个非常常见的误解,我专门说清楚。
PDB约束的是自愿驱逐(eviction),比如节点维护、滚动更新时Kubernetes主动把Pod赶走。但HPA缩容不是驱逐,它是直接通过ReplicaSet把副本数调小,然后ReplicaSet删掉多余的Pod——这个流程完全不经过PDB的准入检查。所以别指望只配一个PDB就能保护长请求。
真正对HPA缩容有效的是这几件事:
- 给容器配置优雅停止:
preStop钩子加一段等待时间,再配合terminationGracePeriodSeconds,让正在处理的推理请求有收尾时间。 - 适当拉长缩容稳定窗口,等于给所有长请求一段"安全撤退时间"。
- 在Service层面做多副本流量分散,避免缩容瞬间把连接集中在少数Pod上。
PDB在节点维护、集群升级时依然很有用,但它管不了HPA,两者别混为一谈。
4.5 HPA和ClusterAutoscaler的联动
HPA管的只是"够不够Pod",至于节点够不够,那是ClusterAutoscaler(CA)的事。AI服务扩容到一定规模后,集群里所有空闲节点都调度完了,新Pod会卡在Pending。这时候CA会观察到Pending Pod,自动申请新节点。
联动关系看着很美,但有个现实问题:云厂商的新节点从申请到可用,通常要2~5分钟,比Pod冷启动还慢。所以在AI场景里,HPA+CA适合应对"持续数分钟以上的负载上升",不适合应对秒级突刺。我的习惯是:应对秒钟到分钟级突发,靠HPA的minReplicas和热副本;应对长时间业务潮汐,靠CA伸缩节点省钱。两者配合,才算真正的资源管家。
5. 排错链路:HPA不扩容、乱扩容时怎么查
5.1 TARGETS显示<unknown>:先查指标管线
如果kubectl get hpa里TARGETS列显示<unknown>,先别怀疑HPA本身,十有八九是指标没采集上来。
按这条链路排查:
kubectl get hpa kubectl describe hpadescribe输出里的Events很关键。如果看到FailedGetResourceMetric,说明HPA根本没拿到指标。接着查APIService状态:
kubectl get apiservice | grep metrics.k8s.io状态必须是True。然后直接测数据:
kubectl top node kubectl top pod -l app=llm-inference如果top pod返回空或者只有少数Pod,检查这些Pod是否处于Running状态、是否配置了requests、metrics-server是否收集到数据。最阴间的坑是:Pod里只写了limits没写requests,HPA算不了利用率;没有就会一直显示<unknown>,没有任何报错,因为这不是"错误",是"没法算"。
如果是自定义指标,链路更复杂一层:查kubectl get apiservice | grep custom.metrics,确认聚合API可用,再看Prometheus里那个query能否查询出数据。Prometheus里查不到,就往上查应用metrics端点;Prometheus里有但HPA读不到,就查适配器配置。
5.2 配了HPA但就是不扩容:阈值和实例数问题
指标正常但HPA一直按兵不动,是第二常见的问题。我的排查顺序是:
- 看当前实际用量:
kubectl top pod -l app=llm-inference,确定CPU是不是真的超过目标值。 - 检查目标值是不是设太高了。
averageUtilization: 90在个别服务上可能永远到不了,因为90%已经是极端水位了。 - 回忆公式和容忍度:当前利用率50%、目标60%,算出期望副本数不变;90%、目标60%时,如果当前副本数3,
3 × 1.5 = 4.5取整是5,也才加2个。你要是压测量不够,HPA可能真的认为"不需要动"。 - 最后才怀疑behavior配置。有些人把scaleUp的
stabilizationWindowSeconds设成600秒,等于扩个容先犹豫10分钟,看起来就像完全不扩。
5.3 副本数像过山车:全是毛刺惹的祸
另一个典型症状是副本数忽上忽下,比如5分钟内从3扩到8又缩回4再扩到7。根因通常是指标毛刺:某个秒级采样刚好冲到高位,触发扩容;下一轮采样平复,又触发缩容。HPA默认对缩容有5分钟稳定窗口,但扩容的稳定窗口是0,所以毛刺最容易放大扩容动作。
对策分几个层次。最推荐的是让指标本身平滑,如果是自定义指标,在PromQL里套一层窗口函数,比如avg_over_time(metric[3m]),滤掉秒级毛刺。其次是把目标阈值调高一点,用小波动换低抖动。如果还不行,就给scaleUp也加一个稳定窗口,比如60秒,HPA就不会因为单次采样立刻扩容:
behavior: scaleUp: stabilizationWindowSeconds: 60但代价是扩容响应变慢,AI场景要权衡清楚。
5.4 扩容了但依旧超时:最后一公里没走完
最让人崩溃的是:HPA确实扩容了,副本数从2涨到10,但业务依然超时。这时候别骂HPA,它已经做了它该做的。查两个地方。
第一,kubectl get pods看新Pod是不是Ready。大模型服务最典型的坑是启动阶段模型加载很慢,readiness探针配置不当的话,新Pod会被Kubernetes判定为不健康,永远不接入Service后端。我通常同时配startupProbe和readinessProbe,startupProbe给足模型加载时间,readinessProbe负责加载完成后的健康检查。
第二,看新Pod是否真的在接收流量。用kubectl logs看新副本有没有真实请求进来。有时候Service的endpoint因为某些原因没有把新Pod加进去,或者流量亲和性把连接都粘在旧Pod上,副本翻倍了但压力完全没分担。
一句话总结:HPA告诉你"需要更多",但让新副本真正可用,是应用启动设计、探针配置、网络层三者共同的事。HPA是资源管家,不是性能保险。
6. 几个我踩坑后养成的HPA实操习惯
最后分享几个我长期养成的习惯,不一定能让你立刻变高手,但一定能让HPA少出幺蛾子。
第一,凡是新服务上线HPA,必须压测验证扩容和缩容两个方向。只验扩容不验缩容,容易在深夜被"缩过头"背刺。压测停止后,耐心等满5分钟的稳定窗口,再确认副本数降到了合理水位。
第二,监控告警不要盯"HPA是否在扩",要盯"HPA是否已经顶到maxReplicas且持续处于目标值之上超过15分钟"。前者会频繁误报,后者才是真正需要你出面的信号——说明副本上限设低了,或者流量超出了当前集群容量。
第三,Deployment里的requests必须好好写。很多人随手写cpu: 500m,结果HPA的利用率基于500m计算,这个数漂不漂直接决定你的扩缩容准不准。用kubectl top pod观察一段真实用量,再反过来定requests,比拍脑袋靠谱。
第四,用AI辅助写HPA配置时,让AI生成YAML后自己至少核对apiVersion、metric类型、behavior三个点,再翻一遍kubectl api-resources。AI生成的东西可以当草稿,但它对你的集群版本和业务指标一无所知,最终判断必须是你自己。
对我来说,HPA不是"配完就不管"的组件,它更像一个需要偶尔校正的智能管家。它帮你半夜不用爬起来扩缩容,但也需要你在白天花点时间理解它的脾气。把这个管家调教好了,AI服务上线之后,你终于能睡个整觉了。