Day63—AI服务的K8s弹性伸缩:基于GPU利用率的HPA

Day63—AI服务的K8s弹性伸缩:基于GPU利用率的HPA AI 推理服务有个让运维抓狂的特质它吃 GPU但 Kubernetes 原生的 HPA 只看 CPU 和内存。结果就是——流量打进来GPU 已经 99% 占满HPA 却因为 CPU 才 30% 而纹丝不动请求在队列里越积越多P99 从 300ms 飙到 30 秒。这篇文章不聊概念直接给你一套能落地的组合拳DCGM Exporter 采 GPU 指标 → Prometheus Adapter 转成 Custom Metrics → HPA v2 按 GPU 利用率伸缩 → 预热池化解模型冷启动 → 限流 降级兜底。每一段配置都能直接抄。一、为什么 CPU HPA 在 AI 服务上失灵先说清楚一个根因不然后面的配置你会「知其然不知其所以然」。传统 Web 服务是CPU-bound请求来了吃 CPUCPU 高了扩CPU 低了缩很自然。但 AI 推理服务是GPU-bound一次 forward 推理CPU 只是打打杂预处理、tokenize真正干活的是 GPU 的 CUDA Core 和显存。结果就是三种「指标错配」场景CPUGPU原生 HPA 反应流量小、单请求大低15%高95%不扩请求排队流量大、模型小高80%低40%乱扩浪费 GPU显存被打满低显存 100%不扩直接 OOM结论AI 服务必须用 GPU 指标做伸缩CPU 最多当辅助。下面按「采集 → 暴露 → 伸缩」三步走。二、GPU 指标采集DCGM Exporter Prometheus AdapterK8s 自带的 metrics-server 只提供 CPU/Memory压根不认识 GPU。要拿到 GPU 利用率、显存占用、温度业界标准做法是 NVIDIA 的DCGMData Center GPU ManagerDCGM Exporter。2.1 部署 DCGM ExporterDaemonSetDCGM Exporter 以 DaemonSet 跑在每个 GPU 节点上通过 NVIDIA 驱动提供的 NVML 库读取 GPU 状态暴露成 Prometheus 格式的指标。# dcgm-exporter.yaml —— 每个 GPU 节点跑一个采集该节点所有 GPU 指标 apiVersion: apps/v1 kind: DaemonSet metadata: name: dcgm-exporter namespace: monitoring spec: selector: matchLabels: app: dcgm-exporter template: metadata: labels: app: dcgm-exporter spec: # 关键需要 hostPID 才能访问 NVML且挂载设备 hostPID: true hostNetwork: true tolerations: - operator: Exists # 容忍 GPU 节点的污点保证能调度上去 containers: - name: dcgm-exporter image: nvcr.io/nvidia/k8s/dcgm-exporter:3.3.5-3.4.0-ubuntu22.04 env: - name: NVIDIA_VISIBLE_DEVICES value: all ports: - name: metrics containerPort: 9400 volumeMounts: - name: pod-gpu-resources mountPath: /var/lib/kubelet/pod-resources readOnly: true volumes: - name: pod-gpu-resources hostPath: path: /var/lib/kubelet/pod-resources --- # 对应的 Service ServiceMonitor让 Prometheus 自动抓取 apiVersion: v1 kind: Service metadata: name: dcgm-exporter namespace: monitoring spec: selector: app: dcgm-exporter ports: - name: metrics port: 9400 targetPort: 9400部署完访问http://节点IP:9400/metrics你会看到DCGM_FI_DEV_GPU_UTILGPU 利用率、DCGM_FI_DEV_FB_USED显存已用这类指标。DCGM_FI_DEV_GPU_UTIL就是我们伸缩的核心指标。小提示阿里云 ACK 上如果不想自己搭这套可以直接装「云原生 AI 套件 ack-ai-dev-console」它内置了 GPU 指标采集和 Dashboard开箱即用。但自建 DCGM Exporter 的好处是任何云/自建机房行为一致不锁厂商。2.2 Prometheus Adapter 把 GPU 指标变成 Custom MetricsPrometheus 里有指标还不够HPA 只认Custom Metrics API。这一步由Prometheus Adapter负责它把 PromQL 查询结果包装成custom.metrics.k8s.io的 APIHPA 就能直接引用。# prometheus-adapter 的自定义规则把 GPU 指标翻译成 HPA 能懂的 custom metrics # 关键seriesQuery 从 Prometheus 找原始指标metricsQuery 定义最终暴露的名字rules: default: false custom: - seriesQuery: DCGM_FI_DEV_GPU_UTIL{exported_namespace!, exported_pod!} resources: overrides: exported_namespace: { resource: namespace } exported_pod: { resource: pod } name: matches: DCGM_FI_DEV_GPU_UTIL as: gpu_utilization # 对同一个 Pod 的多卡取平均得到「这个 Pod 的平均 GPU 利用率」 metricsQuery: avg by (.GroupBy) (avg_over_time(.Series{.LabelMatchers}[1m]))配置完重启 Prometheus Adapterkubectl get --raw /apis/custom.metrics.k8s.io/v1beta1 | grep gpu能看到pods/gpu_utilization这个指标就说明通了。三、HPA v2按 GPU 利用率伸缩指标就位HPA 配置反而最简单——就是标准 HPA v2 语法把 metrics 从resource换成pods类型的自定义指标。# hpa.yaml —— 按 Pod 平均 GPU 利用率伸缩目标 70% apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: ai-inference-hpa namespace: ai spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: llm-inference minReplicas: 2 maxReplicas: 20 metrics: # 核心GPU 利用率目标 70%留 30% 缓冲应对突发 - type: Pods pods: metric: name: gpu_utilization target: type: AverageValue averageValue: 70 # 辅助显存占用防止利用率不高但显存先爆的情况 - type: Pods pods: metric: name: gpu_memory_utilization target: type: AverageValue averageValue: 80 behavior: scaleUp: stabilizationWindowSeconds: 60 # 扩容60 秒窗口GPU 紧张要快扩 policies: - type: Percent value: 100 # 每次最多翻一倍防止误判打爆 periodSeconds: 60 - type: Pods value: 4 # 或每次最多加 4 个 periodSeconds: 60 selectPolicy: Max scaleDown: stabilizationWindowSeconds: 300 # 缩容5 分钟窗口模型冷启动贵别急着缩 policies: - type: Pods value: 1 # 每次只缩 1 个慢缩 periodSeconds: 120 selectPolicy: Min这里有两个老兵经验值得记住目标值别设太满。GPU 利用率 70% 不是拍脑袋——留 30% 缓冲是因为 GPU 负载是「突发式」的一个 batch 突然变大如果没有缓冲HPA 扩容的那几分钟里队列已经爆了。扩容快、缩容慢。这是 AI 服务的铁律。扩容慢 用户等缩容快 频繁冷启动几十秒的模型加载反而更伤。所以上面scaleUp用 60 秒窗口、scaleDown用 300 秒窗口。四、模型冷启动预热 预热池GPU 伸缩有个 Web 服务没有的「天坑」模型冷启动慢。一个 7B 的模型从 Pod 起来到把权重从磁盘/对象存储 load 进显存少则几秒多则几十秒。这几十秒里 Pod 是「Ready 但不 Serve」流量打进来全是超时。解决思路两条预热warm-up和预热池warm pool。4.1 预热启动即加载加载完才报 Ready用startupProbe 一个「模型加载完成」的健康端点把「进程起来了」和「模型能用了」区分开// Spring Boot AI 推理服务暴露模型加载状态配合 startupProbe RestController public class ModelReadyController { ​ // 原子变量标记模型是否已加载进显存 private static final AtomicBoolean MODEL_LOADED new AtomicBoolean(false); ​ PostConstruct public void warmUp() { // 启动后立即加载模型权重 → 显存加载完才置 true Thread.ofVirtual().start(() - { try { // 触发模型加载 跑一次 dummy 推理让 CUDA 完成初始化 llmService.loadModel(qwen2.5-7b-instruct); llmService.predict(ping); // 预热一次把 kernel 编译缓存也热了 MODEL_LOADED.set(true); } catch (Exception e) { log.error(模型预热失败, e); } }); } ​ // startupProbe 会反复访问这个端点直到返回 200 才算启动成功 GetMapping(/actuator/ready) public ResponseEntityString ready() { return MODEL_LOADED.get() ? ResponseEntity.ok(ready) : ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(loading); } }配合 Deployment 里的探针# deployment.yaml 片段 —— 三探针各司其职 spec: template: spec: containers: - name: llm-inference image: myrepo/llm-inference:1.0 resources: limits: nvidia.com/gpu: 1 # 关键申请 1 张 GPU调度器才会把它放到 GPU 节点 startupProbe: # 启动探针给足模型加载时间最多等 5 分钟 httpGet: path: /actuator/ready port: 8080 failureThreshold: 30 # 30 * 10s 最多等 5 分钟 periodSeconds: 10 readinessProbe: # 就绪探针加载完成后才把流量引进来 httpGet: path: /actuator/ready port: 8080 periodSeconds: 5 livenessProbe: # 存活探针进程还活着就行别用 ready 当 live httpGet: path: /actuator/health/liveness port: 8080 periodSeconds: 10划重点startupProbe 和 readinessProbe 都指向/actuator/ready但含义不同。startupProbe 给模型加载「兜底时间」readinessProbe 决定「流量什么时候进来」。如果只配 liveness/readiness模型还没加载完readiness 失败次数多了 Pod 会被直接干掉重启陷入「加载 → 被杀 → 再加载」的死循环。4.2 预热池预启动 N 个副本温热待命预热解决了「单个 Pod 冷启动」的问题但没解决「扩容时新 Pod 依然要冷启动」的问题。预热池的思路是永远保持比实际需求多 1~2 个「温热副本」一旦 HPA 要扩容直接从池子里拿而不是从零加载。最简单的做法是把minReplicas设成 2并配合「定时预热」业务低谷期定时给所有副本发一个 dummy 请求保持模型常驻显存长时间无请求驱动可能把权重换出显存。// 定时预热任务每隔一段时间发个空请求防止模型被换出显存Component public class ModelKeepWarmTask { ​ Scheduled(fixedDelay 4 * 60 * 1000) // 每 4 分钟一次 public void keepWarm() { // 发一个最小代价的请求让 GPU 保持活跃、权重常驻显存 String resp llmService.predict(ping); log.debug(keep-warm: {}, resp); } }生产上更完整的做法是KEDA 预热KEDA 支持基于「队列长度」的伸缩比 GPU 利用率更贴近业务配合一个常驻的预热副本就能做到「流量一到池子顶上同时后台悄悄加载新副本」。五、降级 限流GPU 打满时的最后防线弹性伸缩再好也有一段时间差扩容快也要 1~2 分钟。在这段「GPU 已经打满、新副本还没起来」的空窗期光靠 HPA 是不够的必须上两道保险限流挡住过量请求和降级GPU 不可用时切到备选路径。5.1 限流保护 GPU 集群不被冲垮AI 服务限流不能用简单的 QPS——因为「1 个短请求」和「1 个长推理请求」资源消耗差百倍。更好的维度是并发数in-flight 请求数或Token 配额。// 基于 Semaphore 的并发限流同时只允许 N 个推理请求进入 GPU Component public class GpuConcurrencyLimiter { ​ // GPU 显存能同时容纳的最大并发推理数压测出来的不是拍脑袋 private final Semaphore semaphore new Semaphore(16); ​ public String predictWithLimit(String prompt) { // tryAcquire 拿不到就立刻拒绝别排队——排队只会让后面的请求更慢 if (!semaphore.tryAcquire()) { throw new GpuBusyException(GPU 繁忙请稍后重试); } try { return llmService.predict(prompt); } finally { semaphore.release(); } } }在网关层Spring Cloud Gateway 或 Sentinel也配一道「预热期熔断」当检测到多个 Pod 处于 loading 状态时直接熔断返回兜底话术「服务繁忙请稍候」而不是让请求无意义地等。5.2 降级GPU 不可用时切 CPU 或小模型这是很多团队忽略的「性价比之王」不是所有请求都值得占 GPU。简单问题用 CPU 推理或小模型只有复杂问题才走大模型 GPU。// 模型路由 降级链复杂问题走 GPU 大模型简单问题走 CPU 小模型 Service public class ModelRouterWithFallback { ​ public String route(String prompt) { // 第一步意图分类轻量CPU 就能跑 boolean complex intentClassifier.isComplex(prompt); ​ try { if (complex) { return gpuBigModel.predict(prompt); // GPUqwen2.5-14b } else { return cpuSmallModel.predict(prompt); // CPUqwen2.5-0.5b延迟高但免费 } } catch (GpuBusyException | TimeoutException e) { // 第二步降级——GPU 打满时复杂问题也先甩给 CPU 小模型兜底 log.warn(GPU 不可用降级到 CPU 小模型: {}, e.getMessage()); return cpuSmallModel.predict(prompt); } } }这套「大模型 GPU 小模型 CPU 兜底」的组合实测能把 GPU 峰值压力削掉 30%~40%因为大量「打招呼、简单问答」根本不需要 14B 的大模型。实战建议照抄版先把 GPU 指标接进来再谈伸缩。别急着写 HPA。先确认kubectl get --raw /apis/custom.metrics.k8s.io/v1beta1 | grep gpu能查到指标这一步不通后面全是空中楼阁。自建就用 DCGM Exporter Prometheus Adapter托管就用 ACK 云原生 AI 套件二选一别混着用。HPA 目标值用压测定别用经验值。70% 是我给的经验值但你的模型、你的 batch size、你的显卡型号都不一样。正确姿势是压测出「GPU 利用率到多少时 P99 开始劣化」这个临界值减 10% 就是你的 target。没有压测数据的伸缩阈值都是玄学。冷启动和伸缩是同一个问题必须一起设计。只做 HPA 不做预热池等于「修了油门忘了打火」。最省事的组合是startupProbe 兜底加载时间 minReplicas2 保底温热 定时 keep-warm 任务三件套上齐冷启动对用户的伤害能降到最低。别在「GPU 要不要留缓冲」上抠留的 30% 缓冲省下来的是无数个深夜的告警。CPU 时代的伸缩是「水涨船高」GPU 时代的伸缩是「量体裁衣」——指标选错扩得越多死得越快。下篇预告第 9 周「云原生与 AI 工程化」收官从下一篇 Day 64 起我们正式杀进大模型的内核——《后端工程师的大模型第一课Transformer 通俗理解》。不需懂高数我会用「图书馆检索」的直觉把 Self-Attention 讲明白让你彻底搞懂这些天一直在调的大模型到底是怎么「看懂」一句话的。往期回顾Day 62 - AI云服务企业级集成百炼/混元/方舟平台接入对比。Day 61 | Docker部署AI推理服务Ollama Open WebUI生产实践。Day60-Serverless函数计算改写传统Spring Boot。Day59-阿里云ACK实战生产级K8s集群部署与运维。90篇JAVA高级深度分析与实践文章做AI的主人代码直接可用,附完整目录