跳到主要内容

嘈杂邻居竟是你:当失控的 Agent 让共享账户中的其他人全都触发 429 错误

· 阅读需 12 分钟
Tian Pan
Software Engineer

事故的开端和大多数此类事件如出一辙:周二下午两点,一个面向客户的功能在生产环境中不断抛出 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)的代码仓库中。这种跨团队的不可见性正是此类事故持续时间长的原因:信号和原因归属于不同的人,而他们并不在同一个房间里。

为什么“仅仅增加退避”救不了你

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