侧边栏壁纸
  • 累计撰写 181 篇文章
  • 累计收到 2 条评论

网站加了 Nginx 限流防 CC,搜索收录却断崖暴跌?排查 limit_req 误杀蜘蛛、503 降权与 map 白名单旁路我踩过的几个坑

2026-10-9 / 0 评论 / 9 阅读
AI 摘要由 AI 生成

文章指出,在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% 以下。

做性能防护与安全规则时,把搜索引擎爬虫当作特例慎重考量,往往能避开许多莫名其妙的收录暴跌。

评论一下?

OωO
取消