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

网站开启 HTTP/2 甚至 HTTP/3 百度收录却断崖暴跌?排查 Baiduspider 协议兼容、Nginx ALPN 降级协商与连接重置 (RST) 我踩过的几个坑

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

文章探讨了网站升级HTTP/2甚至HTTP/3后百度收录暴跌的问题。作者发现,百度爬虫Baiduspider的内核与TLS ALPN协商存在短板,导致其无法与服务器建立连接。同时,Nginx配置变更也影响了连接。这些问题导致抓取频次剧减,收录断崖式下跌。

前两个月为了压榨整站的加载性能,给几台主业务服务器全量升级了 Nginx,把 HTTP/2 和基于 QUIC 的 HTTP/3 全部跑了起来。当时做完优化心里挺美,Chrome 和 Safari 测速跑分几乎拉满,首屏 TTFB 缩短了一大截,Googlebot 的抓取抓得飞快,搜索后台的抓取频次一路往上涨。

结果安稳日子过了不到三周,百度搜索资源平台的抓取诊断突然红了一大片。

我起初没当回事,以为是机房线路抽风或者百度平台的偶发误报。直到后台收录曲线直接走出断崖式暴跌,新发的技术文章连续两周一条都不进索引库,再去翻看抓取异常日志,才发现整站抓取频次跌去了将近八成,报错信息清一色写着“连接超时”或“连接重置”。

现代浏览器访问一切如丝般顺滑,Google 抓取没有任何阻碍,偏偏百度蜘蛛像撞墙一样死活进不来。在抓包抓了整整两天、翻烂了 Nginx 源码文档与抓取日志后,我揪出了这一连串隐蔽到令人发指的技术深坑。


陷阱一:以为协议向下兼容万无一失?Baiduspider 爬虫内核与 TLS ALPN 协商短板

很多人有个固有认知:HTTP/2 和 HTTP/3 建立在现代传输层之上,客户端不支持就会自然降级到 HTTP/1.1,服务端照单全收即可,怎么可能会把蜘蛛拒之门外?

理想很丰满,但现实是国内主流搜索引擎的抓取节点集群历史包袱沉重。

Googlebot 早在多年前就宣布全面支持 HTTP/2 抓取,但百度蜘蛛(Baiduspider)庞大的分布式爬虫节点中,有很大一部分旧节点依然运行在陈旧的 HTTP/1.1 客户端底层上,其 TLS 握手组件甚至不支持现代的 ALPN(Application-Layer Protocol Negotiation,应用层协议协商)扩展。

HTTPS 建立连接时,浏览器会在 TLS ClientHello 报文的扩展字段里塞入 ALPN 列表,明确告知服务端:“我支持 h2, http/1.1”。服务端看到之后,会选定 h2 完成协商。

但当老旧的爬虫节点发起请求时,它发送的 ClientHello 根本没有 ALPN 扩展字段,或者只带了陈旧的 SSL 扩展。如果你的 Nginx 在编译时链接了较新的 OpenSSL/BoringSSL,且配置了较为苛刻的密码套件(Cipher Suites)或 TLS 1.3 强制规则,服务端在处理这种没有 ALPN 的请求时,某些中间代理层或者特定的握手逻辑会判定协议协商未达成一致,直接向客户端甩出一个 TLS Handshake Alert 或者关闭连接。

要在服务器上验证这个问题,不需要去猜,用 OpenSSL 命令行就能模拟爬虫的协商行为:

# 模拟支持 h2 的现代客户端协商
openssl s_client -connect yourdomain.com:443 -alpn h2,http/1.1 -servername yourdomain.com

# 模拟只支持 HTTP/1.1 的陈旧爬虫协商
openssl s_client -connect yourdomain.com:443 -alpn http/1.1 -servername yourdomain.com

# 模拟完全不发送 ALPN 扩展的裸 ClientHello 请求
openssl s_client -connect yourdomain.com:443 -servername yourdomain.com

当时我在终端执行第三条命令,抓包立刻暴露出问题:服务器中间层在握手未声明应用层协议时,虽然回了证书,但在接下来的协议切换逻辑中触发了异常状态,直接中断了后续的 TCP 流。

百度爬虫在遇到这种情况时,不会像真实浏览器那样反复重试并智能容错,只要几次探测超时,它的调度系统就会自动给你的站点贴上“服务器不可达”的标签,随后断崖式降低抓取配额。


陷阱二:升级 Nginx 1.25+ 踩坑:listen http2 废弃与多虚拟主机套接字污染

如果你最近几年升级过 Nginx,肯定注意到官方在 1.25.1 版本引入了一个配置结构的大变动。

在老版本 Nginx 里,开启 HTTP/2 非常直观,直接在监听指令后面加个参数就行:

listen 443 ssl http2;

从 1.25.1 开始,http2 参数被标记为 deprecated(已废弃),继续使用会在 reload 时报警告。官方推荐把协议开关独立出来:

listen 443 ssl;
http2 on;

看起来只是语法拆分,但在多虚拟主机(vhost)的服务器上,这个改动藏着致命的套接字级别(socket-level)继承陷阱。

Nginx 底层监听端口是按 IP:Port 绑定的。当你在其中某一个虚拟主机里配置了 http2 on;,这个端口的底层套接字就会被激活为支持 HTTP/2 模式。

很多站长为了安全,会配一个默认的兜底虚拟主机(default_server)用来阻断未绑定域名的直接访问。如果你在主站的 server 块配了 http2 on;,但兜底的 default_server 还是旧语法,或者漏掉了相应配置,就会出现端口状态割裂。

更为坑爹的是,如果有多个业务站点共用同一个 443 端口,其中一个站点没写 http2 on;,当百度爬虫通过 SNI 访问该域名时,Nginx 在选择虚拟主机后会尝试在已经处于 HTTP/2 预备状态的套接字上强行解析 HTTP/1.1 纯文本流,极易诱发 ERR_HTTP2_PROTOCOL_ERROR 或 400 Bad Request 错误。

排查 Nginx 错误日志(error.log),如果看到类似下面的报错,几乎就是套接字污染引发的血案:

[error] 18432#18432: *49102 client sent invalid request while reading client request line, client: 220.181.108.77
[info] 18432#18432: *49103 client sent invalid HTTP/1.1 request on HTTP/2 connection

看到日志里 client: 220.181.108.xx(典型的百度爬虫 IP 段)在连接上报出协议冲突,我就知道爬虫把 HTTP/1.1 请求怼进了没有正确重置的协议上下文中。


陷阱三:多路复用变自杀工具?并发流打满与 http2_max_concurrent_streams 引发的静默 RST

HTTP/2 最大的卖点就是单 TCP 连接上的多路复用(Multiplexing),多个请求和响应可以并发跑在不同的 Stream 里,彻底告别队头阻塞。

对浏览器用户来说这是天大的好事。但对搜索引擎爬虫来说,多路复用却成了一把双刃剑。

大型爬虫调度器为了追求吞吐率,会尽可能复用已建立的 TCP 长连接,在同一个连接里一口气甩出几十甚至上百个页面的抓取请求。

Nginx 对单连接内的并发 Stream 数量有严格限制,控制这个行为的指令是 http2_max_concurrent_streams,默认值通常是 128:

http2_max_concurrent_streams 128;

如果爬虫瞬间打过来的并发请求超过了这个上限,或者后端应用(如 PHP-FPM、Node.js 或数据库查询)在某几个耗时页面上卡住了十几毫秒,未完成的 Stream 就会在连接中堆积。

一旦并发 Stream 超过限制,Nginx 不会友好地排队等待,而是会按照 HTTP/2 协议规范,直接向客户端发送 RST_STREAM 帧(流重置),错误码通常为 REFUSED_STREAM 或 CANCEL。

FRAME: RST_STREAM (stream_id=135, error_code=REFUSED_STREAM)

不仅如此,如果连接空闲稍久,Nginx 默认的 http2_idle_timeout(通常 3 分钟)一旦超时,或者读写超时(http2_recv_timeout)触发,Nginx 就会发送 GOAWAY 帧并粗暴关闭 TCP 连接。

现代浏览器收到这些状态帧懂得无缝切换重发,但百度爬虫的底层抓取进程对 HTTP/2 Stream 级异常的处理极为脆弱。一旦收到了 RST 帧或者连接被中间重置,爬虫进程往往直接记录抓取失败,既不重发也不fallback,导致整个抓取批次全部告吹。


陷阱四:严格小写伪头规范与老旧后端中间件的反代崩溃

排查过程中还发现了一个藏在应用层的次生灾害。

HTTP/1.1 的请求头规范是不区分大小写的,大家平时不管是写 Host、host 还是 HOST,网关和后端都能无感解析。许多历史遗留的 PHP、Python 或者 Java 老系统,在提取请求头时都硬编码了首字母大写的键名。

然而到了 HTTP/2,RFC 7540 协议标准做出了极其苛刻的限制:所有传输的 Header 必须统一转换为全小写字母,同时引入了以冒号开头的伪头(Pseudo-headers),比如 :method、:path、:scheme、:authority。

在纯 Nginx 处理静态资源的场景下这不会出事,但如果你的架构中间套了反向代理,或者用特定模块重写了 Header:

location / {
    proxy_pass http://upstream_backend;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
}

当爬虫用不同协议访问时,如果经过了特定的 Lua 脚本处理或者中间件转换,部分网关模块在解析 HTTP/2 的 :authority 与 HTTP/1.1 的 Host 映射时,在特定边界条件下会导致后端应用获取不到合法的站点域名。

后端框架拿不到正确的域名,直接回了一个 400 Bad Request 或者 500 服务器内部错误。而在站长工具里,你看到的只是爬虫返回了非 200 状态码,根本联想不到是 HTTP/2 头部规范在作祟。


陷阱五:鱼和熊掌兼得:Nginx 优雅分流、日志观测与蜘蛛平滑降级方案

搞清楚这一整套链条的问题根源后,我不可能因为爬虫兼容性就把整个站点的 HTTP/2 和 HTTP/3 回退掉,毕竟现代浏览器用户占比超过 95%,速度体验和 Core Web Vitals 是排名的基石。

解决的核心思路只有一条:让支持现代特性的客户端享受 HTTP/2 与 HTTP/3,同时确保没有 ALPN 或只支持 HTTP/1.1 的搜索引擎爬虫能丝滑降级,绝不触发协议重置与流阻断。

1. 在日志中单独记录协议版本,告别抓瞎

要治理问题,第一步是看清楚每个请求到底走了什么协议。在 Nginx 日志格式中把 $server_protocol 打进去:

log_format main_with_proto '$remote_addr - $remote_user [$time_local] "$request" '
                           '$status $body_bytes_sent "$http_referer" '
                           '"$http_user_agent" "$server_protocol"';

改完配置 reload 之后,跑一条命令统计百度蜘蛛的实际协议分布:

tail -n 10000 /var/log/nginx/access.log | grep -i "Baiduspider" | awk '{print $NF}' | sort | uniq -c

改动之前,我的日志里全是乱糟糟的异常断开;治理之后,你会清楚看到百度蜘蛛基本稳定运行在 "HTTP/1.1",而 Googlebot 和正常访客则绝大多数跑在 "HTTP/2.0",泾渭分明。

2. 规范 Nginx 1.25+ 全局套接字配置

消除虚拟主机之间的套接字污染,兜底虚拟主机与主站虚拟主机必须保持一致的协议定义,杜绝语法混用:

# 兜底默认虚拟主机,阻断野域名
server {
    listen 80 default_server;
    listen 443 ssl default_server;
    server_name _;

    ssl_certificate /etc/nginx/ssl/dummy.crt;
    ssl_certificate_key /etc/nginx/ssl/dummy.key;

    # 规范声明支持 http2,避免套接字状态不一致
    http2 on;

    return 444;
}

# 核心业务虚拟主机
server {
    listen 443 ssl;
    server_name yourdomain.com www.yourdomain.com;

    http2 on;

    ssl_certificate /etc/nginx/ssl/yourdomain.crt;
    ssl_certificate_key /etc/nginx/ssl/yourdomain.key;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;

    # 放宽并发流限制与超时窗口,防止蜘蛛突发请求被打满
    http2_max_concurrent_streams 256;
    http2_recv_timeout 60s;
    http2_idle_timeout 180s;

    # 保持长连接池健康
    keepalive_timeout 75s;
    keepalive_requests 1000;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }
}

3. 排查 CDN / WAF 边缘节点的协议回源配置

如果你的网站前面套了 Cloudflare、腾讯云 EdgeOne 或者阿里云 CDN,问题很可能不在源站,而是在 CDN 边缘节点。

很多 CDN 厂商的控制台里有“HTTP/2 访问”和“HTTP/2 回源”两个独立选项。有的控制台开启了边缘 HTTP/2,但对爬虫的握手没有做自动嗅探降级,直接把前端连接当成了强制 h2。

在 CDN 控制台检查这几项配置:

  1. 确保客户端到 CDN 节点的协议协商开启了自动降级,允许无 ALPN 客户端使用 HTTP/1.1。
  2. CDN 回源协议建议固定为 HTTP/1.1,除非源站是微服务集群且明确配置了支持 h2c。回源走 HTTP/1.1 连接池不仅极其成熟稳定,还能彻底避免源站 Nginx 与 CDN 节点之间的 HTTP/2 握手摩擦。
  3. 检查 WAF 规则,确保没有把未带 ALPN 扩展的握手请求当成“异常扫描器”直接阻断。

调整完上述这套架构后,连续观察了一周的日志。百度蜘蛛的抓取状态码全部回到了绿色的 200,抓取耗时稳定在 100 毫秒以内。随后的半个月里,百度搜索资源平台的抓取异常彻底归零,新文章发布当天就能完成收录,之前的断崖式暴跌曲线终于被彻底抚平。

评论一下?

OωO
取消