Nginx基于IP限流必须用limit_req_zone + limit_req组合,键必须是$binary_remote_addr;因其为固定4/16字节二进制表示,哈希稳定、内存占用小,而$remote_addr为变长字符串,易致键错乱、限流失效。

直接说结论:Nginx基于IP限流必须用 limit_req_zone + limit_req 组合,键必须是 $binary_remote_addr,否则内存浪费、命中率低,甚至限不住真实IP。
为什么 $binary_remote_addr 不能换成 $remote_addr
因为 $remote_addr 是字符串形式,IPv4地址如 192.168.1.100 占7–15字节,且带点号和ASCII字符;而 $binary_remote_addr 是固定4字节(IPv4)或16字节(IPv6)的二进制表示,哈希查找快、内存占用小。实测中用 $remote_addr 定义 zone=ip:10m,实际只能存约2万IP;换成 $binary_remote_addr 后能存超200万IP。
常见错误现象:
配置写了 limit_req_zone $remote_addr zone=ip:10m rate=5r/s,但压测时发现同一IP发10个请求全放行——本质是每次解析出的字符串不一致(比如带空格、换行符),导致哈希键错乱。
burst 和 nodelay 怎么配才不卡用户也不放水
这是最常翻车的环节。burst 不是“允许超速”,而是“最多缓存几个等令牌”,nodelay 决定这些缓存请求要不要排队。
- 不加
nodelay:第6个请求(rate=5r/s, burst=5)会进入队列,每200ms放一个,第5个排队请求要等1秒才处理——用户明显感知卡顿,还可能触发客户端超时 - 加
nodelay:前5个超速请求立刻处理(只要burst有空位),第6个开始直接返回503 Service Temporarily Unavailable,响应快、边界清晰 - 典型安全配法:
limit_req zone=ip burst=10 nodelay,配合limit_req_status 429返回标准限流码,前端可据此做退避重试
注意:burst 值不是越大越好。burst=100 意味着突发流量下,Nginx要为每个IP维护100个等待上下文,内存和调度开销陡增;生产环境建议 burst ≤ 20。
怎么让白名单IP完全绕过限流
不能靠 if 或顺序 allow/deny 跳过 limit_req——Nginx 的限流模块在 rewrite 阶段之后、access 阶段之前执行,if 判断根本拦不住它。
正确做法是用 map 构造动态键:
http {
map $remote_addr $limit_key {
default $binary_remote_addr;
192.168.10.5 "";
2001:db8::1 "";
}
limit_req_zone $limit_key zone=ip:10m rate=5r/s;
}
原理:map 把白名单IP映射为空字符串,而 limit_req_zone 遇到空键直接跳过计数。这样既不改 location 结构,又不影响其他IP的限流逻辑。
容易踩的坑:
— map 必须定义在 http 块顶层,不能放在 server 或 location 里
— 白名单写成 192.168.10.0/24 会失效,map 不支持 CIDR,得拆成多行或用 geo 模块替代
— 空字符串键名不能用引号包裹,写成 "" 或 '' 都会报语法错
验证限流是否真生效,别只看 curl
curl http://localhost/api/test 发一次,看不出任何效果——限流是按时间窗口统计的,单次请求永远在桶里有令牌。
必须用并发压测工具模拟真实场景:
- 快速验证 burst:
ab -c 20 -n 20 http://localhost/api/test,观察返回中503的数量是否 ≈20 − burst - 验证速率控制:
wrk -t4 -c10 -d30s http://localhost/api/test,看平均每秒请求数是否稳定在 rate ±10% - 查日志确认:
tail -f /var/log/nginx/access.log | grep " 429 "或" 503 ",注意 Nginx 默认不记录限流拒绝,需显式加limit_req_log_level error;
真正难的不是写对那几行配置,而是想清楚:你限制的是“单位时间请求数”(limit_req),不是“连接数”(limit_conn),也不是“下载速度”(limit_rate)。三者混在一起,limit_conn 先断连,limit_req 根本没机会执行——这种隐性冲突,日志里只显示 connection reset,不会告诉你哪条指令干的。


















