侧边栏壁纸
  • 累计撰写 158 篇文章
  • 累计收到 2 条评论

网站 TTFB 动辄两三秒拖垮爬虫抓取?排查 Nginx 缓存穿透、TLS 握手慢与上游阻塞我踩过的几个坑

2026-10-5 / 0 评论 / 6 阅读
AI 摘要由 AI 生成

本文分析了网站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 的许多规则看似繁琐,底层逻辑离不开扎实的服务器网络和进程调优。把基础性能打磨干净,搜索引擎的抓取效率自然就能上一个台阶。

评论一下?

OωO
取消