本文分析了网站TTFB(首包时间)过慢导致爬虫抓取困难的问题,并针对Nginx缓存穿透、TLS握手慢和上游阻塞等问题提出了排查和解决方案。作者指出,虽然DNS解析和TCP连接时间较短,但TLS握手耗时过长,影响了爬虫抓取效率和内容更新速度。针对fastcgi_cache配置不当导致爬虫穿透缓存的问题,作者建议通过配置Nginx忽略私有缓存头来解决。
上个月查看 Nginx 访问日志,发现百度和必应的爬虫抓取频次降得厉害。翻看百度搜索资源平台的抓取诊断,好几个栏目页直接报错抓取超时。
在服务器终端里用 curl 跑了一次带耗时统计的请求:
curl -o /dev/null -s -w "DNS解析: %{time_namelookup}s\nTCP连接: %{time_connect}s\nTLS握手: %{time_appconnect}s\n首字节到达: %{time_starttransfer}s\n总耗时: %{time_total}s\n" "https://example.com/article/1024"
终端打印出来的数字很难看:
DNS解析: 0.024s
TCP连接: 0.038s
TLS握手: 0.412s
首字节到达: 1.846s
总耗时: 1.865s
DNS 解析和 TCP 连接加起来才六十毫秒出头,但 TLS 握手花了四百多毫秒,从握手结束到首字节返回整整等了一点四秒。
搜索引擎爬虫判断页面可抓取性的首要指标就是首包时间(TTFB)。如果每个页面都要等上一两秒才开始传输数据,爬虫在单次抓取预算(Crawl Budget)耗尽前就会放弃后续链接,新发布的文章很难被及时建库。
坑点一:fastcgi_cache 配置了,但爬虫次次穿透
很多人以为在 Nginx 开启了 fastcgi_cache 就万事大吉。我也在 server 块里写了 fastcgi_cache 指令,并且设置了缓存时间。
用带有爬虫 User-Agent 的请求一查,响应头暴露出了问题:
curl -I -A "Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)" "https://example.com/article/1024"
返回头里写着:
HTTP/2 200
content-type: text/html; charset=UTF-8
set-cookie: PHPSESSID=b4f8a9c2fa98c71b; path=/; HttpOnly
cache-control: no-store, no-cache, must-revalidate
x-cache-status: BYPASS
问题出在 PHP 代码与 Nginx 默认缓存策略的冲突。
很多程序会在全局入口引入 Session 机制,或者插件为了统计访客直接调用了 session_start。PHP 只要启动 Session,就会在响应头里自动附加 Set-Cookie 和 Cache-Control: no-store。
Nginx 的 fastcgi_cache 遇到这两个响应头,默认会认为内容涉及用户私密状态,自动绕过缓存。
解决办法分两步走。第一步是在 Nginx 配置里忽略上游吐出来的私有缓存头,让公开的文章详情页强制走缓存:
# 定义缓存路径与内存键空间
fastcgi_cache_path /var/cache/nginx/fastcgi levels=1:2 keys_zone=WORDPRESS:100m inactive=60m max_size=1g;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
server {
# 后台管理和已登录用户跳过缓存
set $skip_cache 0;
if ($request_method = POST) {
set $skip_cache 1;
}
if ($query_string != "") {
set $skip_cache 1;
}
if ($request_uri ~* "/admin/|/wp-admin/|xmlrpc.php") {
set $skip_cache 1;
}
if ($http_cookie ~* "comment_author|wordpress_logged_in|admin_user") {
set $skip_cache 1;
}
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_cache WORDPRESS;
fastcgi_cache_valid 200 301 302 30m;
fastcgi_cache_valid 404 1m;
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
# 忽略 PHP 自动吐出的阻碍缓存的头
fastcgi_ignore_headers Cache-Control Expires Set-Cookie;
# 隐藏上游给匿名访客分配的无意义 Cookie
fastcgi_hide_header Set-Cookie;
add_header X-Cache-Status $upstream_cache_status;
}
}
配置重载后再次测试,第二次访问时 X-Cache-Status 变成了 HIT,首字节响应直接压到了二十毫秒以内。
坑点二:缺少 TLS 会话复用与 OCSP Stapling
爬虫并发抓取时会发起大量瞬时连接。前面测出来的四百毫秒握手延迟,直接消耗了三分之一的等待时间。
排查发现 SSL 相关的几项优化项一直空着。
首先是会话复用。客户端握手完成后,如果服务器没有保留会话状态,后续新连接依然需要重新跑两轮协商。
其次是证书吊销检查(OCSP)。爬虫或者验证客户端发起连接时,需要去证书颁发机构验证证书有没有被吊销。国内服务器访问海外 CA 的验证端点常常遇到网络抖动,一卡就是几百毫秒。
在 Nginx 配置中补齐相关的安全与会话设置:
# 开启 TLS 1.3 与加密套件
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
# 开启 SSL 会话缓存,10m 内存大约可以存储四万个会话
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets on;
# 开启 OCSP Stapling,由服务器代向 CA 查询并缓存结果
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
# 指定稳定快速的公共 DNS 用于 OCSP 查询
resolver 223.5.5.5 119.29.29.29 valid=300s;
resolver_timeout 5s;
配置生效后,在终端通过 OpenSSL 测试会话重连效果:
openssl s_client -connect example.com:443 -reconnect -no_ssl2 2>&1 | grep "Re-used"
如果输出连续显示 Re-used,说明会话缓存生效,爬虫抓取时的后续连接握手时间能够降低到单次往返延迟(约三十毫秒)。
坑点三:fastcgi_buffers 过小导致响应落盘
在排查某些图文混排的长文章时,我发现即便绕过了 PHP 慢查询,首字节依然会偶尔抖动到八百毫秒。
翻看 Nginx 的错误日志 /var/log/nginx/error.log,抓到了一条告警信息:
[warn] 14205#14205: *88201 an upstream response is buffered to a temporary file /var/cache/nginx/fastcgi_temp/1/02/0000000021 while reading upstream, client: 123.125.71.55, server: example.com, request: "GET /article/580.html HTTP/2.0", upstream: "fastcgi://unix:/run/php/php8.2-fpm.sock:"
警告含义很明确:PHP 吐出来的 HTML 内容体积超出了 Nginx 分配的内存缓冲区,Nginx 只能先把超出部分写进磁盘临时文件,再读取出来发送给客户端。
如果服务器当时正好有日志写入或者 MySQL 刷盘,磁盘 I/O 排队会导致首字节传输被挂起。
调整 Nginx 的 FastCGI 缓冲参数,让常见的文章页面全部留在内存完成流转:
# 增大 FastCGI 响应头和响应体缓冲区
fastcgi_buffer_size 32k;
fastcgi_buffers 16 32k; # 缓冲池总大小达到 512KB
fastcgi_busy_buffers_size 64k;
fastcgi_temp_file_write_size 64k;
修改后重启 Nginx 并压测包含大体积代码块的文章页,临时文件告警彻底消失,大页面响应平稳得多。
坑点四:PHP-FPM 进程池动态伸缩与慢查询拖垮队列
有时候爬虫在夜间集中抓取几百个页面,短时间内涌入大量未缓存请求,PHP-FPM 就会成为瓶颈。
当时配置文件里采用了动态模式:
pm = dynamic
pm.max_children = 20
pm.start_servers = 2
pm.min_spare_servers = 2
pm.max_spare_servers = 5
白天低谷期闲置进程被回收,只剩下两个空闲 worker。爬虫突然发起二十个并发抓取,master 进程临时 fork 子进程需要耗费时间和系统资源。
更糟糕的是文章底部挂了一个随机推荐模块,SQL 语句直接写了 ORDER BY RAND() LIMIT 5。
在几万条数据的表上执行随机排序属于全表扫描,单个请求耗时就要八百毫秒。两个正在工作的 worker 被慢查询占住,新请求在 socket backlog 队列里排队,后来的爬虫直接遇到两秒以上的首包卡顿。
针对这个环节采取两项改动。把进程模式改为静态分配(static),固定子进程数量,省去运行期间 fork 进程的开销:
pm = static
pm.max_children = 16
pm.max_requests = 1000
开启慢请求跟踪日志定位耗时调用:
request_slowlog_timeout = 2s
slowlog = /var/log/php-fpm/slow.log
把低效的随机查询重写为基于主键 ID 范围的快速点查,并将推荐结果放入内存缓存。这样即便爬虫集中抓取冷门旧文章,单个页面的执行时间也稳定在五十毫秒以内。
优化后的实测效果
完成上述四处改动后,再次针对线上文章页执行测量。
在关闭浏览器缓存的情况下多次运行 curl 拆解耗时:
DNS解析: 0.021s
TCP连接: 0.035s
TLS握手: 0.052s
首字节到达: 0.071s
总耗时: 0.089s
首字节响应从最初的一点八秒缩减到了七十毫秒左右,TLS 握手也稳定在五十毫秒。
连续观察了一周的爬虫日志,百度蜘蛛单日抓取频次恢复到了两千次以上,抓取诊断里再也没有出现超时报错。
技术 SEO 的许多规则看似繁琐,底层逻辑离不开扎实的服务器网络和进程调优。把基础性能打磨干净,搜索引擎的抓取效率自然就能上一个台阶。
文章标题:网站 TTFB 动辄两三秒拖垮爬虫抓取?排查 Nginx 缓存穿透、TLS 握手慢与上游阻塞我踩过的几个坑
文章链接:https://www.llbbs.cn/jishujaocheng/218.html
本站文章均为原创,未经授权请勿用于任何商业用途
评论一下?