文章指出,在Nginx中加入`limit_req`限流模块防止CC攻击时,误将搜索引擎爬虫视为恶意攻击,导致抓取频率大幅下降,收录停滞。原因在于爬虫的高并发请求与限流配置冲突,以及错误地使用User-Agent进行过滤。文章建议不要在location中使用if判断User-Agent,并指出简单放宽限流参数不能解决问题。
上周看服务器监控,有几个 IP 拿着扫描器在暴力翻扫动态接口,一秒抛过来十几个请求,把 PHP-FPM 进程数拉得老高,机器负载一度飘到 8 以上。为了应急,我顺手在 Nginx 加上了 limit_req 模块做单 IP 频率限制,给每个 IP 限制每秒 5 次请求。
配置热重载以后,恶意请求确实被大片拦截,负载直接降到了 0.3。原本以为这事处理得很利落,结果到了第三天上午去翻百度搜索资源平台和 Google Search Console,整个人直接愣住:抓取频次曲线从平时每天两千多次断崖下跌到不足五十次,抓取异常监控里全是密密麻麻的 503 报错,刚发出来的几篇技术文章在抓取队列里全被标记为“服务器错误”,新内容收录彻底停滞。
查了一下 Nginx 错误日志,真相非常直白:我亲手配置的防御规则,把真正的搜索引擎爬虫当成 CC 攻击给就地正法了。
为什么普通用户没事,蜘蛛却被精准干掉?
普通人看网页,点开一个页面看两分钟,再点下一个链接,平均请求频率连 0.1r/s 都不到,哪怕限流设成每秒 2 次,正常浏览几乎毫无感觉。
但搜索引擎调度器的抓取策略完全不同。一旦百度蜘蛛或者 Googlebot 的调度算法决定抓取站点的更新列表或 Sitemap,常常是由同一个爬虫 IP 节点集中发起 TCP 链接,在几秒内并发发出几十个甚至上百个页面请求。
看看我最初写在 nginx.conf 里的配置:
http {
# 每个 IP 每秒最多 5 个请求,申请 10M 共享内存
limit_req_zone $binary_remote_addr zone=req_limit:10m rate=5r/s;
server {
location / {
# 突发允许 10 个请求,超出立即拒绝
limit_req zone=req_limit burst=10 nodelay;
proxy_pass http://backend;
}
}
}
当爬虫节点以 30 个并发瞬时涌进来时,前 5 个请求正常处理,接下来的 10 个被 burst=10 兜底接纳,而剩下的 15 个请求在同一毫秒内直接被 nodelay 判定为超标,Nginx 默认向客户端甩出 503 Service Temporarily Unavailable。
普通用户遇到 503 可能会按 F5 刷新一下,但搜索引擎爬虫对 503 极其敏感。在搜索引圣诞抓取算法中,503 明确意味着“源站当前负载过载崩溃”。为了防止把站长的服务器压垮,调度器内置的自我保护机制会立刻触发,主动把该站点的抓取预算(Crawl Budget)削减到谷底。抓取配额一旦腰斩,搜索引擎不再高频派蜘蛛来访,哪怕你的内容写得再好,爬虫不来抓取,收录自然直接停摆。
踩坑一:千万别在 location 里用 if 判断 User-Agent
发现是爬虫被限流拦截后,第一反应通常是:“那我放行爬虫的 User-Agent 不就行了?”
当时随手在 location 里写了类似这样的逻辑:
# 严重反模式:千万不要在 location 里这么写
location / {
if ($http_user_agent ~* "Baiduspider|Googlebot") {
# 以为写个空指令就能跳过限流,或者重定向到无限流的内部 location
}
limit_req zone=req_limit burst=10 nodelay;
}
这写法踩了两个暗坑。
第一,伪造 User-Agent 没有任何门槛。攻击者用 Python 脚本发请求时,随便加一行 headers:User-Agent: Mozilla/5.0 (compatible; Baiduspider/2.0),整套 CC 防御形同虚设,限流瞬间被击穿。
第二,Nginx 社区里著名的 “If is Evil” 不是开玩笑的。在 location 上下文中混用 if,会强行打乱 Nginx 的内部阶段划分。一旦里面混着 proxy_pass、fastcgi_pass 或者 rewrite,偶发性的 404、参数丢失、甚至配置静默失效排查起来极其痛苦。
踩坑二:单纯把 rate 和 burst 放大解决不了问题
第二个容易犯的妥协,是把参数放宽,比如改成 rate=50r/s burst=100。
跑了两天我发现这完全是自欺欺人。阈值一放宽,稍微猛一点的采集脚本一秒发 30 个请求直接长驱直入,限流模块形同虚设,服务器该卡顿还是卡顿。
更尴尬的是,当百度蜘蛛多线程调度跑满上百并发时,偶尔还是会在毫秒级撞碎 burst=100,错误日志里依旧零星冒出 503。阈值放得太宽,恶意 CC 挡不住;阈值放得太死,高并发蜘蛛又遭殃。根本原因在于不能拿同一把尺子去卡所有人,必须把受信任的爬虫单独摘出来。
优雅解法:利用 Nginx 空 Key 旁路机制
翻读 Nginx 的 ngx_http_limit_req_module 官方文档,有这样一条极为关键的底层规则:
If the key value is empty, the request is not accounted.
(如果 key 的值为空字符串,则该请求不计入限流,直接放行。)
这就是实现搜索引擎白名单旁路最优雅、性能损耗近乎为零的方式。我们根本不需要在 location 里写任何混乱的 if 跳转,只要通过 geo 和 map 构造一个动态变量:
- 普通访客:变量值计算为客户端真实 IP(
$binary_remote_addr),正常进入限流计数。 - 白名单爬虫与受信任 IP:变量值计算为空字符串(
""),Nginx 自动跳过限流模块。
生产可用配置:geo + map 建立干净的旁路通道
把规则写在 http 块中,整个结构层次分明:
http {
# 第一步:根据客户端 IP 判定是否属于官方爬虫网段或自用信任 IP
# 0 表示普通流量,1 表示白名单流量
geo $is_trusted_bot {
default 0;
# 内部运维办公出口与监控探针
192.168.1.0/24 1;
10.0.0.0/8 1;
# 百度蜘蛛常见回源网段(按实际采集到的真实节点添加)
116.179.32.0/24 1;
180.76.15.0/24 1;
220.181.108.0/24 1;
# Googlebot 常用网段
66.249.64.0/19 1;
# 必应 Bingbot 常用网段
157.55.39.0/24 1;
207.46.13.0/24 1;
}
# 第二步:将判断结果映射为限流 Key
# 白名单流量输出空字符串,普通访客输出二进制 IP
map $is_trusted_bot $spider_limit_key {
0 $binary_remote_addr;
1 "";
}
# 第三步:使用映射后的 key 声明限流桶
limit_req_zone $spider_limit_key zone=dynamic_limit:10m rate=10r/s;
# 设置被拦截时的响应状态码(默认为 503,推荐显式指定 429)
limit_req_status 429;
# 自定义日志格式,把限流状态打到 access.log 里方便观察
log_format main_with_limit '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" limit_status:$limit_req_status';
server {
listen 80;
server_name example.com;
access_log /var/log/nginx/access.log main_with_limit;
location / {
# 引入限流,白名单请求由于 key 为空会直接放行
limit_req zone=dynamic_limit burst=20 nodelay;
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
}
这里有两个细节特别需要留心:
其一,被限流时推荐设置 limit_req_status 429。Nginx 默认返回的 503 代表服务器内部错误,搜索引擎看见 503 容易误判源站崩溃而大减配额;而 HTTP 429(Too Many Requests)在现代语义中明确表示请求频次过高,规范的爬虫在收到 429 时会稍后按节奏重试,语义更加透明。
其二,如果网站前面套了 CDN(比如 Cloudflare、又拍云、阿里云全站加速等),$remote_addr 拿到的是 CDN 回源服务器的 IP。CDN 节点回源本来就高度汇聚,如果不配 real_ip 模块,所有用户在 Nginx 眼里都是同一个 CDN 节点的 IP,整站流量会在几秒内被彻底打死。
如果使用了 CDN,必须在 http 块中补上真实 IP 透传转换:
# 信任 CDN 节点的回源网段
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
# 从 CDN 约定的请求头中提取真实用户 IP
real_ip_header X-Forwarded-For;
real_ip_recursive on;
怎么验证限流旁路确实生效了?
配置改完以后,千万别单凭感觉就上线,用工具在命令行压测两圈,状态看得清清楚楚。
1. 验证普通 IP 能够被正常拦截
在另一台测试机上,用高并发测试工具压一下站点:
# 模拟 30 个并发请求打过去
ab -n 50 -c 10 http://example.com/
查看 Nginx 的 access.log,可以看到前十几个请求状态码为 200,日志末尾的 limit_status:PASSED,随后的请求立刻收到 429,状态标记为 limit_status:REJECTED。
2. 验证白名单 IP 能够顺畅穿透
用 curl 伪装或直接在白名单网段的机器上发起连续请求:
for i in {1..30}; do
curl -s -o /dev/null -w "%{http_code}\n" http://example.com/
done
在白名单机器上执行,返回结果整整齐齐全是 200。此时检查服务端的 access.log,所有请求的限流状态全部显示为 limit_status:BYPASS。
改完后的抓取恢复情况
配置切换上去后跑了一周,翻开各家站长平台的监控:
- 百度抓取频次在第三天从几十次快速回弹到了每天一千八百次,抓取诊断不再出现任何 429 或 503。
- Google Search Console 里的服务器错误曲线彻底归零,之前抓取失败的 URL 重新进入自动索引流程。
- 真正恶意扫描和刷接口的黑产 IP,在 access.log 里被稳稳卡在 429 拦截线外,服务器 CPU 占用一直稳定在 10% 以下。
做性能防护与安全规则时,把搜索引擎爬虫当作特例慎重考量,往往能避开许多莫名其妙的收录暴跌。
文章标题:网站加了 Nginx 限流防 CC,搜索收录却断崖暴跌?排查 limit_req 误杀蜘蛛、503 降权与 map 白名单旁路我踩过的几个坑
文章链接:https://www.llbbs.cn/jishujaocheng/239.html
本站文章均为原创,未经授权请勿用于任何商业用途
评论一下?