你的 LLM 服务在平稳压测下表现完美vLLM 吞吐量漂亮得可以截屏发周报。结果一上线白天被用户一冲TTFT 从 300ms 飙到 6 秒GPU 显存时不时报警甚至直接 OOM。这种“测试环境没问题、生产环境全暴露”的场面绝大多数时候不是模型本身的问题而是流量特征的判断错了。真实世界的 LLM 请求从不是均匀到达的。用户会思考、会停顿、会反复修改 prompt会看到回答慢就重试。这些人类行为叠加在一起形成了高度突发bursty的流量模式。这也是“Burstiness is all you need for LLM serving”这个研究标题想要传达的核心判断与其在稳态吞吐量上死磕几个百分点不如先解决突发流量下的服务稳定性。这篇文章会从 LLM serving 的底层机制出发讲清楚为什么突发流量比均匀流量更危险然后给出可落地的模拟、配置、代码示例和生产建议。读完你可以做三件事复现突发流量下的性能问题、调整服务端参数和调度策略、建立面向 burst 的压测与监控体系。1. 为什么 LLM 服务的性能问题总在“流量上来”之后暴露先看一个典型场景。某团队用 vLLM 部署了一个 8B 模型内部压测用固定并发 32 路持续打 10 分钟吞吐量 1800 tokens/sTTFT 平均 400ms指标报告写得非常漂亮。上线后第二天运营团队搞了一次产品活动用户同时涌入服务直接雪崩。查监控发现 GPU 利用率在 20% 和 99% 之间反复横跳队列深度从 0 涨到几千大量请求超时。这个场景的问题不在压测工具也不在模型而在压测模型与真实流量的差异。固定并发压测制造的是“稳定到达”的请求流等于假设用户像水龙头一样匀速发请求。但真实用户的行为是“波次式”的一波人同时进来处理完一波下一波又同时进来。LLM 请求还有一个特殊之处它不是单一请求而是请求内部包含复杂的计算过程。一个请求进来后要经历 prefill处理 prompt和 decode逐 token 生成两个阶段耗时可能从几百毫秒到几十秒不等。所以 LLM 服务天然对流量突发更敏感——请求到达的突发性会被模型推理本身的非线性放大。很多团队把精力花在优化模型推理内核、调整 continuous batching 参数、尝试 speculative decoding但忽略了最上游的问题流量到达模式。如果流量本身是突发的系统没有针对突发做设计下游再优化也会被请求堆积淹没。2. Burstiness 是什么LLM 流量与其他流量的本质差异Burstiness突发性/突发度描述的是事件到达时间分布的不均匀程度。完全均匀的流量单位时间内到达的请求数恒定突发流量则表现为“一段时间内密集到达然后空闲”而且这种密集到达往往不是平滑的而是自相似的——在大时间尺度看有波峰波谷在小时间尺度看仍然有聚集特征。流量类型到达模式服务时长典型场景传统 Web 请求相对均匀高峰期平滑爬升毫秒级页面访问、API 调用电商秒杀流量极端突发但请求处理简单毫秒级抢购、秒杀流媒体请求长连接到达率低分钟到小时视频播放LLM 推理请求突发到达 长耗时 资源消耗不均秒级对话、代码生成、Agent 调用LLM 流量之所以突发性强有几个结构性原因。第一交互式用户的行为天然是聚集的。用户在聊天界面里打字、停顿、修改、再发送一批人可能在同一时段发问。客服机器人、代码助手、企业内部 Copilot 都有明显的“上班高峰期”。第二Agent 和自动化的出现加剧了突发。多个 Agent 并行调用 LLM API 时上层编排器会同时向服务端打出一批请求。如果上层有重试逻辑突发会被放大——原请求超时后重试请求会在短时间内再次涌入。第三LLM 请求的服务时长方差极大。一个 prompt 可能只有 50 个 token另一个可能长 5000 个 token输出长度从几十到上千不等。这种资源消耗的不均匀性让突发流量对系统的冲击更加不可预测。衡量突发性有一些技术指标比如峰值因子peak-to-average ratio、自相似参数 Hurst exponent、到达间隔的变异系数。在实际工程中最直接的做法是绘制“每秒请求数”曲线观察短窗口1 秒或 5 秒内的峰值与均值之比。如果这个比值长期大于 5你的系统就是典型的突发流量模型。3. LLM Serving 的底层机制为什么突发会放大系统压力要理解为什么突发流量对 LLM serving 的冲击如此之大需要看模型推理过程中三个关键机制。3.1 Prefill 与 Decode 的计算差异LLM 生成过程分两个阶段。Prefill 阶段处理整个输入 prompt并行计算所有 token 的注意力计算密集但可以高度并行Decode 阶段逐 token 生成每一步依赖前一步的结果只能串行访存密集。这两个阶段的计算特征差异导致了一个问题不同长度的请求占用的资源差异巨大。一个长 prompt 请求的 prefill 计算量可能是短 prompt 请求的几十倍。突发流量到来时服务端可能同时面临多个长 prompt 的 prefill 请求计算峰值瞬间拉满。3.2 KV Cache 的线性增长每个请求在推理过程中都需要缓存 Key 和 Value 向量这个缓存称为 KV Cache。KV Cache 的大小与模型层数、注意力头数、序列长度成正比。并发请求越多、序列越长KV Cache 占用显存越大。当突发流量到达时大量请求同时进入 prefill 阶段KV Cache 需求在几毫秒内暴涨。如果显存预留不足服务端必须等待显存释放后才能接受新请求表现为 TTFT 急剧上升。这是很多 vLLM 服务在突发流量下被击穿的直接原因。3.3 Continuous Batching 与突发流量的关系Continuous batching连续批处理是现代 LLM serving 框架的基石vLLM、SGLang、TensorRT-LLM 都在使用。它的核心思想是不再等一个 batch 全部完成后才送入下一批而是一个请求的 decode 结束后立即从队列中拉入新请求补位。这大幅提升了稳态吞吐。但 continuous batching 面对突发流量时有一个隐含假设队列中有持续不断的请求可以补位。如果队列深度在短时间内从 0 涨到几百batching 策略可能来不及调整导致 prefill 请求大量堆积decode 阶段的请求等待队列调度整体延迟飙升。调度器需要在“及时处理新来的 prefill”和“保障已有请求的 decode 吞吐”之间做权衡这正是突发场景下最难的部分。4. “Burstiness is all you need”的核心判断“Burstiness is all you need for LLM serving”这个标题的措辞方式明显是在向 Attention is all you need 致敬但它的核心意图不是提出一个新的注意力机制而是重新定义 LLM serving 系统设计的优先级。这个研究方向的判断可以概括为三句话。第一LLM serving 工作负载的本质特征是突发性而不是稳态吞吐。真实用户流量、Agent 调用流量都以 burst 形式到达系统设计如果只按平均 QPS 规划资源一定会在高峰期出问题。第二面向突发性的设计应该前置。与其在系统被突发放倒后被动扩容不如在调度、批处理、准入控制、弹性伸缩等环节就把突发性作为第一约束条件。第三突发性本身包含可用信息。通过观察请求到达模式可以预测下一轮突发从而提前扩容、调整 batch 策略、做请求优先级排序。换言之burst 不是应该被“硬扛”的噪声而是可以被“利用”的信号。从更宏观的角度看这也是 LLM serving 与传统 Web 服务架构的一个关键分岔点。传统 Web 服务的水平扩展策略——加机器、加负载均衡、加缓存——面对 LLM 这种长耗时、资源消耗不均、状态依赖KV Cache的工作负载时直接搬过来是不够的。你需要在请求调度层面做更多文章。5. 面向 Burstiness 的服务化设计与代码实现这一节给出可落地的方案先构造一个能复现突发流量问题的压测脚本再分析服务端参数最后给出调度层设计和弹性伸缩配置。5.1 流量模拟构造突发场景的压测脚本写压测脚本时很多人习惯用 wrk、hey 这类工具做固定并发压测但这无法模拟 LLM 的突发流量。这里提供一个简单的 Python 脚本用“短时突发 空闲间隔”的模式模拟真实用户行为。# 文件路径burst_simulator.py import threading import time import random import requests TARGET_URL http://localhost:8000/v1/chat/completions MODEL_NAME your-model-name PROMPT 用通俗的语言解释什么是量子纠缠并给出三个生活化类比。 def send_request(request_id: int): payload { model: MODEL_NAME, messages: [{role: user, content: PROMPT}], max_tokens: 512, temperature: 0.7, } start time.time() try: resp requests.post(TARGET_URL, jsonpayload, timeout60) latency time.time() - start if resp.status_code 200: print(f[{request_id}] status200 latency{latency:.2f}s) else: print(f[{request_id}] status{resp.status_code} latency{latency:.2f}s body{resp.text[:200]}) except Exception as e: latency time.time() - start print(f[{request_id}] error{e} latency{latency:.2f}s) def burst_cycle(cycle_id: int): # 每轮突发短时间内发出 15-30 个请求 burst_size random.randint(15, 30) print(f--- cycle {cycle_id}: burst start, size{burst_size} ---) threads [] for i in range(burst_size): t threading.Thread(targetsend_request, args(fc{cycle_id}-r{i},)) t.start() threads.append(t) for t in threads: t.join() print(f--- cycle {cycle_id}: burst end ---) def main(duration_seconds: int 120): start_time time.time() cycle_id 0 while time.time() - start_time duration_seconds: burst_cycle(cycle_id) cycle_id 1 # 突发后的空闲期5-15 秒 idle random.uniform(5, 15) time.sleep(idle) if __name__ __main__: main(duration_secondsint(sys.argv[1]) if len(sys.argv) 1 else 120)这段脚本会以“密集请求、然后空闲”的节奏持续运行 120 秒。运行后观察服务端日志和监控面板如果 TTFT 出现明显尖峰、队列深度快速上涨就说明你的服务在突发流量下存在问题。5.2 服务端配置vLLM 的关键参数vLLM 是目前使用最广泛的 LLM serving 框架之一。以下配置针对突发流量场景做了针对性调整参数含义见注释。python -m vllm.entrypoints.openai.api_server \ --model meta-llama/Meta-Llama-3-8B-Instruct \ --gpu-memory-utilization 0.85 \ --max-num-seqs 64 \ --max-num-batched-tokens 8192 \ --enable-chunked-prefill \ --max-tokens 2048几个关键参数的考量如下。--max-num-seqs限制同时处理的请求数。面对突发流量时这个值不宜设置过大。因为并发请求越多KV Cache 占用越大反而容易触发显存瓶颈导致全队列阻塞。建议从 32 到 64 开始测试。--enable-chunked-prefill将长 prompt 的 prefill 计算拆分成多个小块与 decode 请求交错执行避免一个长 prompt 独占 GPU 导致其他请求长时间等待。这个选项对突发场景非常有用因为它缓解了“长 prefill 请求堵塞 decode 流水线”的典型问题。--max-num-batched-tokens限制一个 batch 内的总 token 数。突发流量下如果这个值设得过大调度器可能把大量请求塞进同一个 batch造成单 batch 执行时间过长后续请求等待加剧。5.3 队列与批处理自适应 Batcher 设计如果你的场景比较特殊或者想更精细地控制调度策略可以在服务前端加一层自定义请求队列做请求级别的优先级管理和自适应批处理。# 文件路径adaptive_batcher.py import asyncio from dataclasses import dataclass, field from typing import List, Optional dataclass(orderTrue) class Request: arrival_time: float request_id: str prompt: str field(compareFalse) max_tokens: int field(compareFalse) # 可以动态调整优先级例如等待时间越长优先级越高 priority: float field(compareFalse, default0.0) class AdaptiveBatcher: 按等待时间和 token 预算构建 batch 的调度器 def __init__(self, max_batch_tokens: int 6144, max_batch_size: int 32): self.max_batch_tokens max_batch_tokens self.max_batch_size max_batch_size self.pending: List[Request] [] self.lock asyncio.Lock() async def submit(self, req: Request): async with self.lock: self.pending.append(req) async def build_batch(self) - List[Request]: 从等待队列中选择一批请求先到先服务 token 预算约束 async with self.lock: if not self.pending: return [] # 按到达时间排序等待最久的请求优先进入 batch self.pending.sort(keylambda r: r.arrival_time) batch: List[Request] [] total_tokens 0 for req in self.pending: estimated_tokens len(req.prompt) req.max_tokens if len(batch) self.max_batch_size: break if total_tokens estimated_tokens self.max_batch_tokens: # 当前请求超预算跳过这里可以根据策略决定是否 break continue batch.append(req) total_tokens estimated_tokens if batch: for req in batch: self.pending.remove(req) return batch这个 Batcher 的设计思路是优先照顾等待时间最长的请求同时用 token 预算避免单个 batch 过大。实际使用时你还需要一个后台任务定期调用build_batch()把取出的请求发送给推理引擎。这样前端队列就有了“平滑突发”的能力——突发到达的请求会先进入队列而后台以可控的节奏消费。5.4 弹性伸缩基于队列深度或突发水平的自动扩缩容Kubernetes 环境下的标准做法是使用 HPA但默认 CPU 指标对 LLM 服务不够敏感。更可靠的方案是基于 Prometheus 指标做自定义伸缩。以下是 KEDA 的 ScaledObject 示例。# 文件路径keda-scaledobject.yaml apiVersion: keda.sh/v1alpha1 kind: ScaledObject metadata: name: llm-serving-burst-scaler spec: scaleTargetRef: name: llm-serving-deployment minReplicaCount: 1 maxReplicaCount: 8 cooldownPeriod: 120 triggers: - type: prometheus metadata: serverAddress: http://prometheus.monitoring.svc:9090 query: | sum(rate(llm_request_queue_depth[1m])) threshold: 50 # 也可以同时叠加多个 trigger例如检查请求到达速率 - type: prometheus metadata: serverAddress: http://prometheus.monitoring.svc:9090 query: | sum(rate(llm_request_total[10s])) threshold: 200这里的思路是用请求到达速率和队列深度作为伸缩信号而不是 CPU 或内存。因为 LLM 服务的瓶颈通常首先是请求调度和显存CPU 指标往往会滞后。6. 运行验证与效果评估6.1 对比测试平稳压测 vs 突发压测要验证你的系统是否真正解决了突发流量问题需要做两组对比测试。第一组固定并发 32持续 5 分钟记录 TTFT 均值和 P95、吞吐量。第二组用第一节的 burst 脚本跑同样时长记录相同指标。然后对比两组结果的差异。如果第二组的 TTFT P95 是第一组的 5 倍以上说明系统在突发下存在问题需要继续调整调度或扩缩容策略。如果两组结果接近说明系统已经能较好平滑突发流量。6.2 关键指标如何解读指标含义突发场景下的合理表现TTFTTime To First Token从请求发出到收到第一个 token 的时间P95 增长不应超过稳态的 2 倍TPOTTime Per Output Token每生成一个 token 的耗时应保持平稳不应因突发明显恶化ITLInter-Token Latency相邻 token 之间的间隔持续抖动说明调度不稳定队列深度等待处理的请求数突发后可快速回落不应持续累积GPU 显存利用率显存占用情况不应触顶导致 OOM 或强制排空SLO 达成率满足延迟目标的请求比例应保持在 95% 以上运行验证的时候建议同时采集服务端日志和 Prometheus 指标不要只看压测脚本的输出。很多时候压测端显示的延迟只是表象真正的根因在服务端的调度日志、显存分配记录和 GC/排队日志里。7. 常见问题与排查思路以下是在突发流量场景下比较常见的问题供排查时对照。问题现象可能原因排查方式解决方案突发到来后 TTFT 飙升请求大量超时队列积压调度器来不及处理 prefill查看队列深度和 vLLM 日志开启 chunked prefill降低 max-num-seqs增加前置队列做平滑GPU 显存占用触顶后 OOMKV Cache 预留不足突发流量瞬间占满显存观察显存监控和 vLLM 启动日志调低 gpu-memory-utilization限制 max-num-seqs使用 PagedAttention 的显存回收策略单 batch 执行时间过长部分请求延迟异常高长 prompt 的 prefill 占用了整个 batch 时间分析 batch 内请求的 token 长度分布启用 chunked prefill设置 max-num-batched-tokens将长 prompt 请求单独调度扩缩容滞后请求已经积压但副本数还没增加伸缩指标不敏感或冷却时间过长查看 KEDA/HPA 事件和指标曲线改用队列深度或请求速率指标缩短冷却时间重试风暴原始请求超时后重试导致服务二次过载客户端重试策略过于激进查看客户端日志中的重试时间戳增加随机退避限制最大重试次数服务端增加过载保护空转时段 GPU 利用率低人为浪费成本最小副本数过高弹性缩容不及时查看副本数与请求速率对应曲线降低 minReplicaCount使用预测式伸缩或定时伸缩排查突发流量问题有一个通用顺序先看客户端有没有重试风暴再看服务端队列有没有积压然后看调度器 batch 构建是否合理最后看 GPU 显存是否在突发瞬间被打满。绝大多数问题都能在这一条链路里定位到。8. 生产环境的最佳实践结合前面几节的方案这里整理一份在生产环境落地时可以参考的实践清单。第一把突发压测纳入 CI/CD 流程。不要只跑固定并发压测至少在每个版本上线前跑一轮突发压测模拟真实用户的“波次式”请求。把突发压测的指标基线纳入 P0 检查项。第二为请求设置优先级和分级准入。不同业务线的请求可以打上不同的优先级标签。在线对话请求需要低延迟可以优先调度离线生成任务可以容忍延迟在突发时降级或进入低优先级队列。很多框架支持全局调度策略但如果你用了自定义队列优先级逻辑可以在队列层实现。第三重视客户端退避与重试策略。服务端的过载保护只能“兜底”最有效的防线在客户端。建议设置指数退避 随机抖动限制最大重试次数为 2 到 3 次并且把重试请求分散到不同实例避免都打到同一个节点上。第四监控要细化到请求级别的 token 维度。只监控 QPS 和延迟是不够的需要同时监控平均 prompt token 数、平均生成 token 数、KV Cache 命中率、每批 token 总数。因为 LLM 服务的负载和请求的 token 长度强相关同样的 QPStoken 长度翻倍系统压力可能翻几倍。第五显存预留策略要保守。不要把 gpu-memory-utilization 设到 0.95 以上。生产环境建议保留 10% 到 20% 的显存余量用于应对 KV Cache 的瞬时波动和显存碎片。突发流量下一点余量可能就是服务稳定与崩溃的差别。第六弹性伸缩要设计冷却期和缩容保护。突发流量结束后不要立刻缩容否则下一波突发可能直接打穿。建议冷却时间设置为 2 到 5 分钟或者使用滑动窗口判断“突发是否真的结束了”。9. 总结与进一步学习方向这篇文章围绕“Burstiness is all you need for LLM serving”这个研究标题讲清楚了 LLM 服务的流量本质、突发流量如何放大系统压力以及面向突发性的系统该如何设计和验证。核心结论可以归纳为几条。LLM 流量天然是突发的这是由用户交互模式和上层自动化机制决定的突发流量与 LLM 推理的 prefill/decode 计算差异、KV Cache 显存增长机制叠加会在毫秒级放大系统压力面向 burst 的设计应该在调度、批处理、准入控制、弹性伸缩四个层面同步发力而不是只靠加机器硬扛验证手段同样不能停留在固定并发压测必须使用突发压测脚本复现真实场景。下一步值得深入的方向包括基于请求到达模式做预测式扩缩容目前多数方案仍是反应式伸缩存在滞后窗口更细粒度的请求调度策略比如基于预估 token 长度的短作业优先调度在混合负载场景下可能有明显收益KV Cache 的突发感知显存管理类似操作系统的内存换页策略在显存紧张时优先淘汰低优先级请求的缓存。建议收藏这篇文章在下次给你的 LLM 服务做上线前检查时按第 5 节的脚本和第 6 节的验证流程跑一遍。突发流量问题不会因为你的模型更大、GPU 更多而自动消失它需要在系统设计层面被正面解决。