lua-resty-limit-traffic的漏桶非标准,实为“固定窗口+请求排队”混合模型:它用shared dict计数、支持burst队列缓冲,但无恒速漏出机制,依赖ngx.sleep(delay)模拟延迟,导致高并发时响应延迟陡增。

为什么 lua-resty-limit-traffic 的漏桶不是标准漏桶?
它实际实现的是「固定窗口 + 请求排队」的混合模型,不是教科书里那个匀速滴水的漏桶。核心在于 resty.limit.traffic.new() 创建的实例底层用共享字典(shared dict)计数,每次 limit:incoming() 调用会检查当前请求数是否超过阈值,若未超则立即放行;若超,则根据 burst 和 rate 判断能否入队等待——这个“等待”是内存中排队,不是真正的匀速释放。
这意味着:高并发瞬间涌入时,只要没超 burst,请求全被塞进队列,等前面的处理完才逐个返回,容易造成响应延迟陡增,而非平滑限流。
-
rate = 10表示每秒允许 10 个新请求开始处理(非完成) -
burst = 20表示最多积压 20 个待处理请求 - 没有「水以恒定速率漏出」的物理模拟,只有「令牌发放节奏 + 队列缓冲」
如何用 limit.req 实现按 IP 动态限流?
关键在 key 构造和共享字典生命周期。不能直接写死 "ip:" .. ngx.var.remote_addr,因为 IPv6 地址带冒号会导致 shared dict key 合法性问题;也不能忽略代理场景,remote_addr 可能是上一跳 Nginx 的地址。
实操建议:
- 用
ngx.var.http_x_forwarded_for提取真实客户端 IP,取第一个非127.0.0.1或::1的值,并做 IPv6 标准化(如用string.gsub(ip, ":", "_")替换冒号) - 共享字典必须在
nginx.conf的http块预定义:lua_shared_dict limit_traffic_store 10m;
- 在
location中调用时,确保access_by_lua_block在proxy_pass前执行,否则限流失效 - 错误返回要显式设状态码:
ngx.exit(503),否则默认 200 会误导上游
limit:incoming() 返回值怎么判断才不踩坑?
它返回三个值:err、delay、remaining。常见误判是只看 err == nil 就放行,但其实 delay > 0 表示该请求被排队,需确认是否接受延迟。
典型错误现象:接口平均耗时突增 200ms+,监控显示 QPS 正常但 P95 延迟飙升——大概率是大量请求在 limit:incoming() 后没检查 delay 就直接进后端,把排队压力转嫁给了业务服务。
- 正确做法:当
delay > 0且你不想让客户端等,应主动ngx.exit(429) -
remaining是剩余配额(非剩余队列长度),单位是「可立即处理的请求数」 -
err为"rejected"表示已超burst,直接拒绝;为nil仅表示未被硬拒绝,不等于“立刻执行”
动态调整 rate / burst 为什么 reload nginx 不生效?
因为 resty.limit.traffic.new() 初始化时就把 rate 和 burst 绑定到对象实例里了,reload 只重建 Lua VM 上下文,不会自动更新已有 limit 对象的参数。想热更新,必须把 rate/burst 存到外部(如 Redis 或 shared dict),每次调用 limit:incoming() 前重新 new 一个实例——但这会带来性能损耗和 key 冲突风险。
更稳妥的做法:
- 把不同档位的限流策略做成多个预定义 limit 实例(如
limit_low、limit_high),通过变量或 header 决定走哪个 - 用
lua_shared_dict存储每个 key 的个性化 rate(如按 user_id 查表),但 new 实例时仍需传入该值,无法全局覆盖 - 真正需要频繁变更的场景,建议改用 OpenResty 的
balancer_by_lua*+ 外部控制面,而不是在 access 阶段硬扛
动态漏桶听起来灵活,但实际落地时,shared dict 的 key 设计、IP 提取可靠性、delay 处理逻辑、以及参数热更路径,四者缺一不可——少盯住一个,线上就可能出长尾延迟或误放行。











