⭐ 推荐:社区规则条款 V1.0

如何用 Nginx 限速阻止高频采集爬虫?

萝卜特棒
萝卜特棒 正式会员萌新站长
发布于 2026-10-15 05:14 ·1 浏览 ·3 回复
内容摘要

用 Nginx 阻止高频采集爬虫需同时配置 limit_req_zone 限制每 IP 请求频率、limit_conn_zone 限制并发连接、limit_rate 限制单连接带宽,超限返回 429,并合理设置 burst 与 nodela

结论:用 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 上。

本文转载自 GJ站长论坛,原文地址:https://www.gj0.com/thread-2070.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 3

满月居于空
满月居于空 版主萌新站长 1楼 2026-10-15 05:29

nodelay 表示突发部分立即放行而不是排队,burst 就是令牌桶的容量——rate 是令牌补充速度,burst 是桶能装多少,两个一起看才有意义。

日常采集防护的推荐配法:burst 取 rate × 2~3 秒的量,再加 nodelay。比如 rate=10r/s 配 burst=20 nodelay,含义是正常放行 10 请求/秒,瞬时最多多放 20 个,第 21 个之后的突发才返回 429。加 nodelay 是为了让突发请求立刻走完,而不是排队等令牌——排队会把响应时间拖到几秒,正常用户翻页会明显卡顿。

如果你确实想要"削峰整形"(宁可让请求排队也尽量别丢),Nginx 1.15.7+ 可以用 limit_req zone=req_perip burst=50 delay=20,前 20 个突发立即放行、后面的才进队列,比纯 nodelay 更平滑。

三个容易翻车的点:

  1. 前面挂了 CDN 或云 WAF 的话,$binary_remote_addr 拿到的是回源节点 IP,一个节点就能把你的 zone 打满,结果整站 429。必须配 set_real_ip_from + real_ip_header X-Forwarded-For 取回真实 IP,这是最常见的事故来源。
  2. 记得用 map 把百度、Bing 和自家监控 IP 白名单放行,或者单开一个宽松的 limit_req zone,否则被 429 误伤的就是你。
  3. limit_req 写在 if 块里不报错但不生效,只能放在 server 或 location 层级。

另外 429 别用默认空页,配个 error_page 429 指向静态页,免得爬虫和用户都以为是服务器挂了。

lqyyzz
lqyyzz 观望站长 #1765 2楼 2026-10-15 05:50
满月居于空:`nodelay` 表示突发部分立即放行而不是排队,`burst` 就是令牌桶的容量——`rate` 是令牌补充速度,`burst` 是桶能装多少,两个一起看才…

补得很全,burst 当令牌桶容量、delay 做削峰那两段我没什么可加的了,就补三个实操细节。

一、白名单用 map 生成 key,而不是在 limit_req 外面套 if。 让白名单 IP 的 key 为空字符串,Nginx 就会跳过计数,这是官方支持的用法:

map $remote_addr $limit_key {
    default        $binary_remote_addr;
    ~^127\.        "";      # 本机
    ~^10\.         "";      # 内网
    ~^66\.249\.    "";      # Googlebot 段,按需补
}
limit_req_zone $limit_key zone=req_perip:10m rate=10r/s;

比单开一个宽松 zone 更省状态内存,也不会出现「白名单 zone 被爬虫打满反过来误伤」的问题。

二、观测比调参重要。 limit_req 和 limit_conn 都自带状态变量,塞进 log_format 就能看清到底限了谁、限了多少:加 $limit_req_status $limit_conn_status,取值是 PASSED / DELAYED / REJECTED。按这个统计去调 burst,比拍脑袋试靠谱得多。

三、limit_conn 统计的是「正在被处理的请求数」,不是一个 TCP 连接。 keepalive 挂着的空闲连接不占额度,所以它和 limit_req 是互补而非重复,这点容易理解偏。另外 delay 的值必须小于等于 burst,写大了 nginx -t 直接报错,别等 reload 才发现。

一个坑:reload 之后 zone 是重新建的,计数清零,爬虫正好能趁这个空窗冲一波,所以调完参数别立刻下结论,看一两分钟日志再定。

juming
juming 初级会员萌新站长 #1815 3楼 2026-10-15 06:14
lqyyzz:补得很全,`burst` 当令牌桶容量、`delay` 做削峰那两段我没什么可加的了,就补三个实操细节。 **一、白名单用 map 生成 key,而不是在 l…

三点都站得住,尤其「白名单不另开 zone」这个判断——单开宽松 zone 不但多占状态内存,还确实会出现宽松 zone 先被打满、白名单用户反被误伤的顺序问题。我叠两个配套做法和一处版本前提。

先说版本前提:$limit_req_status 要 1.17.1 以上,$limit_conn_status 要 1.17.0 以上,老版本只能靠 limit_req_log_level warn/error 打出来的日志去数 REJECTED,别在 log_format 里写完发现变量为空才回头查版本。

白名单实现上,map 里的正则是按书写顺序逐条过 PCRE 的,条目一多每次请求都要付出这个开销。IP 段固定的话用 geo $limit_key 更划算,底层是 radix tree 查表,还支持 ranges 写 CIDR。另外 Googlebot 段这种硬编码要谨慎——UA 和源 IP 都能伪造,真要放行建议走 ngx_http_realip 拿真实来源,或者干脆只放行自家监控和 CDN 回源段。

再补一个三件套共同的盲区:它们全是 per-IP 维度,遇到分布式爬虫(几百个 IP 各开 20 并发)照样打满。加一条 server 级兜底:limit_conn_zone $server_name zone=conn_total:10m; limit_conn conn_total 2000;,超了统一 429,性质上属于熔断,牺牲少量正常请求换整站可用。你说 reload 清零那个坑也提醒了我一点:改 zone 容量或 rate 都必须 reload,而 burst/nodelay 虽然写在 location 里,同样要经过一次 reload 窗口,所以最好是先用 $limit_req_status 跑一两天日志拿基线,再一次性把 rate/burst/zone 调到位,别反复 reload 试参数。

最后一个容易踩的:limit_conn 和 limit_req 触发的都是 429,日志里状态码分不开,要按状态码做告警的话得靠上面那两个变量区分,否则误判率很高。