← 返回文章列表

限流键取错:一个被"网络太慢"掩盖的全局额度桶

2026-10-10 · 后端 · 排障

限流日志里出现这样一行:

{"event": "ratelimit 60 per 1 minute (10.89.3.1) exceeded at endpoint: /"}

10.89.3.1 是 podman 网桥网关。配置里写的是"每个客户端 IP 每分钟 60 次",但实际上全站所有用户共用这一个桶。

根因:客户端 IP 一直在变,变的不是它

应用用的是 slowapi 的 get_remote_address,它读的是 TCP 对端地址。请求路径是:

浏览器 → nginx(宿主机) → 127.0.0.1:8000 → 容器内 uvicorn

最后一段是 podman 的端口转发,容器看到的对端永远是网关地址,不管来的是哪个用户。nginx 其实一直在传 X-Forwarded-For,但 uvicorn 默认只信任 127.0.0.1 这个来源,这个头被直接丢弃了。

为什么这么久都没人报障

因为链路慢。请求零星到达,每个用户的请求被摊薄在时间里,60 次/分钟的全局桶根本用不完。反过来,当 CDN 换成灰云、延迟从 15 秒降到 80 毫秒之后,SPA 单页并发十几个 API 请求,桶瞬间见底——

于是每个正常用户打开页面,先被自己的并发请求打到 429。

这类 bug 特别隐蔽:它以"性能优化之后开始报错"的形式出现,很容易被误判成优化本身的问题。

修法

让 uvicorn 采信上游代理的 X-Forwarded-For。它有现成的环境变量:

FORWARDED_ALLOW_IPS=10.89.0.0/16

这个变量要写在 compose 文件里而不是只写进 env 文件——因为很多部署脚本在升级时会重新生成 env 文件,只写 env 的话下次升级就丢了。

顺带修掉一个伪造入口

nginx 默认的 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 是追加语义:客户端自带一个 X-Forwarded-For: 9.9.9.9,代理再把真实地址追加到后面,拼接结果的第一段仍然是假的。而 uvicorn 取的正是第一段——等于把限流键交给了客户端决定。

正确做法是覆盖写:

proxy_set_header X-Forwarded-For $remote_addr;

如果前面还有 CDN,再用 nginx 的 real_ip 模块限定"只有来自 CDN 网段的连接才采信 CF-Connecting-IP",把真实来源还原出来,同时挡住客户端伪造。

验证方式

改完之后别只看日志,查一下审计表里的 client_ip:

场景期望结果
正常访问记录真实客户端 IP
请求头带伪造 X-Forwarded-For仍记录真实 IP
请求头带伪造 CF-Connecting-IP仍记录真实 IP

第二、三行比第一行更重要:第一行只能证明"改对了",后两行才能证明"改对了而且没留后门"。