怎么用 Nginx 的 limit_req_zone 按 URL 路径限速防爬虫?
Nginx 的 limit_req_zone 只能定义在 http 块,按 URL 路径限速需为不同路径分别建 zone 并在对应 location 用 limit_req 引用,或用 map 将路径映射进 key;推荐前者,并注意 loc
用 limit_req_zone 按 URL 路径限速,核心做法只有一条:在 http 块里按不同路径定义多个 zone,再在对应 location 里用 limit_req 引用。因为 limit_req_zone 只允许写在 http 上下文,它无法直接绑定某个 URL,路径的区分只能靠 location 匹配,或用 map 把路径映射进 key。
limit_req_zone 为什么不能直接写在 location 里?
结论:limit_req_zone 是 http 块级指令,写在 server 或 location 里会导致 nginx -t 直接报错“directive is not allowed here”。它负责“定义共享内存区和速率”,真正生效的开关是 location 里的 limit_req。
这个设计决定了按路径限速的两种思路:一是给每条路径定义独立 zone(推荐,速率互不干扰,日志也好排查);二是只定义一个 zone,但把 $uri 拼进 key,让每个 URL 各自计数。前者可控性强,后者配置短但内存消耗大,下面分别说。
另外一个容易踩的点:**limit_req 会从 http/server 继承到 location,但只要 location 里自己写了 limit_req,就以自己的为准,不会和父级叠加**。所以不要在 http 块随手写一条全局限速,否则所有没单独配置的 location 都会被它约束。
按路径限速的具体配置怎么写?
结论:一条路径一个 zone,速率按“该路径正常用户能承受的最高频率”倒推。下面是可直接复制的完整配置:
http {
# 普通页面:单 IP 每秒 10 次
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
# 站内搜索:爬虫最爱,单 IP 每秒 1 次
limit_req_zone $binary_remote_addr zone=search:10m rate=1r/s;
# 登录接口:单 IP 每分钟 10 次
limit_req_zone $binary_remote_addr zone=login:10m rate=10r/m;
limit_req_status 429; # 拒绝时返回 429 而非默认 503
limit_req_log_level warn; # 记录到 error_log,便于统计
server {
listen 80;
server_name www.gj0.com;
location /search/ {
limit_req zone=search burst=5 nodelay;
proxy_pass http://127.0.0.1:8080;
}
location = /login {
limit_req zone=login burst=3;
proxy_pass http://127.0.0.1:8080;
}
location / {
limit_req zone=general burst=20 nodelay;
}
}
}
参数要点:$binary_remote_addr 是二进制格式的客户端 IP(IPv4 占 4 字节,IPv6 占 16 字节),比字符串形式的 $remote_addr 省内存;zone=名字:10m 中的 10m 是共享内存大小,1MB 大约能存 16000 个 IP 状态,10m 够多数中小站点用;rate 支持 r/s 和 r/m 两种单位,10r/m 就是平均 6 秒放行 1 个请求。
burst 和 nodelay 到底该设多少?
结论:burst 是允许的突发桶容量,nodelay 决定突发请求是“立即放行”还是“排队延迟处理”,防爬虫场景通常要加 nodelay。
不加 nodelay 时,超出 rate 的请求会被 nginx 压着排队,用户侧表现为页面加载变慢(可能几秒才返回),爬虫反而“感觉不到被拒”;加了 nodelay,桶内的突发请求立刻处理,桶满后直接返回 429,行为干净利落。
经验值:普通内容页 rate=10r/s + burst=20 nodelay,能容纳用户打开一个含十几张图片的页面时的并发子请求;搜索、列表翻页类接口 rate=1r/s + burst=5 nodelay;登录、短信、导出这类成本高的接口用 r/m 级别并把 burst 压到 3 以内。
用 $uri 做 key 有什么坑?
结论:把 $uri 拼进 key 能实现“每个 URL 独立计数”,但 zone 内存会被撑爆,而且爬虫加随机参数就能绕过。
写法是 limit_req_zone $binary_remote_addr$uri zone=peruri:50m rate=2r/s;。问题是 key 数量 = IP 数 × URL 数,一个日 PV 十万的站点很容易产生几十万个状态,50m 内存很快写满,之后新 key 会被直接放行(日志里出现“too many... zone is full”之类告警),限速形同虚设。
如果确实要针对可疑请求做更精准的限制,用 map 更划算——把正常 UA 映射为空串(key 为空时 nginx 不计数、不占内存),只让可疑 UA 进入限速:
map $http_user_agent $bad_ua_key {
default "";
"" $binary_remote_addr; # 空 UA
~*python $binary_remote_addr; # python-requests
~*curl $binary_remote_addr;
~*scrapy $binary_remote_addr;
}
limit_req_zone $bad_ua_key zone=badua:10m rate=1r/m;
这条规则只对命中名单的 UA 生效,正常浏览器完全不受影响,是成本最低的一层补充。
怎么验证限速真的生效?
结论:用循环 curl 打 30 次目标路径,看返回码里是否出现 429,再看 error_log 里的 limiting 记录。
for i in $(seq 1 30); do
curl -s -o /dev/null -w "%{http_code} " https://www.gj0.com/search/?q=test
done
预期输出前几个是 200,之后陆续变成 429。同时 tail -f /var/log/nginx/error.log 能看到类似 limiting requests, excess: 4.000 by zone "search", client: 1.2.3.4 的记录,其中 excess 就是超出速率的倍数,这个值持续偏高说明 rate 设得太松或确实在被刷。改完配置务必先 nginx -t 再 nginx -s reload,避免 reload 失败导致旧配置继续跑。
只限速就够了吗?
结论:limit_req 只管“请求速率”,不管“并发连接数”,两者要配合用。防止爬虫同时开 50 个连接慢速拉取时,limit_req 完全拦不住,需要 limit_conn_zone $binary_remote_addr zone=conn:10m; 配合 limit_conn conn 10; 限制单 IP 并发连接。
另外记得给 429 配个友好页面:error_page 429 /429.html;,否则部分前端框架拿到 429 会直接白屏。真被大规模爬时,单靠 nginx 限速只能止损,还需要结合日志分析 UA 与 IP 段,在防火墙层封禁,这一点在运维版块的实战帖里讨论得比较多。
一句话收束:limit_req_zone 按路径限速的关键是“一路径一 zone + location 引用 + burst/nodelay 调参”,再叠加 limit_conn 和 UA 维度的 map 白名单,才是完整的防爬组合。
转载请注明出处,版权归原作者所有。