跳到主要内容

LLM 流水线中的背压:排队论在基于 Token 的服务中的应用

· 阅读需 13 分钟
Tian Pan
Software Engineer

凌晨 3 点的重试风暴通常以同样的方式开始:提供商的一次短暂抖动导致少数请求超过了速率限制,你的客户端库对此进行了重试,而这些重试落在了尚未恢复的端点上,导致更多请求失败;在 90 秒内,你的队列深度迅速飙升,而你的提供商仪表板显示你已经用满了 100% 的每分钟 Token 配额(TPM),由此产生的积压工作甚至可以用五位数的美元来衡量。事后分析会将其归结为“惊群效应(thundering herd)”。但诚实的回答是,你在一个容量多变的下游服务之上构建了一个固定吞吐量的重试策略,却忘记了排队论对此早有定论。

大多数知名的服务韧性模式是为那些吞吐量像一堵墙一样固定的下游服务设计的:例如带有连接池的数据库,或者具有已知并发限制的微服务。但 LLM 提供商并非如此。你的有效吞吐量是一个动态目标,受到你的服务层级、所选模型、Prompt 大小、响应大小、一天中的时间,以及同一提供商的其他用户是否正在微调前沿模型的影响。将它视为一根固定的管道,是我今年看到的多数 LLM 故障的根本原因。

本文将探讨如何将标准的排队论工具——利特尔法则(Little's Law)、隔板隔离(bulkheads)、准入控制和令牌桶背压——专门应用于 LLM 的工作负载,以及为什么大多数 SDK 示例中自带的简单重试逻辑与其说是韧性模式,不如说更像是拒绝服务(DoS)攻击工具。

你的提供商是一个容量变动的下游服务

首先看提供商实际暴露了什么。OpenAI、Anthropic 和主要的托管提供商都至少并行执行两个限制:每分钟请求数 (RPM) 和每分钟 Token 数 (TPM)。Anthropic 增加了第三个维度,将输入和输出 Token 分开,因此 Claude 的集成可能会根据它是 Prompt 密集型、生成密集型还是聊天平衡型,而触达三种不同的上限。这些限制都不是保证的;它们是随着容量、你的账户状态和提供商的整体负载而波动的滑动窗口软限制。

这与数据库连接池在两个重要方面有所不同。首先,约束条件在请求之间不断变化——一个长上下文的摘要请求可能会触发 TPM 限制,而高频的分类工作负载可能会触发 RPM 限制,即使它们的成本完全相同。其次,实际容量是部分可观测的:你只有在提供商返回 429 错误后,才知道自己超过了限制,这意味着你对“管道有多满”的本地视图总是滞后且被动的。你无法像查询连接池那样轮询“当前配额使用情况”的指标。

再加上 Token 长度变化的问题。最近关于使用 M/G/1 模型进行 LLM 推理的排队论研究表明,在重尾输出分布下,极少数异常长的响应可能会主导所有人的排队延迟。在你客户请求后的一个 8K Token 生成任务会阻塞其后任意数量的小请求。你的 p99 并不是由你的平均负载决定的,而是由你的流量在那个窗口期内恰好产生的最长输出来决定的。

这意味着“将下游视为固定管道并在失败时重试”的标准心理模型在现实面前无法维系。你需要针对一个动态目标来调整规模、进行调度和处理削峰填谷。

基于 Token 的服务的利特尔法则

利特尔法则 (Little's Law) 是分布式系统中最廉价的数学工具,它在这里同样适用。系统中处理中(in flight)请求的长期平均数量等于到达率乘以每个请求在系统中花费的平均时间:L = λW

对于 LLM 调用链路,带入实际数值:如果你的 Agent 端点每秒接收 50 个请求,且平均端到端延迟(排队 + 提供商调用 + 后处理)为 4 秒,那么平均有 200 个处理中的请求。这就是你调整工作线程池、连接池、内存缓冲区和提供商并发限制时需要参考的数字。如果你的工作线程池只能容纳 100 个并发的处理中请求,那么在稳态下你已经处于排队状态了——而稳态排队并不是稳定的,它是一种缓慢的累积,一旦延迟发生波动,情况就会急剧恶化。

更有用的应用是在另一个方向:利特尔法则告诉你延迟退化时会发生什么。如果提供商的 p50 从 2 秒翻倍至 4 秒(这在模型发布或区域负载偏移期间很常见),在到达率不变的情况下,你的处理中请求数也会翻倍。你系统中任何具有固定并发上限的部分(HTTP 服务器线程数、异步运行时任务槽、提供商客户端池大小)现在的运行负载系数都与一小时前完全不同,你以为拥有的缓冲已经悄然蒸发。

由此可以得出两条实用的规则:

  • 将每个并发限制设置为稳态利特尔法则数值的 2 倍,而不是 1.1 倍。LLM 延迟的方差足够大,以至于运行在临界线上意味着只要提供商出现一分钟的异常,你就会崩溃。
  • 将处理中请求数(in-flight number)作为一等指标进行追踪。大多数团队分别测量到达率和延迟,却从未计算过它们的乘积。将 λ × W 整合到单个仪表板中,并对其二阶导数设置告警——这是你的隔板隔离(bulkheads)即将失效的先行指标。

准入控制层

一旦你接受了下游服务是多变的,且处理中的请求数(in-flight count)随之波动的现实,接下来的问题就是如何在“大门口”进行处理。幼稚的系统会接收每一个请求,将其在内部排队,并让压力以延迟的形式传播。这种做法在出问题之前一直有效,而“出问题”往往来得很突然,因为面向用户的延迟会掩盖队列的增长,直到队列已经达到了病态的程度。

准入控制则反其道而行之。在请求进入处理流水线之前,你先决定系统是否能够吸收它。如果不能,你就明确且立即地拒绝——给出清晰的错误提示,理想情况下还带有 Retry-After 提示。被拒绝的请求变成了调用者的问题,这听起来很残酷,但实际上是唯一的稳定平衡。一个接收无法处理的请求的系统最终会导致所有请求失败;而一个拒绝无法处理的请求的系统则会牺牲部分请求以维持整体运行。

准入决策应该由你对本地处理中任务(in-flight work)的观察驱动,而不是由供应商的响应驱动。当你看到 429 错误时,你已经支付了往返延迟的代价,消耗了一个请求槽位,并在供应商的视角中为你的账户增加了一次失败调用。你的准入层应该在请求被允许尝试调用之前就将其拒绝。

一个合理的 LLM 端点准入流水线如下所示:

加载中…
References:Let's stay in touch and Follow me for more thoughts and updates