resty.limit.conn不是漏桶,而是带延迟调度的并发控制器:它通过连接数限制与ngx.sleep(delay)实现请求排队释放,非匀速漏水;其rate参数实为单请求预期耗时(秒),burst表示并发缓冲队列长度。

为什么 resty.limit.conn 不是“漏桶”,而是带延迟调度的并发控制器
很多人一看到 resty.limit.conn.new(..., burst, rate) 就默认它是标准漏桶,其实不是。它本质是「连接数限制 + 请求排队延迟调度」的组合:当并发超限时,不直接拒绝,而是把请求塞进一个逻辑队列,按预估处理时长(如 0.5)逐批释放。这和经典漏桶“固定速率匀速漏水”不同——它的“出水”是离散的、依赖 ngx.sleep(delay) 主动挂起的。
常见错误现象:
- 误设
burst > 0但没配ngx.sleep(delay),导致超限请求直接穿透,限流失效 - 把
rate参数当成 QPS,实际它是「单请求预期耗时」(单位:秒),填0.1≈ 每秒最多处理 10 个请求,填2就变成每秒最多处理 0.5 个 - 在
log_by_lua_block中漏掉lim:leaving(),shared dict 计数不减,后续所有同 key 请求全被卡死
resty.limit.req 和 resty.limit.conn 怎么选:QPS 限流 vs 连接数限流
二者底层都用 shared dict,但语义完全不同:
-
resty.limit.req:面向「请求频次」,适合防刷、API 调用配额。例如limit_req.new("my_limit_req_store", 100, 20)表示每秒 100 令牌,桶容量 20,超限返回503或延时 -
resty.limit.conn:面向「并发连接数」,适合保护后端吞吐瓶颈(如数据库连接池、下游 HTTP 服务)。它不关心你调了多少次,只看你此刻有多少连接活着 - 混用风险:对同一 key 同时用两种限流,shared dict key 冲突、计数错乱,
lim:incoming()和lim:leaving()必须成对出现在同一模块中
如何让令牌桶“动态”:绕过 limit_req.new() 的静态初始化陷阱
limit_req.new() 在每次 access_by_lua_block 中执行,会导致令牌桶参数无法运行时变更(比如根据上游负载自动降级 QPS)。正确做法是把初始化提到 init_by_lua_block 或 set_by_lua_block 中缓存实例:
init_by_lua_block {
local limit_req = require "resty.limit.req"
-- 预创建多个策略实例,key 为策略名
_G.limit_strategies = {
normal = limit_req.new("my_limit_store", 100, 30),
degraded = limit_req.new("my_limit_store", 20, 10)
}
}
<p>access_by_lua_block {
local strategy = ngx.var.upstream_status == "503" and "degraded" or "normal"
local lim = _G.limit_strategies[strategy]
local delay, err = lim:incoming(ngx.var.binary_remote_addr, true)
if err == "rejected" then
return ngx.exit(503)
end
if delay >= 0.001 then
ngx.sleep(delay)
end
}注意:_G 是全局变量,OpenResty 多 worker 下需确保线程安全;更稳妥的方式是用 lua_shared_dict 存储策略元数据,运行时按需 new 实例。
共享字典内存爆炸与 key 泄漏的真实原因
lua_shared_dict 不会自动清理过期 key,尤其用 binary_remote_addr 做 key 时,NAT 网关下的大量客户端共用一个 IP,容易撑爆 dict。真实踩坑点:
- 未设置
lua_shared_dict my_limit_store 10m的大小,用默认 1m,几百个 IP 就满 - 用
ngx.var.remote_addr而非ngx.var.binary_remote_addr,前者是字符串,后者是二进制,节省约 50% 内存 - 没做 key 归一化:CDN 回源时
X-Forwarded-For可能含多个 IP,直接取首段易被伪造;应结合realip_remote_addr或可信 header 校验 -
limit_conn:leaving()报错被静默吞掉(如 shared dict 已满),导致计数永久卡住,必须加日志兜底
最隐蔽的问题是:同一个请求路径下,多个 access_by_lua_block 块重复调用 lim:incoming(),但只在一个 log_by_lua_block 里调 leaving(),结果就是计数只增不减。

















