爬虫拦截没效果?带宽和CPU没降的原因

加了 UA 黑名单、也开了限速,带宽和 CPU 却没降,这是爬虫拦截最常见的翻车现场。本文用 10 个问答讲清:拦截发生在哪一层、带宽和 CPU 到底被谁吃掉、怎么三步验证,以及哪些配置错误会让规则悄悄失效。

Q:为什么规则加了,带宽和 CPU 却没降?

  • 因为省不省资源取决于拦截发生在哪一层,而不是规则写得多狠。写得很严但位置不对,一样白拦。
  • 带宽是在边缘节点产生的。源站上的 deny 再严格,边缘该发的响应体已经发出去、也计过费了。
  • Nginx 的拦截发生在连接建立、请求解析之后,TCP 握手与 TLS 握手的 CPU 已经花掉了。
  • 最常见的假生效:规则只拦了首页,而爬虫实际爬的是列表页、详情页和图片。

Q:拦截到底分几层,各层能省什么?

  • 边缘层(CDN / 云 WAF):请求根本没到源站,带宽与 CPU 全省,效果最好。
  • 反代层(SLB 或前置 Nginx):省源站资源,但不省前置机自己。
  • Nginx 请求阶段 return 403:省应用 CPU,不省连接与握手开销。
  • 应用层(PHP / Java 里判断 UA):框架已加载、数据库可能已连,几乎不省。
  • 一条判据就能定位:把同一批恶意请求分别从边缘入口和直连源站各打一次,两边返回码不一致,说明拦的是源站那一层。
  • 结论很直接:能往前拦就往前拦,宁可多一层规则,也不要放到业务代码里判断。
# 在日志里区分「本地处理时间」与「回源时间」,差值大说明时间花在本机
log_format timing '$remote_addr $status rt=$request_time urt=$upstream_response_time';

# 403 占全部请求的比例,先用它判断拦截是否真的在发生
awk '{n++; if ($9==403) b++} END{printf "403=%.2f%%\n", b*100/n}' /var/log/nginx/access.log

Q:第一步该查什么?规则真的命中了吗

  • 先对账:控制台或规则里的命中数,和日志里 403 / 429 的条数是否在一个量级。差一个数量级就是没真正匹配。
  • 常见原因:location 优先级(正则 location 先匹配,deny 写在前缀块里可能永不执行)、rewrite ... return 提前返回、include 的文件没被加载。
  • 永远看线上真正生效的那份配置,而不是编辑器里那份。
nginx -T 2>/dev/null | grep -nE 'deny|limit_req|map .*(bot|ua)' | head -20

# 伪造一个爬虫 UA 复现,确认返回码符合预期
curl -sS -o /dev/null -w '%{http_code}\n' \
  -A 'Mozilla/5.0 (compatible; Bytespider)' https://www.example.com/list/

Q:为什么只按 UA 拦,基本挡不住?

  • UA 是最容易伪造的字段,现在多数采集器默认就带一个正常浏览器 UA。
  • 只拉黑几个知名爬虫 UA,等于只挡住了最老实的那一批。
  • 有效做法是多维同时命中:UA 分类 + 路径 + 频率 + 指纹,任一维度单独命中都不拦。
map $http_user_agent $ua_class {
    default                       human;
    ~*(bot|spider|crawl|scrapy)   claimed_bot;
    ""                            empty;
}

# 只有「自称爬虫」且打列表页 / 搜索接口的请求才进入限流键
map "$ua_class:$request_uri" $bot_key {
    ~*^claimed_bot:/(list|search|api)   $binary_remote_addr;
    default                             "";
}

limit_req_zone $bot_key zone=bot:20m rate=5r/s;
limit_req_status 429;

Q:带宽为什么还在涨?

  • CDN 计费按边缘响应体算。爬虫爬图片和视频,即使全部命中缓存、源站零回源,边缘带宽照样计费。
  • 所以图片站、下载站的爬虫防护必须做在边缘,源站拦等于白付带宽钱。
  • 先确认缓存状态,再看是不是爬虫在拉大文件:单 IP 的响应字节数分布最能说明问题。
  • 边缘侧还有三个便宜手段:对大文件开防盗链、给静态资源单独设低速率限额、对图片列表页按 UA 分档缓存。三者都不影响正常用户。
curl -sSI https://www.example.com/static/cover.jpg \
  | grep -iE 'x-cache|age|content-length'

# 按 IP 汇总响应字节数 Top10(默认 combined 格式下 $10 是 body_bytes_sent)
awk '{b[$1]+=$10} END{for(k in b) printf "%s %.1fMB\n", k, b[k]/1048576}' \
  /var/log/nginx/access.log | sort -k2 -rn | head

Q:CPU 为什么没降?

  • 动态页面被爬是最常见的原因:Nginx 放行后 PHP / Java 已经执行、SQL 已经查完,CPU 花在应用侧。
  • 日志 IO 是隐形开销:拦截同样要写 access_log,高 QPS 下写盘和刷盘会吃掉可观 CPU。
  • 缓存击穿:爬虫扫带随机参数的 URL,缓存永不命中,每个请求都回源重建。
  • 先定位是哪一类,再决定是加规则还是改缓存键。随机参数这一类的解法是规范化缓存键:把无意义的 query 剔除出 proxy_cache_key,只有真正影响内容的参数才参与。
# 被请求最多的路径,前 20
awk '{print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20

# 带随机参数的请求占比(这类请求缓存必然不命中)
awk '$7 ~ /\?/ {q++} {n++} END{printf "query=%.1f%%\n", q*100/n}' \
  /var/log/nginx/access.log

Q:哪些配置错误会让规则悄悄失效?

  • location 顺序:正则 location 优先于前缀匹配,写在错误块里的规则不会执行。
  • map 忘了忽略大小写:UA 判断不加 ~*,大小写一变就绕过。
  • 在 if 里写 limit_req 或 add_header:指令不支持,不报错但行为不可预期。
  • 只改源站没同步 CDN:边缘还在放行,源站日志里看不到痕迹,你误以为规则生效了。
  • zone 开太小:10m 大约只放 16 万个 IP 状态,被打量大时状态被淘汰,限流形同虚设。
  • 用 XFF 做限流键却没配 set_real_ip_from:所有人被识别成同一个回源 IP,一次限流全站一起 429。
  • limit_req_status 用默认值:默认 503 会被监控判成宕机,也会被 CDN 当可缓存错误页存下来,必须改成 429 并加 Cache-Control: no-store。

Q:怎么三步验证拦截真的生效?

  • 单点复现:用 curl 伪造 UA 和高频请求,看返回码是否符合设计。
  • 指标对比:同口径对比拦截前后各 15 分钟的 QPS、403 占比、带宽、CPU,别拿昨天比。
  • 源头核对:边缘日志与源站日志对照,恶意 IP 在源站应该基本消失。它们还在源站大量出现,就是没拦住。
# 连续 20 次请求,预期在中后段出现 429
for i in $(seq 20); do
  curl -sS -o /dev/null -w '%{http_code} ' -A 'python-requests/2.31' \
    https://www.example.com/list/
done; echo

Q:拦得太狠误伤正常用户怎么办?

  • 新规则先跑观察模式:只记录不拦截,一周后看命中了哪些请求,确认没有真实用户再上线。
  • 搜索引擎蜘蛛白名单要按 IP 反查验证,不要只信 UA,否则白名单本身就成了绕过后门。
  • 超限返回 429 而不是 403:429 表示可以重试,403 会让部分客户端直接放弃,也会让搜索引擎判定为永久拒绝。
  • 支付、登录、回调三类链路单独放行,这三条链路被拦,损失比被爬更大。
  • 真出现误杀,先看分布再看总量:429 集中在某一类 UA、某个地域或某个接口,说明规则切得太粗,改阈值比加白名单更彻底。

Q:还有什么补充建议?

  • 拦截不是目的,成本才是目的。目标是把带宽和 CPU 拉回基线,不是把拦截数量做大。
  • 顺序固定为分类 → 限速 → 封禁。一上来就封 IP,误伤率和维护成本都最高。
  • 每周固定看一次爬虫请求占比和来源 ASN,趋势比单日数字有用得多。
  • 最后一句:规则生效不等于资源下降,真正省资源的是拦截位置,不是拦截数量。