给推理服务配 HPA(Horizontal Pod Autoscaler)用 CPU 利用率,往往要么纹丝不动、要么疯狂扩缩——因为推理的瓶颈是 GPU、是显存、是队列深度,而不是 CPU。等 CPU 打满时,请求早已在队列里排成长龙;而 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 事件驱动 + 权重本地缓存治冷启动 + 就绪探针探引擎 + 预热池把分钟级扩容变秒级——「信号要领先,扩容要快,缩容要稳」。
延伸阅读
- LLM 推理服务架构设计
- Serverless 推理:冷启动优化与 GPU 弹性
- 分布式推理与 GPU 集群调度
- GPU 共享与调度:MPS、MIG 与多租户隔离
- LLM 服务可观测性
- Kubernetes 上的 LLM 服务与自动扩缩容
- Kubernetes 自动扩缩容机制 — HPA/VPA/KEDA 底层
- Kubernetes 上的 AI/ML 与 GPU 调度 — GPU 节点调度
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。