先交代一下背景。我手上有个做了几年的垂直内容站,平时日活不高,带宽和服务器配置也就普通水平,每个月流量账单一直很稳定。直到某天凌晨开始,监控图上那条平时很平稳的带宽曲线突然像心电图发作一样直直往上冲,冲到我这台服务器跑满带宽还继续往上顶。等我回过神来,出去已经跑掉了接近1.5TB的流量。
1.5TB单纯听数字可能没什么概念,对个人站或者中小业务来说,这个量级相当于一个月账单直接从几十块拉到上千块,而且服务器CPU和数据库也跟着遭殃。查日志排查后发现:不是用户暴涨,而是有人在拼命爬站——自动脚本一分钟发起几千次请求,把我整个站点的网页、图片、接口数据全部洗了一遍。这篇文章把我这次从发现问题、分析流量构成、应急拦截到做长期防护的全过程记录下来,包括排查思路、具体配置和踩坑,希望能让同样遇到爬虫洪水的朋友少走弯路。
1. 问题初现:从零星告警到带宽告急
1.1 那天早上发生了什么
事情发生在一个看似平常的周末凌晨。站点的流量监控比较简陋,就是服务器自带的面板加一个脚本定时探测,平时每天晚上看一眼就算完事。那天早上打开面板,第一眼差点没坐稳:带宽使用率显示100%,CPU负载也接近打满,MySQL的查询堆积成一片红灯。
这个站当时部署在一台4核8G的云服务器上,带宽是5Mbps保底、允许短时突发到100Mbps的那种套餐。平时日均PV两万左右,静态资源量不大,靠浏览器缓存和后台页面静态化扛住了绝大多数请求。但那天早上,页面开始偶尔出现504超时,后台编辑器保存要转圈好久,甚至有个别用户反馈“打开网站会白屏”。
我当时的反应分三步:
- 先重启了一下PHP-FPM和MySQL,想当然以为是代码死循环或者定时任务出了问题——结果没用,几分钟后又打满了。
- 用
top命令看到nginx进程的CPU占用高得吓人,但看不出哪个连接在作妖。 - 把出问题的站点临时切到一个“维护提示页”,前台访问先兜住,再开始逐层排查。
这里面有一个非常重要的教训:异常流量场景下,第一优先级永远是先保证业务可用,而不是先研究攻击者是谁。维护提示页切出去的几分钟内,带宽曲线立刻掉了一半,证明问题出在这个站点的对外访问上,而不是被机房邻居影响或者网络硬件故障。方向定了,后面就好办了。
1.2 1.5TB流量到底是个什么量级
很多人看到1.5TB这个数字第一反应是“也不大嘛”,但实际上要看流量基数。对这个当时月流量常在30GB~50GB波动的小站来说,1.5TB相当于日常流量的三四十倍,等于一夜之间来了过去一年半的量。
用更直观的方式换算:
| 数据口径 | 估算结果 |
|---|---|
| 1.5TB换算 | 约1500GB,折合约1536GB |
| 平均单次完整页面访问(含HTML+CSS+JS+3张图片) | 约2MB |
| 按2MB一个访问折算 | 约75万次完整页面抓取 |
| 按30天折算日均 | 每天50GB |
| 按1Gbps带宽打满计算 | 连续全速跑约3.3小时 |
| 按0.8元/GB流量计费折算 | 约1200元额外账单 |
关键不是这1200元本身,而是攻击者的请求量已经把服务器的处理能力打穿了,再往下走就是数据库被拖死、磁盘IO耗尽、全站永久性雪崩。后来我从云厂商控制台的流量报表确认,那段异常流量里请求数最高峰达到了每秒400多个,而正常时段全站请求每秒也就二三十个。100倍以上的请求量,任何普通配置的服务器都扛不住。
1.3 第一步:稳住业务而不是先查数据
我见过不少同行遇到类似情况,第一反应是开终端看日志看到底谁在爬,结果搞了半小时还没定位到,服务器已经彻底卡死连SSH都登不上了。这次我刻意改了顺序:
- 先确认源站是否还有操作空间:发现CPU和内存还有余量,但带宽被打满,说明瓶颈在出口流量,服务本身没有立刻崩。
- 优先对最耗流量的路径做“外科手术”:找到站点里最值钱的、数据量最大的页面路径(比如列表页、文章详情页、图片附件目录),先用Nginx配置临时将这些路径的访问频率限制到极低,或者直接对可疑来源IP返回状态码。
- 再打开日志做精细化分析。
这个过程花了大半个小时,但把业务损失控制在了最小范围。后面所有分析工作都是在稳定的服务器环境下完成的,效率高得多。
2. 流量画像分析:爬虫是“吃”流量的大户
2.1 翻日志:找出谁是主角
止损动作做完,我开始认真翻日志。当时Nginx的access.log已经涨到了一个可怕的大小——几个小时后单日日志就到了几个GB。在这种量级下,直接用编辑器打开日志文件是不可能的,必须用命令行工具切片处理。
我先做了两个最基础但最有效的统计:
# 按来源IP统计请求次数,取前20名 awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20 # 统计带有常见爬虫标识的请求数量 grep -iE 'python-requests|Go-http-client|Scrapy|curl|wget' /var/log/nginx/access.log | wc -l第一条命令的结果让我直冒冷汗:排名第一的IP在当天凌晨贡献了超过120万次请求,第二名的IP也有80多万次。更麻烦的是,这些IP一看就不是家庭宽带来源,反向解析出来全是某家知名机房的动态地址段,也就是说对方是用一台甚至多台服务器在跑抓取任务。
第二条命令的结果更有意思:明确带爬虫标识的请求只占30%左右,剩下70%的请求UA字符串伪装成了普通浏览器,比如“Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36”,这种复制下来的UA头和真实用户几乎一模一样,单纯靠UA黑名单只能放跑大鱼。
爬虫的大致分类也很清楚了:
| 爬虫类型 | 识别特征 | 本案例中的表现 |
|---|---|---|
| 低端爬虫 | UA包含python-requests、curl等 | 数量最多,但误伤率也高,容易靠UA规则拦截 |
| 伪伪装爬虫 | UA复制浏览器字符串,但TCP/IP指纹异常 | 占比70%,需要结合行为特征识别 |
| 搜索引擎蜘蛛 | UA含Baiduspider、Googlebot且反查IP匹配 | 本次没有异常增长,排除嫌疑 |
| 图片盗链/采集器 | 只对图片和附件路径发起高频请求 | 本次第二大头,后面会详细说 |
2.2 行为特征:爬虫和真实用户的区别
如果只看IP和UA,伪装爬虫很难和真实用户区分。但把行为特征放到微观层面,差别其实很明显。我当时从日志里抽了一个小时的数据做样本,按来源IP统计请求序列,发现几条规律:
- 请求路径高度集中:正常用户的访问路径是分散的,有的看首页、有的翻栏目、有的直接搜站内关键词;而爬虫的请求几乎集中在一类URL上,比如全部指向文章详情页,并且会按照URL的ID顺序从小到大依次抓取,这种“顺序遍历”是人类用户做不出来的。
- 请求间隔异常稳定:正常用户会停顿、跳跃、回退,请求时间分布是明显的暴破状;爬虫则是每隔固定的几百毫秒发一次请求,机械得可怕。
- 缺少前置页面来源:正常用户访问详情页之前通常会有首页或列表页的referer,而爬虫的referer要么为空,要么固定为站点首页,或者干脆伪造得很假。
- 不执行浏览器脚本和Cookie:爬虫直接发出HTTP请求,不带登录态,不带浏览产生的Cookie,也不加载页面上JS脚本发起的异步请求。
那时候我在日志里看到最典型的例子:某个IP从凌晨2点开始,每隔大约0.5秒请求一次文章ID相邻的两个页面,连续跑了一个多小时不停。这种请求模式,是纯粹的人力无法做到的。
2.3 波及范围:不只是HTML页面,图片和接口也被打穿
很多人有个误解,以为爬虫只是把网页HTML文本拉走,流量消耗不大。实际上完整抓取一个页面,浏览器连带的CSS、JavaScript、图片和字体资源也要一起请求。以我那个站为例,一篇带三张配图的文章,HTML正文只有不到100KB,但三张压缩后的图片加起来差不多有1.5MB。爬虫如果要清楚还原页面内容,会把图片也一并拉走,流量就成倍上升。
当时日志里有个现象让我印象很深:图片路径/wp-content/uploads/下的请求量占总请求量的40%以上。这些图片本可以让浏览器缓存起来,但爬虫每次都是直接重新下载,等于把整台服务器的图片资源反复洗了无数遍。
另一个被波及的是站内搜索接口。爬虫为了抓取某些关键词的搜索结果,会模拟用户提交搜索请求,导致数据库查询量爆炸式增长。这直接拖慢了MySQL的处理速度,用户真实的站内搜索也卡成了“正在加载”。所以判断爬虫的影响范围时,绝对不能只看带宽,还要注意到数据库压力、后端接口时延、磁盘IO这些隐藏指标。带宽被打满只是最外露的一个症状而已。
3. 应急止损:先把水龙头拧小
3.1 立即封禁“坐实”的IP,但要用对姿势
确认了爬虫IP和特征之后,我第一时间做的操作是封IP。这里不建议直接在Nginx配置里写deny,因为你要临时封的IP可能有很多,每次改配置都要reload一次,频繁reload会短暂切断所有连接,影响正常用户。我当时用的方法是iptables和ipset组合,把确认有问题的IP和IP段直接丢进内核网络层,一分钟之内生效,不经过应用层。
# 安装并创建一个带过期时间的ipset集合 ipset create crawler_blacklist hash:ip timeout 86400 # 添加单个IP,过期时间86400秒(24小时) ipset add crawler_blacklist 1.2.3.4 # 添加一个IP段 ipset add crawler_blacklist 1.2.3.0/24 # 将ipset集合接入iptables的INPUT链 iptables -I INPUT -m set --match-set crawler_blacklist src -j DROP这样每个IP自动在24小时后过期解封,避免把动态IP长期误封。手动封禁之后,我马上观察到带宽曲线开始掉下来,但并没有完全降到正常值——说明对方的爬虫IP池还不止我手上封掉的这些。
这里有个细节:做封禁之前一定要确认服务器没有套CDN。如果流量是从CDN回源过来的,封掉的IP全是CDN节点的地址,那就会直接把真实用户全封了,造成全站大面积404或者超时。当时我先是把CDN的回源IP段加入白名单,再在更上层的访问控制里对可疑来源IP做限制,避免误伤。
3.2 接入流量入口的“限流模式”
只封IP治标不治本,因为爬虫可以用大量代理IP轮换。我当时做的第二件事,是让流量入口处直接进入“限流模式”。如果你用了云厂商的CDN或WAF产品,这一步通常最快:
- 带宽峰值限制:在CDN控制台设置源站带宽上限,比如把某个域名的带宽峰值限制在5Mbps以下。一旦超过这个值,新进来的请求会被直接丢弃或排队,不会打到源站。这会造成部分正常用户访问变慢,但至少源站不会立刻被拖死。
- CC防护规则:开启WAF上的CC防护,设置单个IP的访问频率阈值,比如“单个IP每分钟超过300次请求就封禁”。这个阈值需要根据自己的业务场景调,也不能设得太死,否则校园网、公司出口这种共用IP的用户会被连环误杀。
- 人机验证:在WAF/CDN层面直接开启“滑块验证”或“JS挑战”,让所有访问者先过一道人类验证再回源。爬虫脚本普遍不会执行浏览器的JS验证逻辑,这一招能拦掉绝大多数恶意流量。
这些操作要占用你一点时间在控制台里点来点去,但相比自己在源站写代码来扛,效率高得多。我当天是把所有可用的入口层保护规则全部打开,大概过了一个小时,带宽曲线从100%降到了40%左右,看着服务器面板不那么吓人了。
3.3 Nginx限速与UA过滤
入口层规则扛住大头之后,我在源站Nginx上再加了一道保险,确保就算前面的规则被绕过,源站自己也有抵抗能力。下面这份是当时直接落地用的配置,摘两段核心的:
# 定义请求频率限制区域:单个IP限制为20请求/秒,超出的排队 limit_req_zone $binary_remote_addr zone=req_per_ip:10m rate=20r/s; # 定义UA黑名单,明显爬虫标识的直接拒绝 map $http_user_agent $is_bad_ua { default 0; "~*python-requests" 1; "~*Go-http-client" 1; "~*Scrapy" 1; "~*curl" 1; "~*wget" 1; "~*Apache-HttpClient" 1; "~*okhttp" 1; } server { listen 80; server_name example.com; # 命中UA黑名单的请求,直接断开连接 if ($is_bad_ua) { return 444; } # 对所有动态请求执行限速 location ~ \.php$ { limit_req zone=req_per_ip burst=40 nodelay; include fastcgi_params; fastcgi_pass unix:/run/php/php-fpm.sock; } # 图片目录单独限速,防止盗链/爬虫拉爆带宽 location ~* \.(jpg|jpeg|png|gif|webp)$ { limit_req zone=req_per_ip burst=60 nodelay; expires 7d; try_files $uri =404; } }这里必须提到一个小技巧:return 444。这是Nginx特有的做法,不是返回错误页,而是直接断开TCP连接,客户端什么都收不到。返回403或者404虽然也能拦,但服务器还是要回一个HTTP响应体,对攻击者来说反而多泄露了信息,也白白消耗了一些流量。444是“你在门口就被轰走了,连门缝都不给你看”,在处理恶意爬虫时非常好用。上面这份配置改完一加载,UA特征明显的爬虫请求瞬间全部变成了“连接被重置”,日志里的错误段也安静了很多。
4. 中长期防线建起“三层挡板”
4.1 与合规爬虫“谈判”:robots.txt的正确写法
应急手段管一时,管不了一世。等攻击者发现UA被拦了,他下次随手改一个UA头就能绕过。所以我们必须把防线从“临时堵”升级成“长期预防”。
先说 robots.txt,这个机制只对“守规矩”的爬虫有效,包括搜索引擎的蜘蛛和少数尊重协议的采集器。恶意爬虫根本不看这个文件,但你写了总比不写好,至少能把站内某些目录从搜索引擎的收录范围里排除掉,减少被动暴露。
我当时重新整理了 robots.txt:
User-agent: * Disallow: /member/ Disallow: /cart/ Disallow: /search?* Crawl-delay: 10 User-agent: Baiduspider Allow: / Crawl-delay: 5 User-agent: Googlebot Allow: / Crawl-delay: 5需要注意,robots.txt里Disallow: /是“全站禁止收录”,对SEO是毁灭性的,千万别学一些教程的写法。Crawl-delay是告诉搜索引擎蜘蛛“抓完一个页面要等多少秒”,这能显著降低蜘蛛对服务器的压力。很多新手不知道这个参数,导致搜索引擎蜘蛛高并发抓全站,把带宽吃掉一大半。这其实不算攻击,但也是流量消耗的大头。
4.2 应用层反爬机制
robots.txt解决不了恶意爬虫,Nginx限速也挡不住换UA的。真正要防得长久,还是得在应用层做一套“行为识别”逻辑。我把当时思考的几个关键点直接列出来,多少有些参考价值:
- IP维度限流:缓存每个IP每分钟的请求次数,超过阈值就直接拒绝或者跳到验证码页面。这个比Nginx层更灵活,因为可以细分到每个接口。
- 用户维度频率限制:如果是登录用户,按账号维度限制次数。这能防止一个正常登录用户被劫持后批量拉数据。
- JS挑战:在核心页面加载一个前端脚本,让浏览器先执行一段计算再带着结果访问后端接口。爬虫如果用的是简单HTTP客户端,就卡在这一步过不去。
- 接口签名:给所有写操作和敏感读接口加上签名校验,时间戳超过5分钟就失效。这样就算被爬到接口地址,也不能直接重放。
- 数据返回节流:列表页默认只返回20条数据,搜索页限制只能翻50页。数据到手难度变高了,攻击者就会觉得“性价比太低”。
这套组合拳里,我建议优先做“IP频率限制+用户行为校验”,因为这两者对正常用户体验影响最小,但对爬虫来说非常致命。行为校验用生活化类比就是:商场里,你看到一个人来了就逛、买了就走了,不会拦他;但如果一个人一天之内进出商场100次还什么都不买,保安肯定要上去问问。爬虫就是这样需要高频进出“商场”的角色,应用层要做的就是在门口把他叫住。
4.3 静态资源与源站彻底解耦
回到流量账本上看,那次1.5TB里超过一半是图片和附件资源。如果源站的图片、CSS、JS都在同一个域名下,甚至直接挂在同一台服务器上,那无论怎么防,只要页面被请求,这些静态资源都会被带着拉走。
最稳妥的方案是“动静分离+对象存储+CDN”。操作路径大概是:
- 把历史图片附件迁移到云上的对象存储服务,通过CDN加速域名对外提供服务。
- 页面里的图片地址全部改成CDN域名,源站只负责输出HTML文档。
- 给可预见的静态资源设置超长缓存时间:
Cache-Control: max-age=2592000,让浏览器和CDN节点缓存起来,不给爬虫反复回源的机会。
做完这步之后,即使爬虫继续请求图片,流量也主要消耗在CDN节点上,不会直接打到你的源站服务器。源站的带宽压力一下子就下来了。很多人觉得这只是“省钱”操作,其实对防爬虫来说,这更接近一种“物理隔离”——攻击者连源站的肉都碰不到,只能在CDN这层跟你的缓存策略赛跑。
4.4 监控与告警:做到“流量异常早知道”
最后一条长期防线,是监控和告警。我承认自己这次反应慢,很大一部分原因就是监控太弱——带宽达到100%才告警,等于人都进医院了才发现生病。后来我把监控体系补全成三层:
| 监控层级 | 监控指标 | 告警策略 |
|---|---|---|
| 基础设施层 | 带宽使用率、CPU负载、内存使用率、磁盘IO | 超过基线的150%持续5分钟即告警 |
| 应用层 | QPS、接口RT、数据库慢查询数、PHP-FPM进程数 | QPS超过日常基线的3倍或RT超过2秒持续3分钟告警 |
| 业务层 | PV/UV、搜索次数、登录失败次数 | PV明显下跌或搜索失败率超过5%告警 |
告警渠道也要分级:夜间电话告警只留给最严重的情况,微信/即时消息通知留给一般异常,避免“狼来了”搞久了大家都不看消息。另外我建议设“基线告警”而不只是固定阈值告警,因为每个站点的日常流量曲线都不一样,固定阈值要么太敏感误报,要么太迟钝漏报。
5. 常见问题与排查技巧实录
这个部分整理一下我在处理爬虫流量过程中踩过的坑和后来总结出来的排查思路,做成速查表的形式,希望能帮你省点时间。
| 问题现象 | 可能原因 | 排查方向 | 解决办法 |
|---|---|---|---|
| 封了某个IP,流量还在涨 | 爬虫用了代理池/动态IP池 | 看日志里有没有新的陌生IP出现,观察请求速率是否下降 | 全局频率限制+行为验证,靠封IP只能暂缓 |
| CDN命中率很高但源站流量依然大 | 动态接口被高频请求,或者回源配置被绕过 | 看源站日志里请求最多的URL是不是接口路径 | 对接口单独限流,在WAF层加人机验证 |
| 爬虫完全复刻了浏览器UA | 普通UA黑名单失效 | 比较UA头字段顺序、Accept-Language、TCP指纹 | 上行为特征识别,不要指望UA规则能拦截 |
| 开启限流后误伤正常用户 | 阈值设得太过激进或用户来自共享出口IP | 检查被拦截用户的来源IP段和User-Agent | 将明显正常的IP段加入白名单,或把验证码改为滑块 |
| 日志文件太大,分析起来卡死 | 直接用编辑器打开了几GB的日志 | 改用grep/awk/sed做流式处理 | 先按小时切片,或者直接采样1万行做统计 |
| 机器人发请求频率不高但单次响应体很大 | 对方在抓下载类文件或高清大图 | 看日志中响应字节数最大的请求链路 | 对下载类URL单独限速,大文件转存对象存储 |
| 服务器没崩但流量费用飙升 | 某些隐匿爬虫长期低频爬取 | 对比账单中“境外流量”占比和日志中境外IP占比 | 在DNS层或Nginx层屏蔽不相关的地区IP |
还有一些排查技巧,是书本上一般不写但实际环境下无比好用的:
- 做日志分析时,先截取异常最严重的五分钟作为样本,而不是全量拉取。样本越聚焦,越容易快速找到精准的IP和URL特征。
- 反过来看带宽日志而不是只看访问日志。有些服务器面板会单独记录每个连接的带宽占用,找出流量最大的几个TCP连接,基本就是爬虫元凶了。
- 留意响应体大小分布。正常用户访问的是小页面,爬虫或采集器通常会把页面内容存下来,单次响应体动辄数MB。通过按月统计响应字节数Top100的URL,可以揪出长期潜伏的低频爬虫。
- 突发情况下先拉黑机房的IP段,比精确定位单个IP快得多。很多爬虫都跑在云服务商的主机上,查一下IP归属你就会发现,无非就是那么几家机房的地址段反复出现。
如果你有一个长期稳定运营的内容站点,我强烈建议平时就把访问日志导入日志分析平台里做好索引,而不是临时用grep去翻几十GB的日志。那是一个你早晚会用到的投资,而且越早越好。
那次事件之后,我们流量账单再也没出现过心跳骤停。回头复盘,对我最有用的其实不是某一套神奇配置,而是把“预防、发现、响应”这套流程跑顺了。现在每次有异常流量进来,我习惯先问三个问题:这个请求是从哪来的?它在请求什么?它访问的频率像不像个人类?如果你也能立即回答这三个问题,绝大多数爬虫在你面前就裸奔了。希望这篇博客能让你下次碰到爬虫时,不用等账单出来才意识到不对劲。