Nginx 防爬虫规则放在 http、server、location 哪个块更有效?
Nginx 防爬虫宜分层配置:limit_req_zone、limit_conn_zone、map、geo 等定义型指令只能置于 http,全站基线拦截放 server,搜索、接口等高价值路径在 location 收紧,多层同时生效时取最严
结论:分层放——limit_req_zone、limit_conn_zone、map、geo 这类「定义型」指令只能写在 http 块;真正的拦截动作写在 server 块做全站兜底,再用 location 块对 /search、/api 这类高价值路径单独收紧。三层同时写会同时生效,取其中最严的一条。
为什么 limit_req_zone 必须写在 http 块?
因为 Nginx 语法就这么定的:limit_req_zone、limit_conn_zone、map、geo 属于只能在 http 上下文声明的指令,写进 server 或 location 会直接启动失败,报 "limit_req_zone" directive is not allowed here。
http {
# 定义限流区,10m 共享内存约可记录 16 万个 IP 的限流状态(1MB ≈ 1.6 万)
limit_req_zone $binary_remote_addr zone=perip:10m rate=5r/s;
limit_conn_zone $binary_remote_addr zone=conn_perip:10m;
# 用 map 定义恶意 UA 变量,比在 location 里堆 if 更安全高效
map $http_user_agent $bad_bot {
default 0;
~*(python-requests|scrapy|curl|wget|Go-http-client|HeadlessChrome) 1;
}
# geo 定义 IP 白名单,搜索引擎与自家监控 IP 放行
geo $white_ip {
default 0;
10.0.0.0/8 1;
}
}
这里有个关键点:map 和 geo 都是「只计算变量、不执行动作」,放在 http 块是零成本的,只有真正被引用时才参与计算,不存在全局拖慢性能的问题。
server 块和 location 块该怎么分工?
分工原则是:server 块管全站基线,location 块管重点区域。爬虫打站往往不是均匀访问,80% 的压力集中在搜索页、列表页、详情页和接口上,全站一个阈值要么误伤正常用户,要么放跑爬虫。
server 块建议给一个宽松基线:
server {
listen 80;
server_name www.gj0.com;
limit_req zone=perip burst=20 nodelay; # 全站基线:5r/s,突发 20
limit_conn conn_perip 20; # 单 IP 并发连接上限
if ($bad_bot) { return 403; } # UA 黑名单直接拒绝
}
location 块对易爬路径收紧,burst 压到 3~5,并加上返回 429 的兜底:
location ~ ^/(search|api/list|thread-) {
limit_req zone=perip burst=3 nodelay;
limit_conn conn_perip 3;
limit_req_status 429;
proxy_pass http://127.0.0.1:8080;
}
提醒一句:if 在 location 里是著名的坑(if is evil),条件里出现 proxy_pass 时行为容易失控,所以 UA 判定统一用 map 生成变量,再在 server 层 return 403,不要在 location 里写复杂 if。
三层同时配置会不会冲突?
不会冲突,会叠加。limit_req 和 limit_conn 在 Nginx 的 preaccess 阶段执行,同一个请求如果同时匹配 http 继承下来的规则、server 的规则和 location 的规则,每一条都会被逐一检查,任意一条超限就按各自的 limit_req_status 返回(默认 503)。
所以不用担心「写了三层只生效一层」,反而要注意别把阈值叠得太死——比如 server 层 rate=5r/s、location 层又挂一个同样 rate=5r/s 的 zone,实际效果等于双重计数,正常用户也会被打。正确做法是同一路径只用一个 zone,靠 burst 区分松紧。
怎么验证规则真的生效?
用 curl 打高频请求 + 看 error.log 两条路交叉验证,比猜靠谱。
for i in $(seq 1 20); do curl -s -o /dev/null -w "%{http_code}\n" https://www.gj0.com/search?q=test & done; wait
# 确认限流日志(默认在 error.log,级别 error)
grep "limiting requests" /var/log/nginx/error.log | tail -5
日志里出现 limiting requests, excess: 3.2 by zone "perip" 就说明规则命中了;如果一条都没有,多半是 $binary_remote_addr 前面有 CDN 或反向代理,取到的是回源 IP,需要配合 real_ip_header 和 set_real_ip_from 还原真实客户端 IP,否则全站会被当成同一个 IP 一起限流。
三个常见配置错误
第一,把 limit_req_zone 写进 server 块,Nginx 直接起不来;第二,用了 $remote_addr 而不是 $binary_remote_addr,后者是二进制压缩存储,同样内存能存的 IP 数量约为前者的一半以内;第三,只靠 robots.txt 和 UA 黑名单,这两样对伪造 UA 的采集器基本无效,真正兜底的还是基于 IP 的频率与并发限制。
具体到实际站点,建议先用 server 层基线跑 3~7 天,从 access.log 里统计真实用户的峰值 QPS,再决定 location 层的 burst 数值,不要一上来就压到 1r/s。这类调优细节在站内的运维版块(https://www.gj0.com/ )也有不少实战帖可以对照。
总结一下:定义放 http,基线和 UA 拦截放 server,重点路径收紧放 location,三层叠加生效、取最严;上线前务必确认真实客户端 IP 能取到,否则限流会误伤全站。
转载请注明出处,版权归原作者所有。