我们一直在限制自己的服务
Mythex 的 API 允许每个网络地址每分钟 300 次请求。这一限制的目的是保护我们,防止某个人或脚本大量调用 API。而在生产环境中,它开始拒绝我们自己的服务——即为已发布应用提供服务的边缘部分,以及运行 AI 智能体的服务——因为它们各自只从少数几个地址访问 API。现在,携带我们内部凭据的请求可以豁免,其他所有请求依然按原有方式受限。
症状
API 开始对我们自己的部分内部调用返回 429 Too Many Requests。被击中的有三类调用,而每一类都关系到使用 Mythex 的人:
- 查询某个自定义域名属于哪个应用。 访客在自己的域名上打开已发布的应用时,我们的边缘会向 API 询问该网站位于何处。这里出现 429,意味着访客无法访问网站。
- 保存正在运行的智能体轮次。 智能体工作时,其进度会随时保存,因此回复在刷新页面或连接中断后仍然保留。这里出现 429,意味着进度没有被保存。
- 检查预览的访问权限。 你正在构建的应用的预览是受保护的,而保护机制会向 API 发起查询。这里出现 429,意味着预览无法打开。
这些都不是有人在调用 API。全部都是我们自己。
为什么会这样
按地址限制的速率限制器有一个前提假设:一个地址大致对应一个调用者。对于个人来说,这基本成立。而对于我们自己的基础设施,这一假设严重不符。
- 为已发布应用和预览提供服务的边缘服务,从少数几个共享的出站地址访问 API。每一位访问任何已发布应用的访客,以及每一次预览检查,看起来都像来自这几个地址之一。
- 编排器——即运行智能体轮次的服务——从一个容器地址访问 API。每个用户的每一个正在运行的轮次都共享这个地址。
因此,本该属于一个人的请求额度,被整个服务的全部流量所瓜分。这些服务合起来把它用光了,而限速器只是按规则办事:拒绝了它们。
修复
我们自己的服务本身就能证明身份。每一个内部调用都携带一个内部令牌,而 API 内部区域下的每个路由在做任何事情之前都会检查该令牌。
现在,速率限制器使用同样的检查。与内部令牌匹配的请求会被放入允许列表,不计入按地址的额度。其他所有请求的计数方式与之前完全相同:每个地址每分钟 300 次请求。
有两个细节使它不会变成一个漏洞:
- 错误的令牌不会获得豁免。 允许列表只匹配真正的内部令牌。携带伪造令牌的请求会像其他请求一样被计数和限流,而内部路由仍然会以
401 Unauthorized拒绝它。 - 限制数值本身没有改变。 我们没有为了腾出空间而提高这个数字。提高它会暂时掩盖问题,直到流量再次增长,而且会削弱限制本应保护的那种情况下的防护能力。
我们把限速器的设置移到了一个小函数中,并为它编写了测试。其中一个测试以允许 5 次的限速器发送 20 个携带内部令牌的请求,并期望全部 20 个都成功。另一个测试从同一地址发送普通请求,期望前五个成功,之后的请求被拒绝。第三个测试发送携带伪造令牌的请求,期望它们以同样的方式被拒绝。
我们的收获
- 限制本身带有假设。 「一个地址对应一个调用者」内置于按地址的限制之中。一旦你自己的服务位于共享地址之后——而很多服务确实如此:负载均衡器、边缘网络、容器、办公网络——这个假设就不再成立。
- 通过身份而不是地址区分自己的流量。 地址只是流量到达的方式,而不是发送者是谁。我们早已有一个能证明调用来自我们自己的凭据,只是限速器没有去看它。
- 不要通过提高数字来解决。 更大的限额可能会让 429 暂时消失,但防护能力会降低,之后还会再次失败。
- 留意谁被限流了。 落在你自己服务上的 429,是关于你的限制的一份错误报告,而不是限速器正在正常工作的迹象。
这对构建在 Mythex 上的人意味着什么
你不需要做任何改动。已发布在自定义域名上的应用、智能体轮次和预览,不再依赖于 Mythex 其余部分的繁忙程度。你的应用自身的流量是你应用自己的事:这里所说的限制针对的是我们的 API,而不是你发布的应用。
如果你运行自己的后端,同样的陷阱也适用于你添加的任何速率限制。我们关于什么是 API和 AI 构建应用的安全检查清单的指南是很好的入门材料。另外,关于我们自己的基础设施妨碍自身的另一个例子,请参阅我们如何将部署的 502 错误降到零。