事故的开端和大多数此类事件如出一辙:周二下午两点,一个面向客户的功能在生产环境中不断抛出 429 错误。没有新代码部署,没有流量激增,该功能自身的日志中也没有任何解释。值班工程师盯着仪表盘看了二十分钟,直到另一个频道的某人随口提到,他们刚刚启动了一个“快速回填(backfill)”任务,要对几十万份旧文档重新生成摘要。两个团队,两套代码库,两组值班轮换——而它们之间共用一个 API Key 的速率限制。回填任务吃光了配额,聊天机器人则被“饿死”了。
这就是“吵闹的邻居”(noisy neighbor)问题。而让模型 API 变得如此危险的转折点在于,这个“邻居”并不是共享云硬件上的某个匿名租户,而是你自己公司里的另一个团队。你们之间的那道墙比任何人想象的都要薄。
究其原因是结构性的。模型供应商的速率限制——无论是每分钟 Token 数(TPM)还是每分钟请求数(RPM)——几乎总是针对组织(organization) ,而非发起调用的具体应用、团队或任务。OpenAI 的官方指南明确指出,在同一个组织下添加更多 API Key 并不会增加你的有效配额;限制是账号本身的属性。这一事实重塑了下游的一切。你针对该账号运行的每一个服务都在共用同一个“水龙头”,而这个水龙头的流速是固定的。当一个服务大口吞咽时,其他服务只能捡剩下的;而在糟糕的日子里,剩下的可能什么都没有。
针对组织的限制使每个 Agent 成为命运共同体资源
在传统的多租户基础设施中,吵闹的邻居是个陌生人。你和他们因为调度程序的偶然安排被分配到同一台物理主机上,其中一人运行了一个失控的查询或 CPU 密集型循环,而虚拟机监控程序(hypervisor)允许这种压力跨越了边界。工业界花了十年时间构建隔离原语——cgroups、ResourceQuotas、针对每个租户的语句超时——正是为了确保一个租户的贪婪不会降低另一个租户的响应速度。
模型 API 在更高一层悄然引入了这个问题,而且是在你自己组织的边界内 。没有调度程序把你放在陌生人旁边。你是故意把自己放在那里的——通过将聊天机器人和深夜批处理任务指向同一个账号。Token 预算是一个共享的、固定的池子,且消费是同质化的:花在总结 2019 年支持工单上的一个 Token,就是一个现在无法用于回答付费客户问题的 Token。供应商根本不知道哪个请求更重要。它只看到针对一个组织的流量,当该组织超过上限时,它就会开始拒绝请求——不分青红皂白。
这种同质化(fungibility)正是陷阱所在。计算隔离给了你一个保底:即使你的容器着火了,我的容器也能保证分到属于自己的那一份。而共享的 TPM 预算根本没有保底。它只是一个数字,谁先抢到谁就花掉。交互式流量(人类正在等待的内容)相比于完全可以放在凌晨三点运行的后台任务,并没有天生的优先级。API 将用户被卡住的光标和回填任务的第一千次重试视为对同一稀缺资源的同等索取。
单个并发设置的爆炸半径
这种故障模式的恐怖之处在于触发变化的微小。没人会故意对自己的生产环境进行 DoS 攻击。相反,情况往往是有人在批处理工人中将并发旋钮从 4 调到了 40,因为批处理运行太慢,而 40 在他们的笔记本电脑上看起来运行良好。那一个整数就是爆炸半径。
做一下没人预先计算过的算术。假设你组织的上限是每分钟几十万个 Token——对于以人类节奏每请求消耗几千个 Token 的交互式产品来说,这很充裕。现在,一个批处理任务启动了 40 个并行工人,每个工人都在以套接字允许的最快速度发送包含数千个 Token 的提示词(prompt)。如果没有速度控制(pacing),40 个工人在几秒钟内就能填满六位数的 TPM 预算。批处理不需要恶意,甚至不需要规模很大。它只需要是无节奏的(unpaced) ,而大多数作业代码的默认姿态正是如此:循环处理任务列表,调用 API,能跑多快跑多快。
从交互式产品的角度来看,这与停机无异。它自身的错误率激增,延迟上升,因为自身的重试请求在排队,且日志中显示了没有本地原因的 429 错误。负责该产品的团队会耗费一个小时来排除自己的部署、依赖项、所在区域,然后才有人想到去问问账号上还在跑别的 什么东西。事故的根源存在于响应人员甚至没有克隆(check out)的代码仓库中。这种跨团队的不可见性正是此类事故持续时间长的原因:信号和原因归属于不同的人,而他们并不在同一个房间里。
为什么“仅仅增加退避”救不了你
每当这时,总有人会说些安慰人心的话:我们有带指数退避(exponential backoff)的重试机制,客户端会平滑处理。事实并非如此,理解其中的原因正是系统能够恢复与持续振荡的区别。
指数退避能完美处理单一客户端 的情况。一个调用者达到限制,等待 1 秒,然后 2 秒,然后 4 秒,不断增加的间隔给资源留出了恢复空间。但批处理任务不是一个调用者。它是一群同时启动、几乎同时触达上限的工人,而且——这是致命伤——它们都根据相同的 失败计算出了相同的 退避延迟。它们都睡眠两秒。它们都一起醒来。它们都一起检查大门,看到门开了,然后一起开火,完美地重现了最初的流量激增,并为之扣动了起跑发令枪。这就是“惊群效应”(thundering herd),而天真的指数退避并不能阻止它。它反而让惊群同步化 了。
解决办法是抖动(jitter) ——在每次退避延迟中加入随机变量,使集群将重试分散在一个窗口内,而不是堆叠在同一个时刻。与其让所有人都在 2.0 秒准时醒来,不如让一个在 1.6 秒醒来,另一个在 2.4 秒,另一个在 3.1 秒,这样恢复中的预算看到的是一个平缓的坡度而不是一堵墙。全抖动(Full jitter) ,即每个延迟都是零到当前退避上限之间的一个随机值,是真正能平息惊群效应的版本。从业者通过惨痛教训学到的反直觉经验是:增加随机性会使分布式系统更稳定,而不是更不稳定 。没有抖动的重试策略不是安全机制,而是你集群冲锋的节拍器。
即使有抖动,单机退避也有第二个缺陷:每个工人只知道自己的 429 错误。工人 A 退避时,工人 B 到 Z 仍在狂轰滥炸,稍后捕捉到自己的 429,并启动各自独立的计时器。资源池在意外平息前会波动好几分钟。真正有效的是共享退避状态 ——一旦任何工人看到 429,它就向共享存储(Redis、协调服务等)写入一个暂停标志,然后整个资源池 集体退避,上限大约设为一个预算恢复窗口。一个工人的痛苦变成了整个资源池的信号。这正是天真的重试逻辑所缺失的协调层。
真正有效的隔离模式 这并不意味着共享账号是行不通的。它意味着你必须在应用层重新构建服务商未提供的隔离机制。以下四种模式承担了大部分工作。
在预算被强行拆分之前,先主动拆分它。 最有效的举措是停止幻想“一个资源池就是一个资源池”。Anthropic 的平台现在允许你将一个组织划分为多个工作区(workspaces),每个工作区在组织总上限之下都有自己可配置的每分钟请求数(RPM)和每分钟 Token 数(TPM)限制——而且你可以通过编程方式查询这些限制来辅助逻辑判断。如果服务商原生提供了这种功能,请务必使用它:给交互式产品分配独立的工作区和子配额,给批处理集群分配另一个,这样一个失控的任务现在只能让它自己 陷入饥饿。如果服务商不提供此功能,请在每个服务都会调用的网关或代理中自行实施子配额管理。这与 Kubernetes 在每个命名空间(namespace)使用 ResourceQuotas 的原理相同:一个无法超出其配额的租户不会拖垮它的邻居。
将交互式任务和批处理任务分流运行。 即使在共享预算内,你也可以表达优先级。将流量分类为交互式(人类用户正在等待)或批处理(截止日期在几小时后),并让交互式车道拥有抢占权。批处理工作应该是系统在空闲时推进、在实时需求攀升时立即推迟的任务——它在设计上就应该是后台化的,最先被限流,最后被恢复。一个主动让位的批处理任务,比一个速度快两倍但偶尔会搞垮产品的批处理任务更有价值。
在服务商限流之前,先在自己侧管控速率。 在 API 之前放置一个客户端限流器——一个根据你实际配额大小设置的令牌桶(token bucket),这样你自己的代码就会在本地拒绝超过预算。令牌桶以稳定的速率填充,允许受控的突发流量,并在桶空时阻塞调用者,从而将混乱的 429 报错重试循环转变为有序的本地排队。在流量离开你的网络之前对其进行整形,绝对优于让服务商用拒绝访问的方式帮你整形,因为你 可以决定谁需要等待。
在供应商断开连接之前,先触发你自己的熔断器。 熔断器监控失败率,一旦超过阈值就会“开启”——短路后续调用以进入冷却期,而不是将更多请求堆积到已经拒绝服务的资源上。通过设置在接近预算饱和前的一刻开启,它能将缓慢的、自发的局部瘫痪转化为快速的、受控的本地失败,并能干净利落地恢复。熔断器的全部职责就是停止那种“失败产生重试,重试产生更多失败”的末日循环。
将共享账号视为生产基础设施 阻碍这一切的心理转变虽小但很难实现:共享的模型账号不是你在团队间瓜分的便利工具,它是一个具有严格容量限制的生产基础设施,值得像共享数据库或共享消息总线一样受到严格约束。你不会允许在高峰时段针对主事务数据库运行无限制的分析数据回填。模型账号正是那个“主数据库”——你只是看不见连接池被填满的过程,所以在 429 错误降临之前,它感觉 不像是一个瓶颈。
具体来说:给每个工作负载分配独立的密钥或工作区以便追溯用量;划分子配额以防单一任务消耗整个资源池;按优先级标记流量以便交互式任务始终优于批处理;在客户端路径中加入控制器和熔断器,使你的代码在本地优雅地失败,而不是在远程不加区别地崩溃;并且——务必——加入抖动(jitter),确保你的恢复过程不会再次制造停机。做好这五件事,嘈杂的邻居就会安静下来,因为你终于补齐了服务商留下的“隔音墙”。
最令人不安的部分是承认谁才是那个一直以来的“邻居”。它不是共享硬件上的陌生人。它是你自己的批处理任务,是你自己的并发设置,是你自己缺失的抖动。这里的坏消息也就是好消息:既然那个嘈杂的邻居是你自己,那么你也就是那个能让它停下来的人。
会员专享
余下内容仅对会员开放。 会员可读完整内容 —— 每一个公开发布的观点背后,那些框架、决策与推理的全貌。
— 完整文章,包含未公开存档的部分 — 可落地的工作框架,附带权衡与决策依据 — 新文章抢先看,先于公开发布 登录以继续阅读→ 随时取消 · 一次订阅,畅读全部