服务器CPU过载,排查发现是假蜘蛛伪造User-Agent导致的。尝试了三套方案,包括Nginx按User-Agent放行、rdns双向反查和Nginx防火墙放行真爬虫,但都遭遇了坑。总结经验:不能单独依赖User-Agent,rdns查询需注意延迟和IP问题,CDN节点IP反查可能导致误判。
前阵子有天凌晨两点多,服务器报警短信直接把我震醒了。一台 4 核 8G 的线上云服务器,CPU 使用率冲到了 99%,load average 飘到了 18,PHP-FPM 的 worker 进程全被占满,正常用户连首页都打不开。
登录机器第一件事就是抓 access.log 看访问量。用 awk 扫了一圈前一千条日志,结果让人哭笑不得:
日志里铺天盖地全是 Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search.spider.html),中间还夹杂着少量的 Googlebot/2.1。
一秒钟涌进来三十几个请求,追着数据库开销最大的几个检索接口和翻页链接死命扫。
起初我还纳闷,网站收录一直不温不火,难道是搜索引擎终于开始全量抓取了?
等我登录百度搜索资源平台和 Google Search Console 抓取频次面板一看,当日蜘蛛抓取曲线平得像一条地平线,抓取频次总共才几百次。
压在服务器头顶的那些所谓的“蜘蛛”,全是用 Scrapy、curl 或者无头浏览器伪造了 User-Agent 的批量采集脚本。
为了把这些披着搜索引擎外衣的吸血采集程序拦在门外,同时又避免误杀真正的搜索爬虫,我先后试了三套方案,中间踩了几个非常隐蔽的深坑。
坑一:在 Nginx 层面按 User-Agent 直接放行白名单
这是许多教程推荐的做法。为了防止搜索引擎蜘蛛被限流规则(limit_req)误伤,直接在 Nginx 里配置 map 匹配:
map $http_user_agent $is_search_engine {
default 0;
~*(Baiduspider|Googlebot|Bytespider|Sogou) 1;
}
紧接着把 $is_search_engine 为 1 的请求从 limit_req 里面剥离出去,甚至直接跳过频率验证。
这样做等于把防盗门钥匙直接挂在了门把手上。
HTTP 请求头里的 User-Agent 只是客户端自己发过来的一行纯文本,没有任何防伪能力。采集者只要在 Python 脚本的 headers 字典里加上一行 'User-Agent': 'Baiduspider',你的所有流控限制、WAF 规则瞬间全部失效。
那次 CPU 飙到 100% 的事故,根源就在于这套按 UA 放行的白名单配置,被采集软件直接当作免费通道打穿了。
在任何场景下,绝不能单独把 User-Agent 当作身份凭证。
坑二:在请求链路里同步执行 DNS 反查导致 Nginx 连接池打满
既然 UA 不能信,官方给出的标准解法是双向 DNS 反查(Double Reverse DNS,即 rdns)。
以百度蜘蛛为例,标准的验证流程包含两步:
第一步,拿访问日志里的客户端 IP,去查它的 PTR 记录(反向解析),看域名是不是以 .baidu.com 或 .baidu.jp 结尾;
第二步,如果反查出来的域名符合规范,再拿这个域名去查正向 A 记录,看解析出来的 IP 是不是刚好等于当前发起请求的 IP。两头对得上,才是真蜘蛛。
逻辑挑不出毛病,但我最开始犯了个大忌:我尝试在 OpenResty 的 access 阶段用 Lua 脚本同步去查 DNS。
公网的 DNS 递归查询是有延迟的。一旦权威 DNS 出现网络抖动,或者海外节点响应迟钝,单次查询耗时很容易上升到 500 毫秒以上。
更要命的是,假蜘蛛的来源 IP 极其分散,常常来自各种家宽、肉鸡或者小众机房,很多 IP 压根没有配置 PTR 记录。本地 DNS 模块在遇到没有 PTR 的 IP 时,往往会等待数秒超时才返回空结果。
结果那次改动上线不到半小时,高并发请求涌进来,数百个连接全卡在 DNS 查询这一步。Nginx 的 worker_connections 很快告警耗尽,上游代理直接抛出 504 Gateway Timeout。
原本只是假蜘蛛消耗 CPU,改完之后连正常的真蜘蛛抓取和人类访问都被 504 挡了回去。
在 Web 服务器的同步请求阻塞链路上做外网 DNS 查询,无异于引火烧身。
坑三:套了 CDN 以后拿 CDN 节点 IP 去做反查
线上业务一般都挂着 CDN 节点做加速与缓存。接入 CDN 之后,Nginx 默认变量 $remote_addr 取到的其实是 CDN 边缘节点的回源 IP,不再是终端访客的原始 IP。
我有一回写了分析脚本,从 access.log 里提了一批 IP 跑 rdns 校验。
脚本执行完,直接提示反查域名全为 cdn.cloudflare.net,全部判定为假蜘蛛,紧接着这批 IP 就被丢进了防火墙封禁列表。
结果封完两分钟,整站流量直接断流。
原因就在于我没在 Nginx 里面正确指定 CDN 的可信回源 IP 段。
如果没有配置 set_real_ip_from 和 real_ip_header,脚本拿到的所有 IP 都是 CDN 的机房节点。把 CDN 回源节点判定为假蜘蛛并封杀,等于把全网所有进站请求全部拒之门外。
正确的 Nginx 回源配置必须提前注入:
# 信任 CDN 节点的真实回源网段(以实际 CDN 厂商公布的 IP 块为准)
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
# 指定真实客户端 IP 的来源头
real_ip_header X-Forwarded-For;
real_ip_recursive on;
只有当 $remote_addr 确实代表最外层访客的 IP 时,后续的蜘蛛判断才有依据。
坑四:死板依赖静态 IP 段白名单,误封移动端抓取蜘蛛
很多站长为了图省事,直接从网上抄一份几年前整理的百度蜘蛛 IP 段大全,把比如 220.181.108.0/24、123.125.71.0/24 写进白名单,其余只要带蜘蛛 UA 的一律封禁。
这招在几年前可能勉强管用,现在极易出事故。
各大搜索引擎的爬虫节点机房一直在动态调整与扩容,特别是移动端蜘蛛和各类区域渲染集群,IP 变动频率很高。
去年我就见过有朋友把静态 IP 段写死在防火墙里,结果百度新上线的抓取节点被全部丢弃,连续两周抓取错误率飙升到 60% 以上,搜索端直接判定网站不可达。
静态白名单只能作为基础参考,必须保留动态发现与动态核验机制。
现在的应对策略:分层过滤与异步核验架构
经过几次折腾,我把整套防护机制改成了分层处理结构:实时层只做柔性控流,强验证全部扔给后台异步守护进程。
访客请求
→ Nginx(识别 UA,给疑似蜘蛛打标签)
→ 放入独立漏桶限流(不放任狂刷,也不直接封死)
→ 异步记录专用日志 spider_access.log
↓
后台 Python 守护进程(定时扫日志,提取未验证 IP)
→ 本地缓存校验(已验证过直接跳过)
→ 执行双向 rdns 校验(PTR 反查 + A 记录比对)
↓
├─ 验证通过(真蜘蛛):加入 ipset 白名单,放宽抓取限额
└─ 验证失败(假蜘蛛):加入 ipset 黑名单,直接丢弃报文
第一步:Nginx 配置独立的蜘蛛限速桶与日志拆分
在 nginx.conf 中,给蜘蛛分配一个比普通接口宽松、但能防止打死后端的单独限速池:
# 普通接口每秒限制 5 次,蜘蛛专用通道限制每秒 20 次
limit_req_zone $binary_remote_addr zone=normal_limit:10m rate=5r/s;
limit_req_zone $binary_remote_addr zone=spider_limit:10m rate=20r/s;
# 提取疑似爬虫 UA
map $http_user_agent $spider_traffic {
default 0;
~*(Baiduspider|Googlebot|Bytespider|Yisouspider|Sogou) 1;
}
server {
listen 443 ssl http2;
server_name www.example.com;
location / {
# 根据标记走不同的限速区域
if ($spider_traffic = 1) {
limit_req zone=spider_limit burst=30 nodelay;
access_log /var/log/nginx/spider_candidates.log combined;
}
if ($spider_traffic = 0) {
limit_req zone=normal_limit burst=10 nodelay;
}
proxy_pass http://backend_upstream;
}
}
这样即便是突发的大量假蜘蛛,由于有 burst=30 nodelay 和 rate=20r/s 的硬卡点,PHP-FPM 和数据库也不会瞬间被冲垮。
第二步:后台轻量 Python 脚本异步验证真伪
用一个独立的脚本,每隔两分钟扫一次 /var/log/nginx/spider_candidates.log,对产生请求的新 IP 执行纯标准库的双向校验:
import socket
import re
# 搜索引擎官方合法反查域名后缀规则
LEGIT_DOMAINS = (
r'\.baidu\.(com|jp)$',
r'\.googlebot\.com$',
r'\.google\.com$',
r'\.bytedance\.com$',
r'\.sogou\.com$',
)
def verify_real_spider(ip: str) -> bool:
try:
# 第一阶段:反向查询 PTR 记录
hostname, _, _ = socket.gethostbyaddr(ip)
except (socket.herror, socket.gaierror, TimeoutError):
return False
# 校验域名后缀是否匹配官方规则
is_valid_domain = any(re.search(pat, hostname, re.IGNORECASE) for pat in LEGIT_DOMAINS)
if not is_valid_domain:
return False
try:
# 第二阶段:正向查询 A 记录核实回源 IP
resolved_ips = socket.gethostbyname_ex(hostname)[2]
return ip in resolved_ips
except (socket.gaierror, TimeoutError):
return False
在测试中,这个检测逻辑的准确率极高:
# 验证一个真实的百度蜘蛛 IP
python3 -c "from verify import verify_real_spider; print(verify_real_spider('220.181.108.77'))"
# 输出: True
# 验证一个伪造 UA 的境外主机 IP
python3 -c "from verify import verify_real_spider; print(verify_real_spider('154.21.32.112'))"
# 输出: False
第三步:结合 ipset 在操作系统内核层快速封禁
对于验证失败的假蜘蛛,脚本会调用系统命令将 IP 加入预先创建好的 ipset 集合中:
# 创建自动超时的黑名单集合,超时时间 86400 秒(一天)
ipset create fake_spiders hash:ip timeout 86400
# iptables 规则只检查一次集合,开销远低于逐条增加封禁规则
iptables -I INPUT -m set --match-set fake_spiders src -j DROP
Python 脚本发现假蜘蛛后,只需调用 subprocess.run(["ipset", "add", "fake_spiders", ip, "-exist"])。后续来自这个 IP 的所有 TCP 报文在到达 Nginx 之前,就直接在 Linux 内核层被丢弃了,根本不会占用任何 Web 服务资源。
对于验证通过的真实蜘蛛,脚本将其 IP 记录在本地缓存列表中,有效期设为 7 天。在 Nginx 层面可以为该 IP 放行更合理的抓取配额,确保搜索引擎爬虫畅通无阻。
梳理下来的几个准则
治理爬虫不能图省事。
请求头是客户端给的,永远不能当作信任依据。
不要在主处理流程里做耗时不可控的外网调用,把验证剥离出去异步处理,系统的承载力会稳固得多。
处理 IP 之前一定要核实真实来源头,搞清楚拿到的是终端还是代理节点,避免一刀切造成误封。
文章标题:服务器天天被假蜘蛛爬到 CPU 100%?排查 User-Agent 伪造、rdns 双向反查与 Nginx 防火墙放行真爬虫我踩过的几个坑
文章链接:https://llbbs.cn/jishujaocheng/234.html
本站文章均为原创,未经授权请勿用于任何商业用途
评论一下?