LLM Serving 过载保护生产实战:用 Queue SLO、Backpressure 与弹性扩容守住尾延迟

生产级大模型推理服务最常见的故障,往往不是 GPU 直接 OOM,而是流量突然上升后,已有副本满载、请求持续进入等待队列,Autoscaler 观察到压力才开始申请 GPU、拉起容器、加载模型。等新副本真正 Ready 时,前面那批请求的 TTFT(Time To First Token) 早已超出 SLO。
这类问题的本质是一个时间差:排队恶化的速度通常快于 GPU 副本扩容的速度。
因此,LLM Serving 的容量治理不能只有 Autoscaling,还需要三层协同:
- SLO 感知:判断队列是否正在消耗用户可接受的等待预算;
- Backpressure / Load Shedding:在系统已无法按 SLO 消化新增请求时停止无限排队;
- Autoscaling:增加长期可用吞吐,修复供需缺口。
一句话概括:扩容负责”增加供给”,Backpressure 负责”限制过量需求”,SLO 负责告诉系统”什么时候必须行动”。
核心原理:从 Queue Length 升级到 Queue SLO
1. 不要把 GPU 利用率当成唯一扩容信号
传统 Web 服务习惯按 CPU 或内存利用率扩容,但 LLM 推理有几个特殊点:请求长度差异大、Prefill 与 Decode 负载形态不同、并发调度会改变单请求延迟,而且 GPU 达到较高利用率时未必意味着 SLO 已经恶化。
更直接的生产信号通常包括:
vllm:num_requests_running:当前正在执行的请求数;vllm:num_requests_waiting:等待调度的请求数;vllm:request_queue_time_seconds:请求实际排队时间;vllm:time_to_first_token_seconds:TTFT;vllm:e2e_request_latency_seconds:端到端延迟。
vLLM 的生产指标已经直接暴露这些维度,因此 Autoscaler 不必只盯 GPU utilization。
2. Queue Length 是容量信号,Queue Time 才更接近用户体验
假设同样有 20 个等待请求:
- 模型 A 每秒能消化 10 个请求;
- 模型 B 每秒只能消化 1 个请求。
二者的 queue length 相同,但用户等待风险完全不同。所以更合理的控制逻辑应该关心:
预计等待时间 + 当前排队时间 + 新副本冷启动时间,是否仍在 TTFT SLO 预算内。
可以把它抽象成一个简单判断:
remaining_slo_budget = ttft_slo - current_queue_time
if estimated_scale_up_time > remaining_slo_budget:
start_backpressure()
else:
scale_out()
这里不是要求生产系统精确预测每个请求,而是建立一条基本原则:如果扩容来不及救当前请求,就不要继续把请求塞进无限队列。
3. Autoscaling 与 Backpressure 必须同时存在
Ray Serve 提供了两个很关键但语义不同的参数:
max_ongoing_requests:限制每个副本同时接收的请求数量;max_queued_requests:限制代理或调用端继续排队的请求数量。
达到 max_queued_requests 后,可以直接产生 Backpressure,并返回 HTTP 503;通过 BackpressureConfig 还可以配置为 429 并带 Retry-After。
这比”所有请求都先接收,然后慢慢排队”更适合严格 SLO 的在线推理服务——因为无限队列只是在把容量不足隐藏成尾延迟问题。
工程落地:四层控制闭环
第一层:建立请求级 SLO
先按业务类型给请求分类,不要全站只有一个统一超时时间。例如:
| 请求类型 | 关注指标 | 建议治理方式 |
|---|---|---|
| 在线对话 | TTFT P95 / P99、TPOT | 保留 warm pool,快速扩容,严格限队列 |
| Agent 工具调用 | 端到端延迟、TTFT | 中等队列,允许有限重试 |
| 后台生成任务 | 完成时间 | 可进入异步队列,优先成本效率 |
| Batch 推理 | 作业完成时间 | 不应与交互请求竞争同一 SLO |
这里的关键不是具体阈值,而是 Interactive 和 Batch 必须采用不同的容量策略。
第二层:用 waiting、queue time 和 TTFT 建立告警
最低限度建议建立以下指标:
- P95 / P99 TTFT
- P95 queue_time
num_requests_waiting/num_requests_runningrequest_reject_ratereplica_ready_latency- GPU replica count
其中 replica_ready_latency 很容易被忽略。它应该覆盖从 Autoscaler 决定扩容到 GPU Pod 可真正接受请求的完整时间,包括调度、镜像启动、模型权重加载和初始化。如果模型需要几十秒甚至数分钟才能 Ready,那么只把扩容观察周期调得更快并不能解决问题。
第三层:Autoscaler 以”每副本持续压力”扩容
Ray Serve 的 Autoscaler 使用 target_ongoing_requests 作为每副本希望维持的平均 ongoing request 数,并支持 upscale_delay_s、downscale_delay_s、min_replicas、max_replicas 等参数。
KServe 则可以通过 KEDA 使用 Prometheus 或 OpenTelemetry 指标驱动扩容,其官方示例直接使用 vLLM 的 vllm:num_requests_running 作为扩容指标。
生产上不建议一开始就追求复杂预测模型,更稳妥的步骤是:
- 压测得到单副本在目标 TTFT 下的安全并发;
- 把 Autoscaler target 设置在这个安全并发以下;
- 测量实际 scale-up latency;
- 根据 scale-up latency 决定需要多少
minReplicas或 warm pool; - 再根据历史流量评估是否值得加入预测式扩容。
第四层:超载时主动拒绝,而不是无限排队
建议把系统状态分成三个区间:
| 状态 | 触发条件 | 动作 |
|---|---|---|
| GREEN | queue_time < 40% TTFT budget | 正常接收 |
| YELLOW | queue_time >= 40% TTFT budget 且 waiting requests 持续增长 | 触发快速扩容,降低非关键流量优先级 |
| RED | queue_time + estimated_scale_up_time >= TTFT budget | Backpressure / load shedding,返回 429 或 503 + Retry-After |
阈值需要通过压测校准,不应该机械照搬。但三段式状态机比单纯”超过并发就扩容”更容易解释和运营。
一个容易忽略的新方向:扩容单位不一定永远是”整模型副本”
2026 年 8 月发布的 OpScale 讨论了一个更细的研究方向:传统 Autoscaling 把完整模型副本作为最小扩容单位,但不同算子在运行时的资源弹性并不完全一致,因此可以探索 Operator-level Provisioning and Autoscaling。
这个方向目前更适合作为前沿架构研究,而不是普通团队直接照搬的生产方案,但它提醒了一个重要事实:LLM 推理扩容的成本和响应速度,受”扩容粒度”强烈影响。
现阶段大多数团队更实际的选择仍然是整副本扩容 + warm pool + Backpressure;只有在 GPU 集群规模足够大、SLO 非常严格、资源成本足够敏感时,才值得进一步研究更细粒度的弹性机制。
适用场景
这套方案尤其适合以下业务:
- 面向公网、流量波动明显的 Chat / Copilot 服务;
- 企业内部共享的大模型网关;
- 多租户 GPU 推理平台;
- 需要严格 TTFT 的智能客服、搜索问答;
- 使用 KServe、Ray Serve、vLLM 等构建的 Kubernetes 推理服务。
如果业务本身是离线 Batch,用户不关心秒级 TTFT,那么优先级应转向 GPU 利用率和单位 Token 成本,而不是强行套用在线过载保护逻辑。
常见误区
误区一:GPU 还有余量,所以服务没有过载
LLM 服务是否过载应以 SLO 是否持续可满足 判断,而不是只看 GPU utilization。队列已经快速增长时,即使 GPU 指标还没到 100%,用户体验也可能已经开始恶化。
误区二:只要扩容够快,就不需要限流
扩容存在物理延迟。GPU 调度、镜像启动和模型加载都不可能是零时间。突发流量足够大时,任何 Autoscaler 都需要 Backpressure 做保护。
误区三:把 max queue 设置得越大越安全
大队列不会创造吞吐,只会把失败从”立即拒绝”推迟成”等待很久后超时”。对于交互业务,过大的队列通常意味着更差的 P99。
误区四:minReplicas=0 一定最省钱
Scale-to-zero 可以省空闲 GPU,但会引入冷启动。若模型权重加载时间接近甚至超过用户 TTFT SLO,那么 scale-to-zero 本身就与交互服务目标冲突。
上线检查清单
上线前至少完成以下检查:
- 已定义交互请求的 TTFT P95 / P99 SLO;
- 已压测得到单副本在目标 SLO 下的安全并发;
- 已采集
num_requests_running、num_requests_waiting、queue time、TTFT; - 已测量从扩容决策到新副本 Ready 的真实时间;
- 已设置合理的
minReplicas或 warm pool; - 已限制最大排队请求数;
- 已定义 429/503 与
Retry-After策略; - 客户端已实现指数退避,并限制最大重试次数;
- 已验证突发流量下拒绝率上升时,现有请求的 P99 不会一起失控;
- 已验证降容不会过快导致反复冷启动;
- 已区分在线和 Batch 请求的容量池或优先级。
FAQ
LLM Serving 为什么不能只按 GPU 利用率做自动扩缩容?
因为 GPU 利用率不能直接表达请求排队多久,也无法表达剩余 TTFT 预算。生产系统至少需要联合 waiting requests、queue time、TTFT 和 replica startup latency。
Backpressure 应返回 429 还是 503?
如果语义是”当前租户或调用方请求过多,稍后重试”,429 更容易让客户端理解;如果语义是”服务整体暂时不可用或容量不足”,503 更自然。Ray Serve 的 Backpressure 配置支持这两种状态码,并可附带 Retry-After。
Autoscaler 的目标并发应该怎样确定?
不要从经验拍值。先固定模型、GPU、输入输出长度分布和推理参数做负载测试,以目标 TTFT/TPOT 为约束找到单副本安全并发,再把 Autoscaler target 设置在该边界以内,并留出突发余量。
参考资料
- Ray Serve Autoscaling Guide — https://docs.ray.io/en/latest/serve/autoscaling-guide.html
- Ray Serve Advanced Autoscaling — https://docs.ray.io/en/latest/serve/advanced-guides/advanced-autoscaling.html
- Ray Serve Production Best Practices — Load Shedding — https://docs.ray.io/en/master/serve/production-guide/best-practices.html
- vLLM Production Metrics — https://docs.vllm.ai/en/latest/usage/metrics/
- KServe — Autoscale InferenceService with LLM Metrics — https://kserve.github.io/website/docs/model-serving/generative-inference/autoscaling
- OpScale: Operator-level Provisioning and Autoscaling for LLM Serving — https://arxiv.org/abs/2608.13499