爬虫不一定会把带宽打满,却常常先把数据库拖垮。本文用 10 个问答讲清爬虫如何触发慢查询、把数据库连接吃满、让 PHP-FPM 排队并抛 502,以及分页与索引优化、热点缓存、限速封禁等可直接落地的配置。
Q:爬虫和带宽型攻击有什么不同,为什么它先打数据库?
- 带宽型攻击的目标是把出口打满,特征是大流量、高 pps、页面大量超时。
- 爬虫式抓取的目标是把后端的计算资源吃干净:它在带宽上往往毫不起眼,但每个请求都要真实查一次数据库。
- 关键差别在于爬虫专挑「每个请求都要算一遍」的页面:列表页、搜索页、详情页、翻页接口。这些页面通常没有缓存,或缓存命中率很低。
- 于是现象很典型:带宽正常、CPU 中等,但数据库连接数打满、慢查询暴涨、网站整体变慢甚至 502。
Q:怎么判断网站变慢是爬虫引起的?
# 1) 按来源 IP 与 UA 统计请求,看是否少数来源占据大量请求
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
awk -F'"' '{print $6}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
# 2) 看请求是否集中在动态路径(搜索、列表、详情)
awk '{print $7}' /var/log/nginx/access.log | sed 's/?.*//' | sort | uniq -c | sort -rn | head -20
# 3) 静态资源占比:正常用户会拉 js/css/图片,纯爬虫几乎不拉
grep -Ec '\.(js|css|png|jpg|jpeg|webp|svg|woff2?)' /var/log/nginx/access.log
- 判据三条:少数来源占据大量请求、请求集中在动态路径、静态资源占比极低。
- 再看数据库侧:
SHOW PROCESSLIST里是否堆着大量相同的 SELECT,慢查询日志是否在同一时间暴增。 - 如果请求分散、静态资源占比正常,那更像缓存配置或业务代码问题,别急着封 IP。
Q:慢查询是怎么被爬虫触发出来的?
- 常见三类:搜索关键词无索引(
LIKE '%关键词%'走全表扫描)、排序字段无索引、翻页偏移量过大。 - 慢查询本身不一定「坏」,坏的是被高频重复执行:单次 300ms 的查询在 1 QPS 下无所谓,到 100 QPS 就把 CPU 顶满。
- 先打开慢查询日志,把真实数据抓出来,再决定优化哪一条:
# MySQL 开启慢查询日志(临时生效,重启失效)
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1;
SET GLOBAL log_queries_not_using_indexes = ON;
SHOW VARIABLES LIKE 'slow_query_log_file';
# 汇总最耗时的查询
mysqldumpslow -s t -t 20 /var/log/mysql/slow.log
# 看当前连接与查询堆积
SHOW STATUS LIKE 'Threads_connected';
SHOW FULL PROCESSLIST;
Q:数据库连接被打满了怎么办?
- 现象是
Too many connections,应用侧报连接超时,页面 502/504。 - 先救火:临时上调连接上限并清理长时间空闲的连接,为排查争取时间。
- 再定位:判断是「连接泄漏」(应用拿了连接不还)还是「单纯被高并发撑爆」。
# 临时提高连接上限
SET GLOBAL max_connections = 500;
# 找出空闲时间过长的连接
SELECT id, user, host, db, command, time, state
FROM information_schema.processlist
WHERE command = 'Sleep' AND time > 60
ORDER BY time DESC;
# 缩短空闲连接存活时间(太短会导致频繁建连,谨慎调整)
SET GLOBAL wait_timeout = 120;
SET GLOBAL interactive_timeout = 120;
# 生成清理语句,确认后再执行
SELECT CONCAT('KILL ', id, ';') FROM information_schema.processlist
WHERE command = 'Sleep' AND time > 300;
- 注意:改服务端参数只是止血。如果应用侧连接池的最大连接数设得比数据库上限还高,照样把数据库压垮,必须两边一起调。
Q:深分页为什么特别耗数据库,怎么改?
LIMIT 1000000, 20会先扫描并丢弃前 100 万行再取 20 行,翻得越深越慢——而爬虫恰恰最爱一路翻到底。- 两条实用改法:游标分页(用上一页最后一条 ID 作为起点)与限制最大页码。
-- 慢:偏移量大,扫描后大量丢弃
SELECT id, title FROM article ORDER BY id LIMIT 1000000, 20;
-- 快:基于游标,用上一页最后一条 id 作为起点
SELECT id, title FROM article WHERE id > 1000000 ORDER BY id LIMIT 20;
-- 兜底:应用层限制 page 上限,超出直接返回 403 或空列表
- 另外两个动作:给排序与筛选字段建立覆盖索引,避免排序时产生临时表;把搜索结果的前两页单独缓存,因为绝大部分流量都落在这两页。
Q:搜索接口被爬虫高频请求怎么办?
- 搜索页是爬虫最爱,也最容易压垮数据库。治理思路是抬高单次请求成本、降低重复计算。
- 设置最低关键词长度(如少于 2 个字符直接拒绝)、结果条数上限,并禁止通配式前缀查询。
- 同一关键词的搜索结果做短时缓存(如 60 秒),爬虫刷同一批关键词时基本打在缓存上。
# Nginx:对搜索路径单独限速,比全站限速更精准
limit_req_zone $binary_remote_addr zone=search:10m rate=3r/s;
limit_conn_zone $binary_remote_addr zone=searchconn:10m;
location ^~ /search {
limit_req zone=search burst=6 nodelay;
limit_conn searchconn 5;
limit_req_status 429;
proxy_pass http://backend;
}
Q:缓存能救吗?怎么挡住爬虫的重复查询?
- 能,而且这是同时降成本又降压力的手段:把热门列表页、详情页、搜索结果缓存起来,重复请求不再触达数据库。
- 两层一起做:Nginx 页面级缓存挡在前面,应用层用 Redis 缓存查询结果兜底。
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=page:100m inactive=30m max_size=2g;
proxy_cache_key "$scheme$request_method$host$uri$arg_page";
server {
location ~ ^/(list|detail|search) {
proxy_cache page;
proxy_cache_valid 200 301 302 5m;
proxy_cache_lock on; # 同一 URL 并发只回源一次
proxy_cache_use_stale updating error timeout;
add_header X-Cache-Status $upstream_cache_status;
proxy_pass http://backend;
}
}
- 缓存键只保留业务必需参数(如
arg_page),随机参数一律不进键,否则爬虫加个随机参数就能绕过缓存。 - 用
X-Cache-Status观察命中率,命中率上不去,说明缓存键或 TTL 设置有问题。
Q:Nginx 层怎么识别「纯抓取会话」并限速?
- 纯爬虫有两个可判别特征:从不请求静态资源、单位时间请求数远超正常用户。
- 最稳的做法不是靠 UA 猜,而是对动态路径做频率限制——正常用户几乎不可能 1 秒请求 10 次列表页。
# 全站动态请求兜底限速
limit_req_zone $binary_remote_addr zone=dynamic:20m rate=8r/s;
location ~ \.(php|jsp|asp|aspx)$ {
limit_req zone=dynamic burst=16 nodelay;
limit_req_status 429;
}
# 对确认的抓取工具 UA 直接拒绝(误伤可控,但只是辅助手段)
map $http_user_agent $bad_bot {
default 0;
~*(python-requests|scrapy|curl|wget|httrack|go-http-client) 1;
}
if ($bad_bot) { return 403; }
- UA 不是可信凭据,攻击者随手就能改;它只适合挡低成本抓取工具,主力防线仍是频率与并发限制。
- 限速阈值上线前先按正常用户的峰值定基线,避免误伤 API 调用与监控探活。
Q:PHP-FPM 排队和 502 怎么缓解?
- 数据库变慢,请求就积压,PHP-FPM 进程池被占满,新请求排队直至超时,前端看到 502/504。
- 先给进程池定一个合理上限,并限制单个请求的最长执行时间,避免一个慢查询拖死一个进程。
; php-fpm 配置片段
pm = dynamic
pm.max_children = 50
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 20
pm.max_requests = 500
request_terminate_timeout = 30s
# Nginx 侧同步设置,避免后端已放弃、前端还在等
proxy_connect_timeout 5s;
proxy_read_timeout 30s;
- 核心原则:宁可快速失败,也不要让请求无限排队。排队会把故障从「某个页面慢」放大成「整站不可用」。
Q:还有什么补充建议?
- 分层拦截:CDN 挡静态与高频请求、WAF 挡明显恶意特征、Nginx 限速限并发、应用层做缓存与查询优化,四层各管一段。
- 建立基线:记录正常时段数据库 QPS、连接数、慢查询数量,异常时才有对照,不至于一慢就慌。
- 监控盯四个量:数据库连接数、慢查询增长率、PHP-FPM 队列长度、动态接口响应时间。
- 封禁要留退路:所有限速与封禁规则都要能一键回滚,并保留误封申诉渠道。
- 一句话总结:爬虫打不垮带宽,但能耗光数据库——把动态请求的重复计算降到最低、把单次查询成本压到最低、再把纯抓取会话的请求频率卡死,数据库这一关就守住了。