那天下午,我的网站没了。
不是变慢,是彻底没了——网页打不开、SSH 连不上、连 ping 都没有回应,整台服务器像被拔了电。我第一反应是被黑了。但真相更荒诞:它是被”正常的访问”活活刷死的。
这是一次真实的排查实录:一个没多少流量的小站,怎么会被爬虫刷到整机瘫痪?我一步步挖下去,挖到一个很多自建站的人根本没设防的东西——“筛选迷宫” + 分布式假爬虫。如果你也自己搭站、跑服务器,这篇能帮你在崩之前就把门关上。
崩的方式很有辨识度:
ping 无响应;load average 冲到 130+(正常是个位数)。这个画像很关键:它不是”被一次性打挂”,而是资源被持续吃光——CPU 和内存被占满,正常请求连排队的机会都没有。是洪水,不是炸弹。
翻访问日志,我以为会看到某个 IP 的请求量暴涨——没有。请求量不算离谱,UA 大多还是”正常浏览器”。真正扎眼的是 URL:满屏都是带一长串筛选参数的组合,像这样:
/tools/?scene=写作&cat=对话&price=free&speed=fast&sort=hot
/tools/?scene=写作&cat=对话&price=free&speed=fast&sort=new
/tools/?scene=编程&cat=对话&price=paid&speed=fast&sort=hot
...(无穷无尽)
这就是经典的“筛选迷宫”(faceted navigation trap):一个列表页,挂上几个筛选维度,就能裂变出天文数字的 URL。
裂变出上万个 URL 还不是最要命的。要命的是——它们没法被缓存。
普通页面第一次访问后可以进缓存,后面直接吐缓存、几乎不耗资源。但筛选组合是无穷的,缓存命中率约等于零。于是每一个组合请求,都要实打实地跑一遍 PHP、查一次数据库。
而每个 PHP 请求会占用进程池里的一个”工人”。爬虫把上万个组合并发地砸过来,进程池瞬间被占满 → 后面的真人请求全部排队 → 排队又占内存 → 内存吃穿 → 整机雪崩。崩的不是某个页面,是整台机器。
我进一步分析这波流量,发现两个特征:
真人不会这样。这是典型的分布式爬虫 / 住宅代理:租一大批家用宽带 IP,套上一个正常 UA,专门绕开”单 IP 限速”这种传统防护——你封了一个 IP,它换一个接着来。这波流量连续十几天,占了我两到五成的总请求。它不在乎你死活,只想把你的数据爬干净。
这就带出一个两难:我不能简单按 UA 或 IP 一刀切。因为真正的 Googlebot、Bingbot 也自称 bot,我要是把它们误伤了,等于自断收录。得先能分清真假。
答案是反向 DNS 双向校验——这是 Google、Bing 官方都推荐的验真方法,假爬虫伪造不了:
关键点:假爬虫可以伪造 UA,但伪造不了它 IP 的反向 DNS 记录。它反查要么没记录,要么域名根本不属于官方,正反一对就露馅。真搜索引擎则永远对得上。
与其纠结”这是谁”,不如看”它在干嘛”。真人和真搜索引擎都不会去枚举无穷的筛选组合,所以:
# nginx:带 4 个及以上筛选参数的请求,直接 444(连响应都不给,掐断连接)
if ($request_uri ~ "^/[^?]+\?([^&]*&){3,}") { return 444; }
# 非人类 UA + 带 ? 参数的筛选页,也 444
if ($http_user_agent ~* "bot|spider|crawler|python|scrapy") { set $bad "1"; }
444 是 nginx 的特殊返回码:不回任何内容、直接关闭连接,比 403 更省资源——连一个字节都不浪费给它。
分布式爬虫会换 IP,但它要抓的 URL 特征躲不掉。所以别只按 IP 限速,要给”筛选类 URL”设一个全局并发上限:
# 给带筛选参数的请求设全局并发上限 + 限速,无论来自哪个 IP
limit_conn faceted 20;
limit_req zone=dyn burst=10 nodelay; # 从 burst=20 收到 10
这样即使一万个 IP 一起冲筛选页,能同时在跑的也就那么几个,其余排队或被拒,后端永远不会被打穿。
这条最反直觉。我的 php-fpm max_children 之前设成了 300,以为”能扛更多并发”。真相是:它反而是崩站的帮凶。
算笔内存账你就懂了:
| max_children | × 单进程内存 | = 峰值内存需求 | 结果 |
|---|---|---|---|
| 300 | ~60 MB | ~18 GB | 被打满 → 内存吃穿 → OOM 整机瘫 |
| 160 | ~60 MB | ~9.6 GB | 扛得住 → 满了就”拒绝服务”,不拖垮全机 |
要点:max_children 应该设成“内存扛得住的上限”,而不是”我希望的并发数”。设对了,最坏情况是它开始拒绝新请求;设太大,最坏情况是整台机器一起死。前者可恢复,后者要重启。
三板斧上齐后,load 从 130+ 掉回个位数。之后再有爬虫来冲筛选迷宫,全被 444 挡在门外,后端稳如泰山。
如果你的站也莫名卡死、崩溃,花十分钟按这个清单查:
load/内存是不是被占光。是洪水不是炸弹。max_children 调到内存扛得住。这次崩站教会我最重要的一件事:威胁不一定长着”攻击”的样子。把你刷崩的,可能就是一堆套着正常 UA、慢慢啃你数据的爬虫。它们不违法、不报警,却能让你的站在某个下午毫无征兆地黑屏。
如果你的站也遇到莫名卡死、502、负载爆高、疑似被爬虫刷,或者你想在崩之前就把这些门关好——可以找我聊。这套”筛选迷宫 + 分布式爬虫”的坑,我刚刚踩透,也把加固方案跑通了。