跳到主要内容

你从未进行过压测的每分钟 Token 上限

· 阅读需 11 分钟
Tian Pan
Software Engineer

Demo 成功了。Beta 测试也通过了。接着,正式发布带来的流量是平时的十倍,直接撞上了你从未进行过压测的每分钟 Token 配额,而在产品最受瞩目的时刻,所有超出上限的用户都收到了 429 错误。

这是无人排练过的故障模式。团队会偏执地对自己的服务器进行压测——副本、连接池、数据库索引——然后将每个请求都路由到一个存在于别人账户中的供应商配额,而那个上限他们从未真正触及过。限流不是一个在 try 块中捕获的错误。它是一个硬性的产品约束,如果你没有围绕它进行规划,那么产品发布之时,就是你第一次发现坑在哪里的时刻。

令人不安的是,这个上限在撞上之前是隐形的。你的仪表盘显示延迟和错误率都很健康,因为在开发和 Beta 阶段,你从未接近过它。供应商的限制是断崖,而不是斜坡:在达到每分钟 Token 允许量的 95% 时你还安然无恙,但在 101% 时就会彻底崩溃。在上升过程中没有逐渐降级的预警——只有一面在你整个发布会的观众面前,直接撞上去才会发现的墙。

为什么这不同于扩展无状态 Web 服务器

你在扩展 Web 基础设施时建立的所有直觉在这里都是错的,而且这种错误微妙到足以让你吃大亏。

当一个无状态服务变热时,你会增加副本。容量是你拥有并可以在几分钟内配置的东西——启动更多容器,扩大自动扩展组,搞定。约束条件是你的计算预算,而你控制着旋钮。

对于托管模型,约束条件存在于别人的账户中。你的每分钟 Token 数 (TPM) 和每分钟请求数 (RPM) 限制由你的供应商层级决定,无论你如何扩展 自己 的集群,都无法改变它们。你可以在负载均衡器后运行一千个应用副本,但每一个副本都在消耗同一个共享配额。在你这边增加容量只意味着有更多的客户端在竞相更快地撞上同一个上限。瓶颈移到了你的影响范围之外,你大部分的扩展方案都无法触及它。

限制本身也比“每秒请求数”更为复杂。供应商同时在多个维度进行计量——RPM、TPM,有时还有每日请求数和每日 Token 数——你只要触及其中任何一个就会受限。Anthropic 将 Token 维度进一步细分为每分钟输入 Token 数和每分钟输出 Token 数这两个独立的桶,因此长提示词密集的负载可能会耗尽输入上限,而输出上限却处于闲置状态。你的有效容量不是一个数字;它是多个数字中的最小值,而具体哪个会卡住你,取决于你流量的 特征,而不仅仅是流量的大小。

而且,层级是根据支出而非需求来划分的。在 OpenAI,随着你的累计平台支出超过阈值,更高层级才会解锁;Anthropic 使用类似的基于支出的阶梯,最高层级需要数千美元的历史记录。一个处于较低层级的新项目,其 TPM 限制可能在发布峰值出现的几秒钟内就被冲破——而且你无法立即通过购买来提升层级,因为层级反映的是你尚未拥有的历史记录。

你真正需要预测的是什么

“多少用户”是一个错误的单位。供应商的限制是以每分钟 Token 数为单位的,因此你的预测也必须如此,而要做到这一点,意味着需要估算一些大多数团队都会忽略的事情。

尾部请求的单次请求 Token 数——而非平均值。 基于平均请求构建的容量计划,在面对极端请求时必然失败。一个将 40 页合同粘贴到你的摘要生成器中的用户,消耗的 Token 是中位数查询的 20 倍,而发布时的流量最先引出的正是这些高级用户。请预测 p95 和 p99 的单次请求 Token 成本,因为这些请求最接近上限。输入和输出都会计入你的配额,因此一个生成长文本的功能在输出端和输入端都会消耗预算。

峰值并发请求数,而非每日总量。 限流是一个按分钟计的桶。“每天一百万次请求”几乎说明不了什么;同样的每日总量,既可以表现为平缓的涓涓细流,也可以在你的发布邮件发出时,表现为 60 秒内洪水般的冲击。重要的是在同一个滑动分钟窗口内有多少请求落入,因为这才是供应商计量的单位。

突发流量的特征。 发布峰值和稳态流量是截然不同的,它们的失效方式也不同。稳态是一个稳定性问题——你是否能在没有缓慢泄漏的情况下维持数小时。发布是一个突刺问题——随着营销活动、媒体报道或病毒式传播在几秒钟内倾泻流量,呈现出近乎垂直的斜率,接着是重连风暴,因为重试请求会堆积在首次请求之上。如果你只测试过平缓的上升,那你测试的曲线就是错的。正是在这个突刺阶段,重连风暴会在你余量最少的时候让你的有效负载翻倍。

计算一下:峰值并发请求数 × p99 单次请求 Token 数,并将其与你所属层级的每分钟上限进行对比。如果乘积超过了限制,你需要在发布 之前 做出决定,而不是在发布期间去捕获 429 错误。

提升缓冲空间的杠杆

一旦你意识到上限太低,有四个应对招式,而最重要的一招其提前期是以周计算的。

尽早申请配额提升。 基于支出的层级升级并非即时的,人工增加配额的请求需要经过后台人员审核。如果你的容量计算显示需要更高的限制,请在发布前数周提交申请,而不是在发布前一晚,因为那时审批根本来不及。把供应商配额视作任何其他长提前期的依赖项:提前订购。这是人们在发布前夜晚上 11 点才发现需要的杠杆,而那时往往已经太晚了。

为溢出流量准备回退模型层级。 当你在高级模型上遇到速率限制时,路由到一个更便宜或更小的模型总好过直接报错——而且回退模型通常来自一个独立的配额池,因此它是名副其实的额外缓冲空间,而不是换个名字的同一个存储桶。一个更短、更便宜的答案,其用户体验也好过超时的加载动画。在需要之前建立回退阶梯:主模型,然后是二级层级,最后是精简上下文的降级变体。用户得到了答案;你保住了系统的运行。

使用队列和降级,而不是硬性报错。 直接返回给用户的 429 报错是最糟糕的结果。通过准入控制和队列,超过上限的请求会等待片刻而不是直接失效——队列吸收了峰值,并以可持续的速率消耗你的配额。当队列本身已满时,有意识地进行降级:快速拒绝并附带清晰的消息,将请求加入更短上下文的变体队列,或者转入回退管道。背压(Backpressure)将一个预算熔断事件转变为一个虽慢但活着的体验。目标是让上限产生的是缓慢,而不是一堵

规划 Prompt 长度以挤出更多配额。 因为限制是以 Token 为单位的,你少发送的每一个 Token 都是你找回的容量。精简臃肿的系统提示词,限制检索到的上下文,并将 max_tokens 设置为你实际预期的输出,这些都能让相同的上限发挥更大的作用。Prompt 缓存也有帮助,供应商会针对重复的前缀 Token 提供折扣。这是唯一一个不需要成本且能立即生效的杠杆——你不是在购买更多空间,而是在每次请求中使用更少的空间。

一个悄悄破坏容量计划的隐患:重试。对 429 报错进行盲目的重试会将峰值放大为自残式的拒绝服务,因为每个被限流的客户端都会再次冲向已经饱和的配额。使用带有抖动的指数退避,尊重供应商在 429 报错中发送的 Retry-After 响应头,并且永远不要重试不可重试的错误——重试 400 错误就是在焚烧配额。你的重试策略是容量计划的一部分,而不是之下的一个小细节。

在用户之前压力测试上限

如果你从不测试极限,那么所有的预测都毫无意义。压力测试的目的不是为了证明你的服务器很快,而是为了在受控环境中让流量直面供应商的上限,从而让你按自己的节奏了解瓶颈在哪里。

构建真实的 Prompt 语料库。 从实际生产分布中抽取 50 到 100 个 Prompt——短查询、长上下文请求、多轮对话、极端边缘案例——并在测试期间随机选择。一个发送一万次相同短 Prompt 的压力测试无法让你了解 TPM 上限的任何信息,因为 Token 成本是上限计量的标准,而统一的 Prompt 掩盖了至关重要的差异性。

测试突发形状,而不只是缓慢增长。 首先逐渐提升流量,找到延迟和错误率开始波动的拐点——那才是你真实的上限,通常低于文档中的数字。然后运行实际的峰值测试:在几秒钟内跃升至发布级别的并发量,观察哪里会崩溃,因为这种近乎垂直的曲线才是发布时真正会遇到的。增加持续负载的浸泡测试(Soak Test),以捕获短时间突发所掩盖的缓慢降级。

衡量揭示饱和度的指标,这些指标不同于传统的 Web 压力测试。 首个 Token 时间(Time-to-first-token)显示模型在压力下何时开始响应。每秒 Token 数(Tokens per second)衡量你获得的真实吞吐量。而 p95/p99 延迟则准确显示了你最慢的用户何时开始感到痛苦——平均值在这里会误导你,因为 LLM 响应时间差异巨大,平均值看起来可能很平稳,而尾部延迟可能已经爆炸。

一个诚实的警告: 这种测试需要真金白银,因为你正在为你发送的每一个 Token 付费。一个推送一万个长上下文请求的压力测试可能会花费不少美元,而这种成本正是关键所在——这是在用户为你发现上限之前,提前了解上限的代价。为此预留预算,在真实的层级上运行,并将发票视为防止发布失败的廉价保险。

上限是设计输入,而不是异常

心态上的转变很小,但能改变一切:停止将速率限制视为需要捕获的错误,并开始将其视为产品的固定维度,就像延迟或成本一样。它有一个具体的数值。这个数值在发布前是可知的。每一个架构决策——选择哪个模型、Prompt 有多长、是否排队、何时降级——实际上都是关于你如何分配那个无法完全控制的每分钟预算的决策。

在发布过程中没有崩溃的团队都做了同样枯燥的工作:他们预测了尾部情况下的每分钟 Token 数,提前数周申请了配额提升,在需要之前建立了回退阶梯,并对流量峰值进行了压力测试,直到亲眼目睹瓶颈所在。发布之所以平稳无趣,是因为困难的工作已在之前的数周内完成了。做好这些工作,429 报错就会成为运维手册(Runbook)中的一行记录,而不是你发布当天的惨痛故事。

References:Let's stay in touch and Follow me for more thoughts and updates