背景问题:LLM 服务最怕的不是慢,而是失控
大模型推理服务上线后,团队很容易把优化重点放在单次请求延迟、吞吐或 GPU 利用率上。但到了真实生产环境,最棘手的问题往往不是”某一次请求慢”,而是流量突增时系统进入不可恢复的排队状态。
典型现象包括:
- 请求入口还在持续接收流量,但后端 GPU 队列已经堆积。
- P50 延迟看起来可接受,P95/P99 已经快速恶化。
- 自动扩容已经触发,但新副本冷启动、权重加载、模型初始化赶不上流量峰值。
- 客户端重试进一步放大流量,形成 retry storm。
- 最终不是少量请求失败,而是大面积请求一起超时。
因此,LLM Serving 的生产治理不能只问”如何提升吞吐”,还要问:当容量不够时,系统应该怎样有控制地变慢、排队、扩容、拒绝和恢复。
本文的核心思路是:用 SLO 驱动的自动伸缩与准入控制 构建 LLM 推理服务的过载保护闭环。
核心原理:从资源指标切换到 SLO 与 Token 压力
传统 Web 服务常用 CPU、内存、QPS、平均延迟做扩缩容依据。但 LLM 推理有几个特殊性,使这些指标不够用。
请求成本高度不均匀
两个请求的 QPS 都是 1,但成本可能完全不同。一个请求可能只有几十个输入 Token 和几十个输出 Token;另一个请求可能有上万输入 Token,并要求生成长答案。对于 LLM Serving,真正消耗资源的是 Token 工作量、KV Cache、显存占用、解码步数和批处理调度空间。
因此,准入控制不能只按请求数计算。至少要把请求拆成:
| 维度 | 影响 |
|---|---|
input_tokens | 影响 prefill 压力和 TTFT |
max_output_tokens | 影响 decode 持续时间和 TPOT |
tenant_id | 影响配额、公平性和成本归属 |
model_id | 影响权重加载、显存池和实例选择 |
priority | 区分交互式请求、后台任务和批量任务 |
GPU 利用率是滞后信号
GPU 利用率高不一定代表系统健康,低也不一定代表系统空闲。LLM 推理中,排队可能发生在入口、调度器、KV Cache 分配、网络传输、模型副本冷启动等多个位置。只看 GPU 利用率,往往要等问题已经发生后才知道。
Ray Serve 的官方文档也把自动伸缩建立在请求队列和 ongoing requests 上,而不是只看底层资源利用率。target_ongoing_requests 表示 autoscaler 希望每个 replica 维持的平均进行中请求数,该值需要结合请求处理时长和延迟目标调小或调大。
SLO 是保护策略的边界
LLM 服务常见 SLO 可以拆成三类:
| 指标 | 含义 | 保护重点 |
|---|---|---|
| TTFT | Time To First Token,首 Token 时间 | 用户是否感觉系统卡住 |
| TPOT / TBT | 每个输出 Token 的间隔 | 流式输出是否顺滑 |
| E2E Latency | 端到端完成时间 | 长答案、批处理和后台任务体验 |
如果一个请求进入系统后必然超过 SLO,继续接收它并不一定更好。更好的做法是:在入口快速判断,能服务就服务,不能服务就降级、排队、返回 429/503,或者建议客户端稍后重试。
工程落地:构建五层过载保护闭环
第一层:入口限流,先控制流量进入速度
入口限流的目标不是简单拒绝请求,而是避免超过系统可恢复的负载范围。建议至少按以下维度做限流:
- 全局 QPS 限制。
- 单租户 QPS 限制。
- 单租户 Token/min 限制。
- 单模型并发限制。
- 单请求最大输入 Token 和最大输出 Token 限制。
- 高风险接口的更严格限流,例如 agent、tool calling、长上下文分析。
一个实用规则是:限流单位要从 request 升级到 token budget。例如:
tenant_policy:
tenant_a:
requests_per_minute: 600
input_tokens_per_minute: 2_000_000
output_tokens_per_minute: 800_000
max_concurrent_requests: 80
max_input_tokens_per_request: 32_000
max_output_tokens_per_request: 2_000
这样可以避免一个租户用少量超长请求耗尽整个模型池。
第二层:准入控制,判断请求是否值得进入队列
准入控制比限流更接近推理服务本体。它不仅看租户额度,还要看当前服务状态:
- 当前队列长度。
- 估计等待时间。
- 可用 KV Cache 或显存预算。
- 当前模型池副本数。
- 冷启动中的副本数。
- 历史 TTFT/TPOT SLO 达成率。
- 请求剩余 SLO 空间。
可以用一个简化的判断逻辑:
def admit(request, state):
estimated_tokens = request.input_tokens + request.max_output_tokens
estimated_wait_ms = state.queue_delay_ms_p95
estimated_service_ms = estimate_service_time(request, state)
predicted_latency = estimated_wait_ms + estimated_service_ms
if request.tenant.remaining_token_budget < estimated_tokens:
return "reject_quota"
if state.kv_cache_free_blocks < estimate_kv_blocks(request):
return "reject_memory_pressure"
if predicted_latency > request.slo_ms:
if request.priority == "background":
return "defer"
return "reject_fast"
return "admit"
这里的关键不是估算绝对准确,而是避免在明显无法满足 SLO 时继续堆积请求。
第三层:队列分级,避免高优先级请求被长任务拖垮
生产系统不应只有一个 FIFO 队列。至少应区分:
- 交互式请求:聊天、搜索、客服问答,对 TTFT 敏感。
- 后台请求:批量摘要、离线分析、低优先级自动任务。
- 高价值租户请求:付费等级更高,SLO 更严格。
- 长上下文请求:Token 成本高,容易拖慢队列。
多租户场景下,还需要避免一个租户挤占所有容量。可以采用 weighted fair queue、priority queue、token bucket、deadline-aware queue 等策略。近期关于 multi-tenant GPU inference 的研究也指出,输出长度和运行时 Token 漂移会影响调度准确性,因此调度器需要运行时反馈修正,而不是只依赖请求进入时的估算。
第四层:自动伸缩,基于队列与 SLO 而不是单点资源指标
自动伸缩的输入建议至少包括:
- 队列长度和排队时间。
- ongoing requests per replica。
- TTFT P95/P99。
- TPOT P95/P99。
- 每秒输入 Token、每秒输出 Token。
- 过去几分钟 SLO 违约率。
- 正在启动的副本数。
- 冷启动时间分布。
Chiron 论文提出了基于 queue size、utilization 和 SLO 的 hierarchical backpressure,用于 LLM Serving 的自动伸缩;它的核心启发是:扩缩容策略要把请求类型和 SLO 压力纳入判断,而不是把所有请求都当成相同的 QPS。
对于 LLM 服务,可以把这些机制进一步扩展成:
autoscaling:
min_replicas: 2
max_replicas: 20
target_ongoing_requests: 2
max_ongoing_requests: 4
scale_up_signal:
- queue_delay_p95_ms > 800
- ttft_p95_ms > 1500
- input_tokens_per_second > baseline * 1.4
scale_down_signal:
- queue_delay_p95_ms < 200 for 10m
- gpu_memory_pressure < 60% for 10m
- slo_violation_rate < 1% for 10m
这里要避免两个误区:
- 扩容不是立即生效。大模型副本可能需要拉镜像、加载权重、初始化推理引擎、预热 CUDA kernel。
- 扩容不一定解决所有瓶颈。如果瓶颈在共享网关、KV Cache 迁移、租户配额或下游工具调用,单纯增加 replica 没有意义。
第五层:降级和快速失败,防止雪崩扩散
过载时不要只有”等”。可选策略包括:
- 对低优先级请求返回 429,并带
Retry-After。 - 对后台任务进入延迟队列。
- 缩短最大输出 Token。
- 切换到更小模型。
- 关闭高成本功能,例如多轮工具调用、深度推理、长上下文扩展。
- 对相同请求做幂等缓存或结果复用。
- 对客户端重试设置 retry budget,避免无限重试。
一个成熟系统应明确区分:
| 状态 | 系统行为 |
|---|---|
| 正常 | 接收请求,按优先级调度 |
| 轻度拥塞 | 降低低优先级并发,提高扩容敏感度 |
| 中度拥塞 | 部分租户限流,后台任务延迟执行 |
| 重度拥塞 | 快速失败,保护核心交互请求 |
| 恢复阶段 | 逐步放开限流,避免瞬间回流 |
适用场景
这种机制尤其适合以下场景:
- 对话类产品:用户对首 Token 延迟敏感,需要优先保护 TTFT。
- 企业多租户平台:不同客户有不同额度、优先级和成本归属。
- Agent 平台:请求可能触发工具调用、长链路推理和多次模型调用,成本波动大。
- 内部 AI 网关:多个业务共享同一模型池,需要配额和公平性。
- Serverless GPU 推理:冷启动和权重加载明显影响扩容速度。
常见误区
误区一:只要 GPU 利用率高就是好事
GPU 利用率高可能代表资源利用充分,也可能代表系统已经没有调度余量。LLM Serving 更应该关注 goodput,即满足 SLO 的有效吞吐,而不是单纯吞吐。
误区二:所有请求都应该排队等待
如果请求等待后必然超时,排队只会消耗资源并放大尾延迟。此时快速失败反而更利于用户体验和系统恢复。
误区三:自动扩容可以替代限流
自动扩容有延迟,且受 GPU 库存、模型加载、调度器和网络限制影响。入口限流、准入控制和自动扩容必须配合使用。
误区四:按请求数计费和限流就够了
LLM 请求的真实成本由 Token、模型、上下文长度、输出长度和推理策略共同决定。按请求数限流会低估长上下文和长输出请求的压力。
上线检查清单
指标层
- 是否采集 TTFT、TPOT、E2E latency 的 P50/P95/P99?
- 是否区分 input tokens、output tokens、tokens/sec?
- 是否能看到 queue delay、running requests、waiting requests?
- 是否能按 tenant、model、priority 维度拆分指标?
- 是否记录 rejected、deferred、degraded 的原因?
策略层
- 是否有全局、租户、模型三个层级的限流?
- 是否有 token budget,而不是只按 QPS?
- 是否定义交互式、后台、批处理请求的优先级?
- 是否有快速失败策略和 Retry-After?
- 是否有重试预算,避免客户端放大流量?
扩容层
- 是否测过模型副本冷启动时间?
- 是否设置 min replicas 避免完全冷启动?
- 是否用队列和 SLO 指标触发扩容?
- 是否设置 max replicas,避免无限扩容造成成本失控?
- 是否区分扩容中、已就绪、不可用副本?
演练层
- 是否做过突增流量压测?
- 是否做过长上下文混入压测?
- 是否做过单租户恶意或异常高负载测试?
- 是否验证过限流恢复过程?
- 是否验证过监控告警能定位到具体租户和模型?
参考资料
- Ray Serve Autoscaling Guide: https://docs.ray.io/en/latest/serve/autoscaling-guide.html
- Chiron: Hierarchical Autoscaling for Large Language Model Serving: https://arxiv.org/abs/2501.08090
- AIBrix: Towards Scalable, Cost-Effective Large Language Model Inference Infrastructure: https://arxiv.org/abs/2504.03648
- TokenScale: Timely and Accurate Autoscaling for Disaggregated LLM Serving with Token Velocity: https://arxiv.org/abs/2512.03416
- DriftSched: Adaptive QoS-Aware Scheduling under Runtime Token Drift for Multi-Tenant GPU Inference: https://arxiv.org/abs/2606.02982