结论:用 Nginx 拦高频采集爬虫,核心是「请求频率 + 并发连接 + 单连接速率」三件套——limit_req_zone 限制每秒请求数、limit_conn_zone 限制同 IP 并发连接数、limit_rate 限制单连接下载带宽,超过阈值直接返回 429,三者缺一,爬虫只要多开几个连接就能绕过去。
Nginx 限速为什么只配 limit_req 不够?
因为 limit_req 只统计「请求数」,不统计「连接数」。采集爬虫最常见的手法就是开 20~50 个并发连接慢速拉取,每个连接每秒只发 1 个请求,总速率完全在限制之内,但带宽和 CPU 全被吃掉。反过来,limit_rate 是单连接级指令,爬虫开 10 个连接就能拿到 10 倍带宽。所以只有三者组合才封得住:
limit_req 管「单位时间请求数」——防高频遍历;
limit_conn 管「同一时刻并发连接数」——防慢速多连接;
limit_rate 管「单连接字节速率」——防整站被拖走带宽。
Nginx 限速的具体配置怎么写?
结论:把 zone 定义在 http 块,把限速指令放在 server 或 location 块,并显式设置返回码为 429。可直接抄下面这份配置(Nginx 1.18 及以上均支持):
limit_req_zone $binary_remote_addr zone=req_perip:10m rate=10r/s;
limit_conn_zone $binary_remote_addr zone=conn_perip:10m;
limit_req_status 429; # 默认是 503,改成 429 语义更准确
limit_req_log_level warn;
server {
listen 80;
server_name example.com;
limit_req zone=req_perip burst=20 nodelay;
limit_conn conn_perip 20;
location / {
proxy_pass http://backend;
limit_rate 512k; # 单连接 512KB/s
limit_rate_after 1m; # 前 1MB 不限速
}
}
两个关键参数说明:$binary_remote_addr 比 $remote_addr 省内存,IPv4 每状态 4 字节;10m 共享内存区在 64 位平台上大约能保存 8000 个 IP 的状态(每状态约 128 字节),超过后旧记录会被 LRU 淘汰。rate=10r/s 的单位只能写整数,要表达「每 2 秒 1 次」就写 30r/m。
还要注意:limit_req 生效在 preaccess 阶段,即使命中代理缓存也会被计数;磁盘缓存命中的静态资源同样受限,如果希望图片/CSS 放宽,就在对应的 location 里单独写一条 limit_req zone=req_perip burst=50 nodelay。
burst 和 nodelay 到底该怎么设?
结论:burst 是允许瞬时突发的请求数,nodelay 表示突发部分立即放行而不排队延迟,两者一起用才不会把正常用户拖成「匀速 1 秒 1 个」。以 rate=10r/s burst=20 nodelay 为例:前 20 个超速请求立刻处理,第 21 个开始返回 429。若不加 nodelay,第 11 个请求会被压到队列里每 100ms 放一个,用户体验会明显卡顿。另外记住 rate=10r/s 的真实精度是毫秒级——它按每 100ms 放行 1 个请求计算,不是「1 秒内累积 10 个」。
爬虫伪造 X-Forwarded-For 或走 CDN 时怎么办?
结论:必须先让 limit_req 拿到真实客户端 IP,并且只信任自己的回源 IP 段。如果站点前面挂了 CDN 或反向代理,$remote_addr 是代理 IP,所有访客会共用一个限速桶,正常用户先被误伤。正确写法是:
set_real_ip_from 10.0.0.0/8; # 你自己的 CDN/代理回源段
set_real_ip_from 172.16.0.0/12;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
set_real_ip_from 只能写你可信的代理段,绝不能写 0.0.0.0/0,否则爬虫只要自己伪造一个 X-Forwarded-For 头就能把限速桶拆成无数个,限速直接失效。real_ip 模块在 postread 阶段执行,早于 limit_req,所以顺序上没问题。
对特定 UA 可以再加一道更严的桶,用 map 实现,key 为空字符串时 Nginx 不计数,这个特性正好用来放行正常流量:
map $http_user_agent $bot_key {
default "";
~*(Scrapy|python-requests|Go-http-client|SemrushBot|MJ12bot) $binary_remote_addr;
}
limit_req_zone $bot_key zone=req_bot:10m rate=1r/s;
怎么验证限速真的生效了?
结论:用同 IP 短时间打 100 个请求,如果前 N 个返回 200、之后出现 429,说明配置生效。命令如下:
for i in $(seq 1 100); do
curl -s -o /dev/null -w "%{http_code} " http://example.com/
done
更贴近真实压测用 ab -n 500 -c 20 http://example.com/ 或 wrk -t2 -c20 -d10s http://example.com/。如果返回的仍是 503 而不是 429,检查 limit_req_status 是否写在 http 块(写在 server/location 也有效,但容易被覆盖)。要长期观察,可在 Nginx 1.17.6 及以上版本用 $limit_req_status 变量落日志:
log_format limit '$remote_addr "$request" $status req=$limit_req_status';
access_log /var/log/nginx/limit.log limit;
这样能直接统计每天有多少请求被 REJECTED,再决定是把阈值调紧还是把某些 IP 段直接 deny 掉。
总结一句:limit_req_zone + limit_req burst nodelay 管频率,limit_conn 管并发,limit_rate 管带宽,配合 real_ip 取真实 IP、limit_req_status 429 明确返回码、$limit_req_status 日志做观测,五个动作配齐,多数高频采集爬虫都会在几十秒内被挡在 429 上。