推理服务弹性伸缩与冷启动优化:从 HPA 到缩容到零

推理服务的扩缩容比普通 Web 服务难得多:GPU 昂贵、模型加载慢、并发数比 QPS 更能反映压力。用 CPU 利用率做 HPA 指标,往往扩不起来或扩过头。本文讲清推理扩缩容的指标选择(队列深度/KV cache/GPU 利用率)、KEDA 与 HPA 实践、冷启动瓶颈与优化、缩容到零与预热池设计,以及多级伸缩架构。

给推理服务配 HPA(Horizontal Pod Autoscaler)用 CPU 利用率,往往要么纹丝不动、要么疯狂扩缩——因为推理的瓶颈是 GPU、是显存、是队列深度,而不是 CPU。等 CPU 打满时,请求早已在队列里排成长龙;而 GPU 利用率这个指标又滞后、抖动大,直接拿来做伸缩信号会引发「扩缩振荡」。推理弹性伸缩是一套独立的工程问题。本文讲清指标怎么选、冷启动怎么治、缩容到零怎么不翻车。

前置:LLM 推理服务架构设计 、Serverless 推理 、分布式推理与 GPU 集群调度 。

一、为什么推理扩缩容特殊

先看推理服务与普通 Web 服务的三个本质差异:

维度普通 Web 服务推理服务
瓶颈资源CPU / 内存GPU / 显存 / 带宽
扩缩单位成本低(秒级起容器)高(分钟级加载模型)
压力信号QPS / CPU队列深度 / KV cache / 批大小
单实例承载数百并发数十并发
冷启动毫秒~秒秒~分钟(加载权重)
三个直接后果:
① CPU 不是瓶颈 → 用 CPU 做 HPA 指标失灵
② 扩容慢 → 峰值来临时「扩不动」,必须提前/预热
③ 实例贵 → 缩容要谨慎,缩多了扛不住下一个峰值

工程要点:推理伸缩的第一原则是**「用反映真实压力的指标,而非资源利用率」。CPU 利用率在 GPU 服务上基本没用;GPU 利用率滞后且抖动。真正能提前反映压力的,是队列深度与KV cache 占用率**——请求开始排队、KV cache 逼近上限,就说明该扩了。

二、指标选择:选对伸缩信号

2.1 候选指标对比

指标提前性稳定性可获取性适用
CPU 利用率差中易❌ 不适用
GPU 利用率差(滞后)差(抖动)中辅助
队列深度好好需埋点✅ 首选
KV cache 占用好好需埋点✅ 首选
批大小中中需埋点辅助
在飞请求数好中易✅ 推荐
TTFT P95好差需埋点辅助

2.2 为什么队列深度最好

队列深度作为伸缩信号的优势:
□ 领先性:请求在队列里等待时,还没开始消耗算力
  → 队列一涨就扩,能在延迟恶化前响应
□ 线性性:队列长度 ∝ 超出容量,扩 N 个副本能明确消化
□ 稳定性:比 GPU 利用率平滑,不易引发振荡

KV cache 占用率:
□ 直接反映「显存还能接多少并发」
□ 逼近 100% 时,新请求会被迫排队或抢占
□ 是「还能不能接更多」的最准确信号
# 推理引擎暴露的 Prometheus 指标(示例)
# vLLM: vllm:num_requests_waiting  → 队列深度
#       vllm:gpu_cache_usage_perc   → KV cache 占用
#       vllm:num_requests_running   → 在飞请求

def scale_signal(waiting, cache_usage, running):
    # 双阈值:队列积压 或 KV 逼近上限 → 扩容
    if waiting > 8 or cache_usage > 0.9:
        return "scale_up"
    if waiting == 0 and cache_usage < 0.4 and running < 2:
        return "scale_down"
    return "hold"

工程要点:队列深度 + KV cache 占用是推理伸缩的最佳组合信号。队列深度提供领先性(延迟恶化前就能扩),KV cache 占用提供容量判断(还能接多少并发)。避免单独用 GPU 利用率——它既滞后又抖动,是「扩缩振荡」的主要元凶。指标要由推理引擎直接暴露(vLLM/TensorRT-LLM 都有内置 Prometheus 指标)。

三、Kubernetes 实践:HPA 与 KEDA

3.1 HPA:原生伸缩

HPA 支持「自定义指标」,把队列深度接进来:

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: Pods
      pods:
        metric:
          name: vllm_num_requests_waiting   # 队列深度(Prometheus Adapter)
        target:
          type: AverageValue
          averageValue: "8"                 # 每副本平均队列 ≤ 8
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 0         # 扩容要快
      policies:
        - type: Percent
          value: 100                        # 一次最多翻倍
          periodSeconds: 30
    scaleDown:
      stabilizationWindowSeconds: 300       # 缩容要稳(5 分钟观察窗)
      policies:
        - type: Pods
          value: 1                          # 一次最多缩 1 个
          periodSeconds: 60
HPA 配置要点:
□ scaleUp 快:stabilizationWindow=0,允许翻倍
□ scaleDown 慢:观察窗 300s + 每次缩 1 个 → 防抖动
□ 指标用 AverageValue(每副本平均),非总量
□ 需要 prometheus-adapter 把队列指标暴露给 HPA

3.2 KEDA:事件驱动伸缩(含缩容到零)

KEDA 比 HPA 更灵活,支持「基于外部事件源」伸缩,并支持 scale-to-zero:

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: llm-inference-scaledobject
spec:
  scaleTargetRef:
    name: llm-inference
  minReplicaCount: 0            # 允许缩到零
  maxReplicaCount: 20
  cooldownPeriod: 300           # 缩容冷却
  triggers:
    - type: prometheus
      metadata:
        serverAddress: http://prometheus:9090
        query: |
          avg(vllm:num_requests_waiting{app="llm-inference"})
        threshold: "8"
    - type: prometheus
      metadata:
        serverAddress: http://prometheus:9090
        query: |
          avg(vllm:gpu_cache_usage_perc{app="llm-inference"})
        threshold: "0.85"
HPA vs KEDA:
□ HPA:K8s 原生,指标来自 metrics-server/自定义指标
□ KEDA:支持任意事件源(Prometheus/Kafka/Redis/HTTP),支持缩到零
□ 推理场景推荐 KEDA(队列/KV 指标来自 Prometheus,天然契合)

工程要点:扩快缩慢是伸缩配置的铁律。扩容要「零延迟 + 允许翻倍」(峰值来得快),缩容要「长观察窗 + 每次缩一个」(避免抖动导致反复起停,而起停一次就是几分钟的模型加载)。推理场景优先用 KEDA——它支持 Prometheus 指标与缩容到零,比 HPA 更贴合推理的负载特征。

四、冷启动优化

推理服务的冷启动是「从零到能服务」的时间,往往以分钟计:

冷启动耗时分解(典型 7B 模型):
□ 调度 + 容器拉起          5-15s
□ 镜像拉取(已缓存则跳过)  0-60s
□ 模型权重加载(本地)      10-60s
□ 引擎初始化(编译/CUDA Graph 捕获)  5-30s
□ 首次推理预热              1-5s
────────────────────────────────
合计:约 30s ~ 3 分钟

4.1 优化手段

手段做法收益
权重本地缓存模型放节点本地 NVMe / 镜像层省拉取与下载
权重预取扩容前先下载到目标节点省加载等待
镜像瘦身只装必需依赖省拉取时间
引擎快照保存已编译引擎(TensorRT)省编译
实例池预热常备 warm 实例秒级接管
并发加载分片并行加载权重缩短加载
# 权重本地缓存:用 hostPath / local PV 挂载模型目录
# 节点上预置模型,Pod 直接挂载,避免每次下载

# 镜像层缓存:模型作为独立层,与其他依赖分层
# → 代码更新时模型层命中缓存,不重复拉取

4.2 预热与就绪探针

# 就绪探针必须等「真正能服务」,而非「进程起来了」
readinessProbe:
  httpGet:
    path: /health/ready      # 引擎加载完成 + 预热跑过一次
    port: 8000
  initialDelaySeconds: 10
  periodSeconds: 5
  failureThreshold: 60       # 允许最多 5 分钟才就绪
关键:readiness 不能只探「端口通」——
□ 必须确认:模型加载完成 + 引擎初始化完成 + 预热请求跑过
□ 否则流量会在「进程起来但引擎没就绪」时打到实例上 → 全失败
□ 预热请求应覆盖「典型输入形状」,触发 CUDA Graph 捕获等一次性开销

工程要点:冷启动优化的投入产出比很高——它直接决定「峰值时能不能及时扩起来」。权重本地缓存 + 镜像分层 + 预热池是三个最有效的手段。就绪探针务必探测「引擎真正可服务」,别只看端口——否则扩出来的实例会在未就绪时接收流量,把「扩容」变成「扩故障」。

五、缩容与缩容到零

5.1 缩容为什么危险

缩容的风险:
□ 缩太快 → 下一个峰值来了,扩容又来不及(冷启动几分钟)
□ 抖动 → 缩了又扩,反复加载模型(浪费 + 不稳定)
□ 缩掉正忙的实例 → 在飞请求被中断

缩容纪律:
□ 长冷却期(cooldown ≥ 模型冷启动时间的数倍)
□ 每次只缩 1 个
□ 优雅终止:先摘流量,等请求排空,再停实例
□ 保留最小副本数(除非确定可缩到零)

5.2 缩容到零(Scale to Zero)

低流量场景(如内部工具、夜间低谷)缩到零能省大量成本,但有代价:

缩到零的前提:
□ 能接受「首个请求等待冷启动」(秒~分钟级)
□ 有预热池或快速冷启动方案
□ 有请求排队机制(缩零期间请求先入队)

缩到零的方案:
□ KEDA minReplicaCount: 0 + 队列触发
□ 请求入队 → 触发扩容 → 实例就绪 → 消费队列
□ 配合「预热池」:保留 0 副本的常驻引擎快照,快速实例化

与 Serverless 的衔接:
□ 缩到零本质是「冷启动换成本」
□ 详见 Serverless 推理的实例池与预热分级
缩到零 vs 保留最小副本:
□ 缩到零:省成本,但首请求慢(适合低频、可容忍延迟)
□ 保留 1-2 副本:常驻成本,但无冷启动(适合在线、延迟敏感)
□ 折中:低谷保留 1 副本,深夜缩到零

工程要点:缩容比扩容更需要克制。缩容省的是钱,但代价可能是「峰值时扩不回来」。默认保留最小副本;只有在「负载可预测 + 能接受冷启动」时才缩到零。缩到零必须配队列机制——请求先入队等实例,而不是直接失败。相关成本权衡见 Serverless 推理 。

六、多级伸缩与预热池

成熟的推理平台往往不止一层伸缩:

多级伸缩架构:
Level 1  副本级(HPA/KEDA)
  → 增减同一模型的副本数
Level 2  节点级(Cluster Autoscaler)
  → 增减 GPU 节点(副本需要 GPU 时才触发)
Level 3  模型级(多模型共享池)
  → 按请求量加载/卸载模型到共享 GPU(见多 LoRA 服务)

预热池(Warm Pool):
□ 常备 N 个「已加载模型但未接流量」的实例
□ 扩容时从池中直接接管(秒级),池空了才走冷启动
□ 池大小按「峰值扩容量」与「可接受冷启动比例」权衡
伸缩决策树:
请求量上升?
├─ 有 warm 实例 → 直接从池中接管(秒级)
├─ 无 warm,有 GPU 空闲 → 冷启动新实例(分钟级)
└─ 无 GPU → 触发节点扩容(更慢)→ 再冷启动

→ 预热池的作用:把「分钟级扩容」变成「秒级扩容」
→ 代价:常驻成本(池中实例空转)
→ 权衡:峰值可预测 → 池大一点;波动大 → 靠弹性

GPU 资源的共享与隔离(MPS/MIG)也会影响伸缩粒度——详见 GPU 共享与调度 。整套伸缩体系的监控指标,纳入 LLM 服务可观测性 的容量水位看板。

七、速查表与一句话记忆

问题一句话答案
别用什么指标CPU 利用率(推理瓶颈不是 CPU)
首选指标队列深度 + KV cache 占用率
配置铁律扩快缩慢(扩容零延迟,缩容长观察窗)
工具选择推理场景优先 KEDA(Prometheus + 缩零)
冷启动瓶颈模型加载 + 引擎初始化,以分钟计
冷启动优化权重本地缓存 + 镜像分层 + 预热池
就绪探针探「引擎可服务」,不是「端口通」
缩容纪律长冷却 + 每次缩 1 + 优雅终止 + 最小副本
缩到零需队列机制,用冷启动换成本
多级伸缩副本级 + 节点级 + 模型级 + 预热池

一句话记忆:推理弹性伸缩 = 用队列深度/KV cache 做信号(别用 CPU)+ 扩快缩慢 + KEDA 事件驱动 + 权重本地缓存治冷启动 + 就绪探针探引擎 + 预热池把分钟级扩容变秒级——「信号要领先,扩容要快,缩容要稳」。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「ai」更多文章

  1. 排序学习与搜索召回排序系统
  2. 数据版本控制与血缘:DVC 与 LakeFS
  3. 模型可解释性:SHAP、LIME 与注意力归因