如何用 Nginx lua_shared_dict 实现跨进程的高性能限流同步?

2026-04-24 20:472阅读0评论SEO问题
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计1106个文字,预计阅读时间需要5分钟。

如何用 Nginx lua_shared_dict 实现跨进程的高性能限流同步?

直接说结论:

为什么 lua_shared_dict 适合限流而不是 Redis

因为它是 Nginx worker 进程直接 mmap 映射的同一块内存,没有序列化、没有网络 IO、没有上下文切换。一次 incr 调用平均耗时在 100ns 级别,而 Redis 单次 INCR 至少是毫秒级——尤其在每秒上万请求的网关场景,差两个数量级。

但它不是万能的:数据随 Nginx reload 清空;不支持复杂查询;value 不能存函数或循环 table;key 长度不能超过 255 字节。

  • 适合场景:IP/Token/URI 粒度的计数、令牌桶、滑动窗口(需自己维护时间戳)
  • 不适合场景:需要持久化、跨机器同步、模糊匹配或聚合统计

lua_shared_dict 必须放在 http 块里,且不能重复定义

这是最常踩的坑:有人把 lua_shared_dict rate_limit 5m; 写在 serverlocation 块里,Nginx 启动直接报错 nginx: [emerg] "lua_shared_dict" directive is not allowed here

另一个隐性问题:同名字典只能定义一次。比如你在多个 include 文件里都写了 lua_shared_dict my_dict 2m;,启动时会失败,错误信息是 duplicate "lua_shared_dict" directive

  • 正确定义位置:http { lua_shared_dict rate_limit 10m; }
  • 名称只允许字母、数字、下划线,大小写敏感(RateLimitratelimit 是两个字典)
  • 大小单位必须是 km,写成 10mb10M 会启动失败

限流逻辑必须用 incr,别手写 get + set

看似简单的两步操作,在多 worker 下就是竞态漏洞。比如:

local dict = ngx.shared.rate_limit local val = dict:get("ip:1.2.3.4") if not val or val < 100 then dict:set("ip:1.2.3.4", (val or 0) + 1, 60) end

当两个请求同时读到 val == 99,都会执行 set(..., 100),结果变成 100,但实际已处理 101 次请求。

正确做法是用原子 incr

local dict = ngx.shared.rate_limit local key = "ip:" .. ngx.var.binary_remote_addr local newval, err = dict:incr(key, 1, 60) -- 第三次参数是初始值+过期时间 if err == "not found" then newval = dict:incr(key, 1, 60) -- 确保初始化 end if newval > 100 then return ngx.exit(429) end

  • incr(key, delta, exptime) 是真正原子的:不存在则设初值并设 TTL,存在则加 delta
  • 返回 nil + "no memory" 表示共享内存满,此时应降级(如记录日志、放行或 fallback 到本地计数)
  • 避免对同一 key 在单次请求中多次 incr,会导致精度漂移(底层是 CAS,但 Lua 层无法保证中间状态)

内存预估不准,会导致限流失效或 OOM

10MB 看似很多,但按默认 slab 分配粒度(最小 128 字节),如果 key 平均长度 64 字节、value 是 number(8 字节),加上节点元数据,一个 key 实际占约 200 字节。10MB 最多存 5 万个 key —— 如果你按秒级窗口统计 10 万个 IP,就必然触发 LRU 驱逐,旧 key 被清掉,限流变“漏斗”。

更危险的是存大 value:比如把整个 JSON 响应体(5KB)塞进 shared dict,几个 key 就能把 slab 撑爆,后续所有 set 都返回 "no memory"

  • 估算公式:字典大小 ≈ (平均 key 长度 + 平均 value 大小 + 64) × 预估最大 key 数量,再加 20% 余量
  • key 过长?用 ngx.md5_bin() 哈希压缩,比如 md5("user:long_token_xxx") 固定 16 字节
  • value 超过 1KB?别硬塞,改存摘要或 ID,查库补全

真正难的不是写对那几行 Lua,而是想清楚你的 key 空间规模、value 生命周期、以及 reload 后如何平滑过渡——这些决定了限流到底靠不靠谱。

标签:Nginxred

本文共计1106个文字,预计阅读时间需要5分钟。

如何用 Nginx lua_shared_dict 实现跨进程的高性能限流同步?

直接说结论:

为什么 lua_shared_dict 适合限流而不是 Redis

因为它是 Nginx worker 进程直接 mmap 映射的同一块内存,没有序列化、没有网络 IO、没有上下文切换。一次 incr 调用平均耗时在 100ns 级别,而 Redis 单次 INCR 至少是毫秒级——尤其在每秒上万请求的网关场景,差两个数量级。

但它不是万能的:数据随 Nginx reload 清空;不支持复杂查询;value 不能存函数或循环 table;key 长度不能超过 255 字节。

  • 适合场景:IP/Token/URI 粒度的计数、令牌桶、滑动窗口(需自己维护时间戳)
  • 不适合场景:需要持久化、跨机器同步、模糊匹配或聚合统计

lua_shared_dict 必须放在 http 块里,且不能重复定义

这是最常踩的坑:有人把 lua_shared_dict rate_limit 5m; 写在 serverlocation 块里,Nginx 启动直接报错 nginx: [emerg] "lua_shared_dict" directive is not allowed here

另一个隐性问题:同名字典只能定义一次。比如你在多个 include 文件里都写了 lua_shared_dict my_dict 2m;,启动时会失败,错误信息是 duplicate "lua_shared_dict" directive

  • 正确定义位置:http { lua_shared_dict rate_limit 10m; }
  • 名称只允许字母、数字、下划线,大小写敏感(RateLimitratelimit 是两个字典)
  • 大小单位必须是 km,写成 10mb10M 会启动失败

限流逻辑必须用 incr,别手写 get + set

看似简单的两步操作,在多 worker 下就是竞态漏洞。比如:

local dict = ngx.shared.rate_limit local val = dict:get("ip:1.2.3.4") if not val or val < 100 then dict:set("ip:1.2.3.4", (val or 0) + 1, 60) end

当两个请求同时读到 val == 99,都会执行 set(..., 100),结果变成 100,但实际已处理 101 次请求。

正确做法是用原子 incr

local dict = ngx.shared.rate_limit local key = "ip:" .. ngx.var.binary_remote_addr local newval, err = dict:incr(key, 1, 60) -- 第三次参数是初始值+过期时间 if err == "not found" then newval = dict:incr(key, 1, 60) -- 确保初始化 end if newval > 100 then return ngx.exit(429) end

  • incr(key, delta, exptime) 是真正原子的:不存在则设初值并设 TTL,存在则加 delta
  • 返回 nil + "no memory" 表示共享内存满,此时应降级(如记录日志、放行或 fallback 到本地计数)
  • 避免对同一 key 在单次请求中多次 incr,会导致精度漂移(底层是 CAS,但 Lua 层无法保证中间状态)

内存预估不准,会导致限流失效或 OOM

10MB 看似很多,但按默认 slab 分配粒度(最小 128 字节),如果 key 平均长度 64 字节、value 是 number(8 字节),加上节点元数据,一个 key 实际占约 200 字节。10MB 最多存 5 万个 key —— 如果你按秒级窗口统计 10 万个 IP,就必然触发 LRU 驱逐,旧 key 被清掉,限流变“漏斗”。

更危险的是存大 value:比如把整个 JSON 响应体(5KB)塞进 shared dict,几个 key 就能把 slab 撑爆,后续所有 set 都返回 "no memory"

  • 估算公式:字典大小 ≈ (平均 key 长度 + 平均 value 大小 + 64) × 预估最大 key 数量,再加 20% 余量
  • key 过长?用 ngx.md5_bin() 哈希压缩,比如 md5("user:long_token_xxx") 固定 16 字节
  • value 超过 1KB?别硬塞,改存摘要或 ID,查库补全

真正难的不是写对那几行 Lua,而是想清楚你的 key 空间规模、value 生命周期、以及 reload 后如何平滑过渡——这些决定了限流到底靠不靠谱。

标签:Nginxred