单个limit_req_zone无法支撑千万级独立IP限流,因每IP需约64字节,1000万IP需近640MB内存;10MB zone仅存约16万个IP状态,超限后新IP限流失效。

单靠一个 limit_req_zone 无法在常规内存下支撑千万级独立 IP 的长期限流——每个 IP 状态槽实占约 64 字节,1000 万 IP 就要近 640 MB 共享内存,且这还不算哈希冲突和文本 key 的额外开销。
为什么 10MB zone 只能存约 16 万个活跃 IP
Nginx 为每个活跃 key(如 $binary_remote_addr)分配固定大小状态槽:含时间戳、计数器、锁、哈希桶头等元数据。实测与社区共识一致——1 MB 共享内存 ≈ 存储 1.6 万个 key 状态。这意味着:
- 10 MB
zone最多容纳约 16 万个独立 IP 状态 - 若每秒有 10 万不同 IP 各发 1 请求,1 秒内就可能填满该区域
- 超出后 Nginx 会淘汰 60 秒无访问的旧记录;若仍不足,新 IP 状态写入失败,限流逻辑对这部分请求失效(表现为“没被限”)
-
$remote_addr(文本形式)比$binary_remote_addr多占 12–15 字节/IP,整体内存再增 15%~20%
别硬扛 IP,改用聚合 key 压缩维度
直接用 $binary_remote_addr 记千万 IP 是最笨的办法。真实业务中,99% 的流量不需要精确到单 IP 级别防护。
- 对 CDN 回源流量,用
$http_x_real_ip+$server_name组合,把“同一源站 + 同一客户”视为一个逻辑单元 - 对未登录用户,用
map提取$remote_addr前两段(如192.168),1 个 key 覆盖整个 C 段 - 对已登录用户,key 改为
$http_x_user_id,真实用户量远小于 IP 总量 - 结合
$geoip2_country_code或自定义地区 map,用国家/省份作为 key,10 MB 即可覆盖全球 200+ 区域
拆分 zone:让内存各司其职,不堆在一个桶里
多个 limit_req_zone 可共存,按风险等级和行为特征分层分配内存,总用量可控,实际防护能力反而更强:
-
zone=burst_api:5m rate=100r/s:突发接口专用,只存最近活跃 IP,自动淘汰冷 key -
zone=login_basic:2m rate=3r/m:登录页限流,key 为$binary_remote_addr,但速率极低,长期存活 IP 极少 -
zone=user_action:8m rate=20r/s:关键用户操作,key 为$http_x_user_id,数量稳定可控 - 三者合计仅 15 MB,却覆盖了来源、身份、行为三层,比单个 100 MB 的 per-IP zone 更有效
必须补上外部兜底,Nginx 本身不是数据库
哪怕做了 key 聚合和 zone 分层,纯 Nginx 内存方案仍有硬上限。生产环境必须配合外部手段:
- 在 LVS 或云 WAF 层做第一道 IP 频控(基于连接数或包速率),过滤掉明显攻击流量
- 对高频异常 IP,由后端服务写入 Redis 黑名单,Nginx 通过
lua-resty-redis实时查表,命中则跳过限流逻辑 - 开启
limit_req_log_level warn和limit_req_status 429,从 access log 的$limit变量中识别高频但未被限的 IP 段,反向优化map白名单
真正难的不是算出 64 字节,而是判断哪些 IP 值得记、哪些请求根本不该进 Nginx 的限流路径——这需要结合业务日志、用户身份、请求路径和上游代理信息综合决策。


















