AI全栈知识14:GPU资源弹缩实战 - 从HPA到Cluster Autoscaler
写在前面
GPU贵。一张T4按量付费大概10-30元/小时(不同云厂商价格不同),A100更是几十上百。如果你的AI推理服务24小时跑着,但实际只有白天8小时有流量,那16个小时的钱就白花了。
怎么办?让GPU资源跟着流量走:忙的时候自动加机器,闲的时候自动减机器。这就是"弹缩"。
但GPU弹缩不是简单把CPU弹缩的配置复制过来就行。GPU节点启动慢、成本高、缩容有风险,需要特殊处理。
这篇帮你搞定GPU场景下的完整弹缩方案。
为什么GPU资源必须做弹缩
算笔账
场景:一个AI推理服务,用一张T4 GPU 不做弹缩(24小时运行): 10元/小时 × 24小时 × 30天 = 7200元/月 做弹缩(只在有流量时运行,白天10小时): 10元/小时 × 10小时 × 30天 = 3000元/月 节省:4200元/月(58%)如果用竞价实例(打3-4折)+ 弹缩,成本还能再降:
竞价实例(3折)+ 弹缩(10小时): 3元/小时 × 10小时 × 30天 = 900元/月 相比全天按量付费节省:87%不做弹缩 = 烧钱。GPU越贵,弹缩价值越大。
K8s弹缩体系:两层架构
在K8s中,弹缩分两层:
第一层:HPA(Pod水平自动伸缩)
作用:自动调整Pod副本数。流量大了加Pod,流量小了减Pod。
流量增加 → HPA检测到指标超阈值 → 自动增加Pod数量 流量减少 → HPA检测到指标低于阈值 → 自动减少Pod数量生活类比:超市收银台。顾客多了多开几个窗口,顾客少了关掉几个窗口。
第二层:Cluster Autoscaler(节点自动伸缩)
作用:自动调整节点数量。Pod创建出来但没地方跑(Pending),就自动加节点。节点空了就自动删。
HPA想加Pod → 但集群没有空余GPU节点 → Pod处于Pending状态 Cluster Autoscaler检测到Pending Pod → 向云厂商申请新GPU节点 新节点Ready → Pod调度上去 → 服务恢复生活类比:停车场。车位满了就扩建新的停车层。空了就关掉省电费。
两层协同工作
用户请求增加 ↓ HPA:推理队列变长了,需要从2个Pod扩到4个 ↓ 调度器:目前只有1张GPU卡,只能跑2个Pod,剩下2个Pod Pending ↓ Cluster Autoscaler:检测到Pending,申请新的GPU节点 ↓ 云厂商:分配一台T4实例,加入集群 ↓ 调度器:把Pending的Pod调度到新节点 ↓ 服务恢复正常 --- 用户请求减少 ↓ HPA:队列空了,从4个Pod缩到2个 ↓ 新节点上的Pod被删掉了,节点空闲 ↓ Cluster Autoscaler:检测到节点空闲超过10分钟,删除节点 ↓ 省钱了HPA基础:怎么配置Pod自动伸缩
最简单的HPA(基于CPU)
apiVersion:autoscaling/v2kind:HorizontalPodAutoscalermetadata:name:my-app-hpaspec:scaleTargetRef:apiVersion:apps/v1kind:Deploymentname:my-appminReplicas:1maxReplicas:10metrics:-type:Resourceresource:name:cputarget:type:UtilizationaverageUtilization:70# CPU使用率超过70%就扩意思是:
- 最少1个Pod,最多10个
- 当所有Pod的平均CPU使用率超过70%,就加Pod
- 低于70%就减Pod
这对GPU推理服务够用吗?
不够。因为:
| 问题 | 原因 |
|---|---|
| GPU推理服务的CPU使用率通常很低 | 瓶颈在GPU不在CPU |
| CPU没超70%但用户已经在排队了 | GPU满了,请求堆积 |
| 需要看的是GPU相关指标 | CPU利用率反映不了真实负载 |
GPU推理服务应该用什么指标做HPA
可选指标对比
| 指标 | 来源 | 优点 | 缺点 |
|---|---|---|---|
| GPU利用率 | DCGM Exporter | 直观 | 波动大,不够灵敏 |
| GPU显存使用率 | DCGM Exporter | 好理解 | 模型加载后就是固定值,没意义 |
| 等待中的请求数 | vLLM暴露的指标 | 最能反映真实排队情况 | 需要推理框架支持 |
| 每秒处理请求数(QPS) | 自定义 | 反映吞吐量 | 不同请求长度不同,QPS不准 |
| 请求延迟P99 | 自定义 | 反映用户体验 | 滞后指标(等慢了才扩,来不及) |
最佳实践:用推理队列长度
vLLM暴露了一个关键指标:
vllm:num_requests_waiting # 正在等待处理的请求数逻辑:队列里有人在等 = 当前Pod处理不过来 = 该加Pod了。
这是"领先指标"(leading indicator),能提前感知压力,比"延迟已经变高了"的滞后指标好。
实战:配置GPU推理服务的HPA
前置条件
要用自定义指标做HPA,需要安装:
1. Prometheus(采集指标) 2. Prometheus Adapter(把Prometheus指标转成K8s metrics API) 3. 推理服务暴露指标(vLLM自带/metrics端口)架构:
vLLM Pod --暴露指标--> Prometheus --采集--> Prometheus Adapter --转换--> K8s Metrics API --供给--> HPA第一步:确认vLLM暴露了指标
vLLM启动后默认在端口暴露Prometheus格式的指标:
# 验证指标是否存在curlhttp://vllm-service:8000/metrics|grepnum_requests_waiting输出类似:
vllm:num_requests_waiting 3第二步:配置Prometheus采集
# prometheus-scrape-config-job_name:'vllm'metrics_path:/metricsstatic_configs:-targets:['vllm-service:8000']第三步:配置Prometheus Adapter
# prometheus-adapter-config.yamlapiVersion:v1kind:ConfigMapmetadata:name:prometheus-adapter-configdata:config.yaml:|rules: - seriesQuery: 'vllm:num_requests_waiting' resources: overrides: namespace: {resource: "namespace"} pod: {resource: "pod"} name: matches: "^(.*)$" as: "vllm_requests_waiting" metricsQuery: 'sum(vllm:num_requests_waiting{<<.LabelMatchers>>}) by (<<.GroupBy>>)'第四步:配置HPA
apiVersion:autoscaling/v2kind:HorizontalPodAutoscalermetadata:name:vllm-hpaspec:scaleTargetRef:apiVersion:apps/v1kind:Deploymentname:vllm-inferenceminReplicas:1maxReplicas:5metrics:-type:Podspods:metric:name:vllm_requests_waitingtarget:type:AverageValueaverageValue:"5"# 每个Pod平均等待超过5个请求就扩behavior:scaleUp:stabilizationWindowSeconds:30# 扩容等30秒确认(避免抖动)policies:-type:Podsvalue:2# 每次最多加2个PodperiodSeconds:60scaleDown:stabilizationWindowSeconds:300# 缩容等5分钟确认(避免缩了又要扩)policies:-type:Podsvalue:1# 每次最多减1个PodperiodSeconds:120关键配置解释:
| 配置 | 值 | 为什么这样设 |
|---|---|---|
| minReplicas | 1 | 至少保持一个Pod(避免冷启动) |
| maxReplicas | 5 | 控制最大成本 |
| averageValue | 5 | 每个Pod平均等5个请求就扩容 |
| scaleUp stabilization | 30s | GPU Pod启动慢,别频繁抖动 |
| scaleDown stabilization | 300s | 缩容要谨慎,等5分钟确认真不需要了 |
| scaleDown value | 1 | 每次只缩1个,避免一下子缩太多 |
Cluster Autoscaler:自动加减GPU节点
为什么需要节点级别弹缩
HPA只能加Pod。但如果集群里没有空闲GPU节点了,Pod就会一直Pending:
情况: 当前集群:1台T4节点(已经跑了1个vLLM Pod占满GPU) HPA要加Pod → 新Pod需要GPU → 没有空余GPU → Pending 如果没有Cluster Autoscaler: 新Pod一直Pending → 用户一直等 → 直到运维手动加节点 有Cluster Autoscaler: 检测到Pending Pod → 自动申请新T4节点 → Pod调度上去 → 自动恢复配置示例(AWS EKS)
# cluster-autoscaler配置(节点组)apiVersion:eksctl.io/v1alpha5kind:ClusterConfigmetadata:name:ai-clusterregion:us-east-1managedNodeGroups:# 固定的CPU节点组(不弹缩)-name:cpu-nodesinstanceType:t3.mediumdesiredCapacity:2minSize:2maxSize:2# 可弹缩的GPU节点组-name:gpu-nodesinstanceType:g4dn.xlarge# T4 GPUdesiredCapacity:1minSize:0# 可以缩到0(完全没流量时不花钱)maxSize:4# 最多4台labels:gpu:"true"taints:-key:nvidia.com/gpuvalue:"true"effect:NoSchedule# 只有需要GPU的Pod才调度到这里关键点:
minSize: 0? 完全没流量时GPU节点缩到0台,一分钱不花maxSize: 4? 设上限防止失控- taint ? 防止普通Pod跑到GPU节点上占资源
阿里云ACK配置
# 阿里云节点池配置节点池名称:gpu-autoscale-pool 实例类型:ecs.gn6i-c4g1.xlarge(T4) 付费模式:按量付费(或竞价实例) 最小节点数:0 最大节点数:4 扩容策略:当有Pod因GPU不足Pending时自动扩容 缩容策略:节点空闲10分钟后自动缩容GPU弹缩的特殊问题
问题一:GPU节点启动慢
| 阶段 | CPU节点 | GPU节点 |
|---|---|---|
| 云厂商分配实例 | 30-60秒 | 30-60秒 |
| 节点加入集群 | 30秒 | 30秒 |
| 拉取镜像 | 10秒(镜像小) | 2-5分钟(GPU镜像大,几个GB) |
| 模型加载 | 不需要 | 1-3分钟(把模型加载到GPU显存) |
| 总计 | 1-2分钟 | 4-10分钟 |
从扩容触发到Pod真正能服务,可能要5-10分钟!
解决方案:
| 方案 | 做法 | 适合 |
|---|---|---|
| 保留最小副本 | minReplicas=1,不缩到0 | 有基础流量的服务 |
| 预热节点 | 保留一台空闲GPU节点常驻 | 对延迟敏感的服务 |
| 提前扩容 | 用更敏感的指标,queue=2就扩 | 高峰可预测的场景 |
| 定时扩容 | CronHPA,每天早9点提前扩 | 流量有规律的服务 |
CronHPA示例(定时扩容)
apiVersion:autoscaling.alibabacloud.com/v1beta1kind:CronHorizontalPodAutoscalermetadata:name:vllm-cron-hpaspec:scaleTargetRef:apiVersion:apps/v1kind:Deploymentname:vllm-inferencejobs:-name:"morning-scale-up"schedule:"0 8 * * 1-5"# 工作日早8点targetSize:3# 提前扩到3个Pod-name:"evening-scale-down"schedule:"0 20 * * 1-5"# 工作日晚8点targetSize:1# 缩回1个Pod-name:"weekend-minimum"schedule:"0 0 * * 0,6"# 周末targetSize:1# 保持最小问题二:缩容时正在推理怎么办
缩容时如果Pod正在处理请求,直接杀掉会导致请求失败。
解决方案:优雅终止(Graceful Shutdown)
# Deployment配置spec:template:spec:terminationGracePeriodSeconds:60# 给60秒处理完在手的请求containers:-name:vllmlifecycle:preStop:exec:command:-/bin/sh--c-"sleep 10 && kill -SIGTERM 1"# 先等10秒让新请求去别的Pod同时配合PDB(Pod Disruption Budget):
apiVersion:policy/v1kind:PodDisruptionBudgetmetadata:name:vllm-pdbspec:minAvailable:1# 缩容时至少保留1个Pod在运行selector:matchLabels:app:vllm-inference问题三:扩容抖动
流量刚超阈值就扩,刚降下来就缩,反复折腾。
解决方案:设置稳定窗口
behavior:scaleUp:stabilizationWindowSeconds:60# 超阈值持续60秒才扩scaleDown:stabilizationWindowSeconds:300# 低于阈值持续5分钟才缩经验值:
- 扩容窗口短一些(30-60秒),快速响应
- 缩容窗口长一些(5-10分钟),避免刚缩了又要扩
竞价实例:GPU省钱利器
什么是竞价实例
云厂商把空闲的GPU实例打折卖(通常3-4折),但随时可能被回收(提前2分钟通知)。
| 对比 | 按量付费 | 竞价实例 |
|---|---|---|
| 价格 | 原价 | 3-4折 |
| 稳定性 | 不会被回收 | 可能被回收(2分钟通知) |
| 适合 | 基础负载(不能断) | 弹性负载(断了有兜底) |
混合策略
基础负载:1台按量付费的GPU节点(稳定,不会被回收) 弹性负载:0-3台竞价实例的GPU节点(便宜,被回收了也没关系)为什么竞价实例被回收也没关系?
因为有多个Pod + Cluster Autoscaler兜底:
- 竞价实例被回收 → 上面的Pod被驱逐
- Pod变成Pending → Cluster Autoscaler申请新的竞价实例
- 新实例到了 → Pod重新调度上去
- 期间其他Pod继续服务(有按量付费的兜底)
AWS配置示例
# 混合节点组managedNodeGroups:# 基础节点(按量,稳定)-name:gpu-ondemandinstanceType:g4dn.xlargedesiredCapacity:1minSize:1maxSize:1capacityType:ON_DEMAND# 弹性节点(竞价,便宜)-name:gpu-spotinstanceTypes:-g4dn.xlarge-g4dn.2xlarge# 多选几种,提高竞价成功率desiredCapacity:0minSize:0maxSize:3capacityType:SPOTlabels:capacity-type:spot阿里云竞价实例配置
节点池:gpu-spot-pool 实例类型:ecs.gn6i-c4g1.xlarge(T4) 付费模式:抢占式实例(竞价) 保护期:1小时(创建后1小时内不会被回收) 最大价格:按量付费的40% 最小节点数:0 最大节点数:3成本对比
方案A:全按量付费,2台GPU,24小时 10元 × 2台 × 24h × 30天 = 14400元/月 方案B:1台按量 + 弹缩竞价(白天10小时平均多1台) 按量:10元 × 1台 × 24h × 30天 = 7200元 竞价:3元 × 1台 × 10h × 30天 = 900元 总计:8100元/月 节省:6300元/月(44%)完整架构图
用户请求 ↓ Ingress / Load Balancer ↓ vLLM Pod × N(N由HPA控制) ↓ ↑ GPU Node Pool Cluster Autoscaler (按量1台 + 竞价0-3台) (检测Pending Pod自动加节点) ↓ Prometheus(采集vLLM指标:queue_length、latency等) ↓ HPA(基于queue_length调整Pod数量)监控和告警
弹缩做好了还要能看到运行状态:
| 监控指标 | 说明 | 告警条件 |
|---|---|---|
| Pod副本数 | 当前有几个Pod在跑 | 达到maxReplicas(扛不住了) |
| Pending Pod数 | 有几个Pod在等GPU节点 | 持续Pending超过5分钟 |
| 节点数量变化 | 今天扩了几次缩了几次 | 频繁扩缩(抖动) |
| 竞价实例回收事件 | 有没有被云厂商回收 | 短时间多次回收 |
| 每日GPU成本 | 今天花了多少钱 | 超预算80% |
| 推理队列长度 | 当前排队的请求数 | 持续>10(扩容可能有问题) |
Grafana面板建议
第一行:Pod数量趋势 + 节点数量趋势(看弹缩是否正常) 第二行:推理队列长度 + 请求延迟P99(看用户体验) 第三行:GPU利用率 + 显存使用率(看资源效率) 第四行:每小时成本 + 竞价实例回收次数(看钱)面试怎么说
如果被问"GPU资源怎么做弹缩":
"GPU弹缩分两层:Pod级别用HPA,节点级别用Cluster Autoscaler。
HPA指标不能用CPU利用率(GPU推理服务CPU利用率很低),要用推理队列长度。vLLM暴露了num_requests_waiting指标,队列超过阈值就扩Pod。
Cluster Autoscaler负责在Pod因为没有GPU节点而Pending时,自动向云厂商申请新节点。我会设minSize=0让完全无流量时GPU节点缩到零。
GPU弹缩有几个特殊处理:一是启动慢(5-10分钟),所以用CronHPA提前扩容或保留预热节点。二是缩容要优雅(terminationGracePeriod+PDB),避免杀掉正在推理的Pod。三是用竞价实例省钱,基础负载按量付费,弹性部分用竞价(3-4折),被回收了Cluster Autoscaler会自动补充。
整套方案在我的集群上大约节省了40-50%的GPU成本。"
延伸思考
| 问题 | 答案 |
|---|---|
| 没有K8s能做GPU弹缩吗 | 可以用云厂商的弹性伸缩组(Auto Scaling Group),但没K8s灵活 |
| 竞价实例真的会被回收吗 | 高峰期会。T4相对稳定,A100竞价更容易被回收 |
| 缩到0台后第一个请求怎么办 | 会等5-10分钟。如果不能接受,minSize设为1保留一台 |
| 多个模型共享GPU怎么弹缩 | 用NVIDIA MIG或时间片共享,HPA按每个模型独立配置 |
| 弹缩和成本哪个优先 | 先保证用户体验(不排队),再优化成本。宁可多花点钱也别让用户等太久 |
小结
本篇核心收获:
- GPU必须弹缩:不做弹缩 = 烧钱。弹缩+竞价可省40-80%成本
- 两层弹缩:HPA管Pod数量,Cluster Autoscaler管节点数量
- GPU的HPA指标:不用CPU利用率,用推理队列长度(vllm:num_requests_waiting)
- GPU启动慢:5-10分钟,用CronHPA提前扩或保留预热节点应对
- 缩容要优雅:terminationGracePeriod + PDB,避免杀掉正在推理的请求
- 竞价实例省钱:基础按量+弹性竞价的混合策略,被回收有兜底
下一篇预告
AI全栈知识15:AI应用的可观测性 - Token监控与成本控制
下一篇进入可观测性领域:
- Token使用量统计和分析
- AI调用链追踪
- 成本归因(哪个团队/功能花了多少Token)
- 异常检测(突然Token暴涨怎么发现)
参考链接
- K8s HPA官方文档
- Cluster Autoscaler
- Prometheus Adapter
- AWS EKS Spot实例最佳实践
- vLLM Metrics文档