拓冰建站拓冰建站
首页 / 资讯中心 / 正文

LLM服务突发流量治理:排队延迟、连续批处理与KV cache优化实践

如果你做过一段时间的 LLM 应用开发大概率会碰到这样一种情况模型跑得好好的单次推理延迟也可接受但流量稍微一上来线上服务就像被什么东西卡住一样排队延迟飙升部分请求直接超时GPU 利用率却不一定满载。这个时候你会发现LLM serving 真正棘手的地方可能不是模型本身而是请求的突发性。“Burstiness is all you need for LLM serving”这句话初看很像经典论文标题的戏仿但它点出了一个容易被低估的事实在大模型服务里短时间内的请求集中到达才是决定系统能不能稳住的胜负手。模型推理速度再快如果服务架构不能吸收突发流量用户体验一样会崩塌。这篇文章想结合生产环境里常见的几个真实问题聊聊为什么突发性对 LLM serving 如此关键以及围绕突发性做服务设计时哪些机制比调一个推理参数更值得投入。1. 先搞清楚 LLM 服务里的“突发性”到底指什么很多人谈到突发性第一反应是“瞬时 QPS 很高”。这不算错但太粗糙了。LLM 服务的特殊性在于请求不是一个固定大小的网络包而是一个长度未知、输出长度更未知的生成过程。1.1 突发性的三个常见来源第一个来源是用户行为。高峰时段、活动投放、某个功能突然被大量调用都会让请求在几秒内集中到达。这个在普通 Web 服务里也常见但 LLM 服务的响应时间比普通接口长得多所以同样的突发请求数会带来更长时间的排队积压。第二个来源是请求本身的“内部波动”。同样是调用同一个模型有的 prompt 只有几十个 token有的 prompt 长达几千 token有的请求只需要生成几十个字有的要生成几百上千字。即便每秒请求数没有变化系统需要处理的 token 吞吐量也可能瞬间翻几倍。这种波动不体现在 RPS 上但会直接压垮 KV cache 和调度器。第三个来源是多业务共享集群。多个应用、多个租户共用一套推理服务时单个业务的一次突发可能会把全局显存、算力都拖进来。这也是为什么很多团队一开始用单实例服务时感觉挺好一旦接入多个上游调用方问题就层出不穷。1.2 为什么它比普通 Web 流量更难处理普通 Web 服务接收一个请求处理时间通常是毫秒级请求之间相对独立。LLM 服务不是这样一个请求在 prefill 阶段需要大量并行计算来编码 prompt在 decode 阶段又变成逐步生成不同阶段的资源需求差异很大。如果同一时刻到达的请求有多个系统就要在显存里为每个请求准备 KV cache。突发流量意味着 KV cache 的占用会快速上涨可能瞬间触达显存上限。显存满了以后要么拒绝新请求要么把旧的缓存换出去而换出缓存又会产生额外的调度开销。这就把问题从“显存不够”逐步放大成了“排队延迟”和“服务不可用”。1.3 量化突发性时该看哪些指标不要只盯着平均 QPS。更务实的做法是同时观察几个维度每秒到达的请求数和峰值/平均值之比。正在并发处理的请求数以及最大排队长度。每秒钟实际处理的 prompt token 和 generation token 总量。请求在排队中等待的时间以及完成时间的中位数和 P99。KV cache 的剩余空间和驱逐次数。这些指标综合起来才能判断一个系统是不是真的能处理突发而不是在“低水位下看起来很稳定”。2. 突发性如何一步步传导到系统瓶颈理解了突发性的来源再来看它具体怎么影响一个 LLM 服务。这个过程不是单一环节的问题而是从请求进入网关开始一路传导到 GPU 计算、显存分配最后反映在用户的体感上。2.1 请求从进入到排队的“挤兑”过程当突发请求同时到达网关或推理引擎首先要把请求放入排队队列。这里的核心矛盾是允许排队的请求太多后面的请求可能要等很久允许排队的请求太少又会直接丢弃请求导致服务可用性下降。在普通服务里处理能力是固定的排队通常只是时间问题。在 LLM 服务里排队还会占用后续的资源估算。如果调度器在排队阶段就为每个请求预留 KV cache那么排队的请求越多真正能运行的请求就越少。很多团队遇到过“队列里明明没多少请求但新请求就是进不来”的情况原因就在这里。2.2 连续批处理和动态调度为什么比固定批处理更稳传统 NLP 服务里常见做法是固定 batch size 地批量处理。但 LLM 请求的长度差异太大固定 batch 很容易造成算力浪费。于是出现了连续批处理continuous batching的思路每当一个请求完成生成就把它从当前 batch 里移出同时把等待中的新请求补进来。这意味着 systemized 不再是“一批一批地结束”而是“不断有请求完成、不断有新请求进入”。连续批处理在一定程度上能吸收突发流量但它也不是万能的。如果同一时刻进入的请求都特别长显存和算力一样会被快速占满如果调度器没有考虑请求长度短请求可能会被长请求堵在后面延迟分位数变得非常难看。2.3 显存和 KV cache 成为最大约束突发流量对显存的影响往往是连锁反应。假设原来每个请求平均占用 1GB KV cache并发 10 个就是 10GB。如果突发使得并发在瞬间变成 30 个显存立刻不够用。这时系统会选择暂停新的请求、驱逐部分缓存或者复算已经解码过的内容。驱逐和复算会额外增加延迟极端情况下甚至会导致吞吐下跌。所以很多生产系统会限制最大并发数而不是让 GPU 自行“发挥”。2.4 为什么不能只看平均负载做容量规划最常见的一个误判是拿“平均请求量 × 平均生成长度”来预估需要的 GPU 数。真实流量几乎不可能均匀分布。某段时间内的 token 吞吐可能达到平均值的数倍如果按平均值扩容突发时系统必然过载如果按峰值扩容大部分时间又在浪费资源。比较好的容量规划思路是先定义自己可以接受的最差延迟比如 P99 不超过 5 秒然后通过压测找出系统在多大并发下仍能满足这个目标最后再根据流量预测为峰值预留一定的余量。这比看平均利用率更符合 LLM 服务的特点。3. 围绕突发性做服务设计真正值得投入的是这几块如果把 LLM serving 看成一场“应付突发流量”的工程那么关键不在某一个小功能上而是一整套配合机制。下面这些点是我认为最值得优先做的。3.1 请求入口超时、排队上限、背压和降级不要等到请求进入 GPU 之后才做约束。在入口处就应该把行为定义清楚给每个请求设置合理的最大等待时间超过就直接返回超时或降级结果。设置排队队列的最大长度队列满了以后直接拒绝新请求而不是无限堆积。如果上游服务依赖 LLM 的结果考虑提供降级响应比如返回一个缓存结果或简化版本。这些机制看起来简单但能避免“突发流量把整个服务拖死”的最坏场景。你可以在入口做限流但限流不是简单的计数还要考虑请求长度和输出长度的预估权重。3.2 批处理策略优先做连续批处理但要注意长请求隔离前面提到了连续批处理这里补充一点它可以显著提高资源利用率但最好搭配“最长处理时间限制”和“长度感知调度”。如果一个请求的预估输出特别长可以把它放到独立的低优先级队列避免一直占据批量窗口。在常见推理框架里很多参数都是可以调节的比如最大 batch token 数、最大并发序列数等。你需要根据实际业务请求的长度分布来调而不是用默认值直接上生产。3.3 自动缩放不要只看 GPU 利用率要看排队长度Kubernetes 场景下很多人喜欢按 CPU 或 GPU 利用率来设置 HPA。但 LLM 服务里GPU 利用率高不一定代表吞吐好有可能是在处理大量无谓的缓存驱逐GPU 利用率低也不一定代表没有突发可能只是部分请求在等待显存。更好的弹性伸缩信号是“排队请求的等待时间”和“队列长度”。当等待时间超过阈值时及时扩容当队列清空并稳定一段时间后再缩容。这样既能在突发时快速反应也能避免频繁扩缩容。3.4 缓存和复用把突发里的重复计算消掉突发流量里往往有大量相似请求。同一批用户可能在相同时间问类似问题多轮对话里有重复的系统提示词或者多个请求共享同一个较长的 prompt 前缀。这些场景都适合做“前缀缓存”。如果你的请求有固定的系统提示或较长的 few-shot 示例建议优先开启前缀缓存。这样即使请求整体是新的但 prefill 阶段的大部分计算可以直接复用能把突发流量对算力的冲击降低不少。更激进一点还可以对常见问题做语义缓存但要注意语义一致性和结果时效性。3.5 多级队列和租户隔离如果多个业务共用一套服务一定要做资源隔离。比较现实的做法是给每个租户设置独立的最大并发、排队长度和 token 吞吐上限。这样即使某个业务出现突发也只会影响它自己的“水位”不会把其他业务的请求全部堵住。租户隔离的粒度可以是上游应用、项目、企业客户等。工程上需要一套配额系统而配额的核心不是固定数值而是“每个租户即使在突发时也不能超过全局资源的安全水位”。4. 实操建议从单实例压测到突发场景下的参数调优理论听再多不如压测一轮来得直观。下面这套流程是我自己验证过、推进项目落地时用到的思路。注意具体命令和参数请结合你自己的框架版本调整这里更重要的是“顺序”和“判断逻辑”。4.1 先构造一个能触发突发的压测脚本不要只做均匀的“每秒固定请求数”压测。要逼出真实问题需要用两个阶段模拟突发第一阶段用较低的请求速率跑 2 到 3 分钟把系统预热到稳定状态。第二阶段在 10 到 20 秒内把请求速率提升到平时峰值的 2 到 3 倍。第三阶段把速率降到正常水平观察系统是否能在短时间内恢复。请求本身的 prompt 长度和输出长度也要有一定分布。不要全是短请求最好混合 20% 的长 prompt 和 20% 的长输出。这样才能覆盖真实工作的资源波动。4.2 参数调整优先级突发压测时建议按下面的优先级调整不要一上来就改模型参数请求超时时间。先确认超时时间不是导致异常失败的触发点。最大排队长度。把它设得保守一点优先保证已受理请求能完成。最大并发序列数。这是保护显存和 KV cache 的关键。最大 batch token 数。这个参数决定 GPU 能否把算力用满。是否开启前缀缓存。如果观察到请求有重复前缀优先开。参数之间是联动的。比如你把最大并发调高但 batch token 上限很低可能实际吞吐并不高你把排队长度调大但并发不增加请求只会越积越多。4.3 一条高效的排查链路如果压测中出现了“QPS 不高但延迟很高”的情况请按这个顺序排查看请求是否在排队。如果队列等待时间远大于推理时间说明入口放行太快或者并发上限太低。看 KV cache 是否被频繁驱逐。如果显存利用率很高且有大量驱逐日志说明并发设置过高或缓存复用机制没生效。看 GPU 算力有没有打满。如果利用率中等但 token 吞吐停滞可能是 batch 里的请求长度分布不合理或者调度器经常被打断。看服务端日志里有没有请求被抢断。很多推理框架会因为超时或资源不足主动取消长请求这会引发“看起来处理了很多请求但实际完成率很低”的问题。排查时不要同时改很多参数。一次只改一个变量观察对应指标变化。4.4 不同部署形态下的突发应对差异单机多卡、K8s 集群、Serverless 是三种常见形态应对侧重点完全不同部署形态突发吸收方式主要瓶颈关键策略单机多卡单机内调度显存和算力交织限制并发、开前缀缓存、做排队控制K8s 集群水平扩展扩容速度和流量调度按排队长度做 HPA、多副本隔离Serverless自动扩缩容冷启动和实例上限缩短冷启动、配置最大并发、为长请求预留超时没有一种部署能同时解决所有突发问题。单机可以做到低延迟但弹性差K8s 能扩容但扩容速度可能跟不上突发Serverless 扩容快但对长请求和成本又不太友好。你需要根据业务属性选择再针对短板做补偿。5. 应对突发性的三段式框架先削峰、再控制、最后工程化前面讲的都是具体机制。如果把它提炼成一套可复用的方法论我习惯用“削峰、控制、工程化”这三个词来总结。5.1 削峰让到达系统的请求更平滑削峰不是要拒绝所有突发请求而是通过限流、排队、缓存、降级等手段把短时间的尖峰压平。比如在入口设置令牌桶按权重估算每个请求的“成本”比如对容易重复的请求做缓存比如让上游调用方在时间上稍微错峰。这里的关键是削峰不能只依赖一个限流器还要和业务方约定好异常时的行为。否则你把请求限掉了业务方拿不到结果就会重试重试又变成新的突发形成恶性循环。5.2 控制让每个请求在可控范围里运行削峰之后系统依然会承受一定程度的突发。这时需要让每一个进入系统的请求都处于可控范围。用大白话说就是“这个请求最多占多少资源、最多等多久、最多生成多少个 token”。具体到实现上至少要做以下几件事给单请求设置最大生成 token 数和最长处理时间。在调度器里限制最大并发序列数。在显存层面对 KV cache 进行上限管理。对不同类型的请求设置优先级保证重要请求不被长尾请求拖住。控制层做得好突发流量只会让部分请求变慢而不是让整个服务崩掉。5.3 工程化把应对突发变成系统能力削峰和控制都是被动应对。真正长期有效的是把突发监测、自动伸缩、容量预测和复盘机制固化下来。比如记录每次突发的时间、请求量、token 吞吐、排队指标和服务表现。把突发的特征和业务事件关联起来比如定时任务、运营活动、上游接口调用链。基于历史数据做容量预估并在突发前主动扩容。这些能力不需要一次性做完可以从“每次突发后能拿到一份完整复盘报告”开始逐步完善。5.4 回到标题突发性为什么是 LLM serving 的首要问题你可以优化算子、量化模型、升级推理框架这些都能提升单请求性能。但只要服务要面对真实用户突发性就会一直在那里。它像一个水位决定了系统有没有冗余空间。如果只能记住一句话我会说LLM serving 的设计重点不是让单次请求变快而是让突发流量出现时整个系统依然能保持可用、可控、可观测。这也是“Burstiness is all you need for LLM serving”最直接的意思。它不是否定模型优化而是在提醒我们真正决定线上体验的往往不是模型跑得有多快而是系统如何在一瞬间涌入大量请求时仍然稳定地完成每一次生成。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门