bigywater.tech
Flask · 111

API 限流

自己实现限流要注意什么?为什么要用 Lua 脚本?限流的 key 取错了会有什么后果?这篇用自己起的 redis 与多线程,把令牌桶、原子性与伪造请求头的绕过逐个验证。

20 → 10
20 个线程抢 10 个名额:非原子实现与 Lua 脚本放行的个数
8 / 8
无条件信任 X-Forwarded-For 时,8 个伪造请求里放行的个数
Retry-After
被限流时告诉调用方还要等多久的响应头

令牌桶

实测 · 本机执行输出
  连发 7 次(序号,是否放行,需要等待秒数): [(1, True, 0.0), (2, True, 0.0), (3, True, 0.0), (4, True, 0.0), (5, True, 0.0), (6, False, 0.5), (7, False, 0.5)]
  等 1.1 秒(补了约 2.2 个令牌)后连发 3 次:True, True, False(第 3 次需要再等 0.39 秒)

上一篇:Refresh Token;下一篇:CORS 预检请求。

第 76 篇讲过 Flask-Limiter 的用法,这一篇讲自己实现时的细节。

原子性

实测 · 本机执行输出
  先 GET 再 SET(非原子)                 放行 20 个(上限 10)
  Lua 令牌桶(原子,容量 10,不补充)            放行 10 个(上限 10)

redis 是自己在非默认端口起的临时实例,验证完就关掉了。

429 与响应头

实测 · 本机执行输出
  第 1 次 → 200 {'RateLimit-Limit': '3', 'RateLimit-Remaining': '2'}
  第 2 次 → 200 {'RateLimit-Limit': '3', 'RateLimit-Remaining': '1'}
  第 3 次 → 200 {'RateLimit-Limit': '3', 'RateLimit-Remaining': '0'}
  第 4 次 → 429 {'RateLimit-Limit': '3', 'RateLimit-Remaining': '0', 'Retry-After': '1'}
  第 5 次 → 429 {'RateLimit-Limit': '3', 'RateLimit-Remaining': '0', 'Retry-After': '1'}

限流的 key

实测 · 本机执行输出
  不信任代理头(用 remote_addr)                  8 个请求(每个伪造不同的 X-Forwarded-For)→ [200, 200, 200, 429, 429, 429, 429, 429]
  ProxyFix(x_for=1) 信任 X-Forwarded-For   8 个请求(每个伪造不同的 X-Forwarded-For)→ [200, 200, 200, 200, 200, 200, 200, 200]

用户与 IP

实测 · 本机执行输出
  按 IP(未登录,3 个用户共用 1 个 IP)           9 个请求(三人轮流)→ 3 个放行,6 个 429
  按用户                                9 个请求(三人轮流)→ 9 个放行,0 个 429

分步演示

API 限流
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.7 / Flask 3.1.3 / Werkzeug 3.1.9 / Jinja2 3.1.6 / SQLAlchemy 2.1.1 / gunicorn 26.2.0(都装在自己的虚拟环境里);大多数实验用 Flask 的 test_client(直接调用 WSGI 应用,不经过网络),需要真实 HTTP 时另起 gunicorn 或开发服务器(端口 9600 以上,本机 127.0.0.1);计时用 time.perf_counter,数字每次运行会有小幅抖动。redis-py 8.1.0、自己起的 redis(非默认端口)
    • 没有验证:滑动窗口日志与滑动窗口计数的实现、redis 故障时限流器的降级策略(放行还是拒绝)、分布式多实例下的时钟偏差、真实反向代理链下 ProxyFix 的层数配置(见第 135 篇)、限流对正常用户体验的影响
    京ICP备2026067502号-1京公网安备11010502063487号