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

为了降低闲置服务器成本把全站迁移到 Serverless 架构(函数计算 / FaaS SSR),搜索收录与关键词排名却断崖式暴跌?排查冷启动延迟飙升致蜘蛛抓取频报 504、突发并发打爆配额返回 429、RDS 数据库连接池瞬时击穿与预留并发及边缘缓存降级我踩过的几个深坑

2026-11-1 / 0 评论 / 12 阅读
AI 摘要由 AI 生成

文章讲述了将网站从SSR架构迁移至Serverless架构后,由于冷启动延迟导致搜索引擎抓取量骤降和关键词排名下降的问题。问题根源在于Serverless架构与搜索引擎爬虫机制的冲突,具体表现为长尾页面冷启动延迟造成爬虫抓取失败和并发配额被打爆。

去年底为了缩减基建账单,技术团队把原本跑在 4 台高配 ECS 上的 Next.js SSR 渲染集群整个迁到了云函数(Serverless / FaaS 架构)。

当时的账算得很漂亮:原集群平时 CPU 占用常年趴在 5% 以下,只有早晚高峰有点动静,每月几千块的固定服务器开销纯属闲置浪费。换成函数计算按量计费后,账单直接砍掉了七成多,前两周业务监控各项指标也都风平浪静。

然而到了第二个月中旬,运营同事突然跑来预警:百度搜索资源平台和 Google Search Console 上的抓取监控曲线全线跳水,原本每天上万次的正常抓取跌到了几百次,有效收录索引量直接腰斩。更严重的是,几个长期霸占搜索前排的核心词排名一夜之间全掉出了前三页。

调出边缘网关与云函数的调用日志,我把那两周爬虫访问链路的每一段链路状态码都翻了一遍,才发现这次所谓“降本增效”的技术重构,精准踩中了 Serverless 架构与搜索引擎爬虫机制之间最致命的几道冲突暗礁。


坑一:长尾页面冷启动延迟拉满,爬虫抓取频现 504 与连接重置

搜索引擎蜘蛛抓取和人类日常浏览最大的不同,在于访问请求的分布极度离散。

正常用户大都集中在热门文章、最新发布或者固定栏目,函数实例处于温热状态,调用耗时通常在两三百毫秒以内。但爬虫为了建全网索引,调度引擎会批量扫描几个月甚至几年前沉淀下来的冷门长尾页面。

函数计算平台的缩容机制非常激进:一个实例连续 5 到 10 分钟没有外部流量,容器就会被直接销毁回收。

当百度蜘蛛一口气调度几百个冷门页面时,每一个请求几乎都会命中一次冷启动:

  1. 底层调度器分派沙箱宿主机;
  2. 镜像拉取与轻量容器初始化;
  3. Node.js 运行时环境启动;
  4. 庞大的 Next.js SSR 服务端 bundle 包加载解析与内存水合;
  5. 首次数据库只读连接建立。

整套冷启动链路跑下来,最快也要 3.5 秒,慢的甚至拖到 7 秒开外:

# 云函数调用耗时监控日志(冷启动片段)
2026-06-12T03:14:22.108Z [FC_INIT] Container cold start begin: image_pull=1420ms, runtime_init=890ms
2026-06-12T03:14:24.418Z [FC_EXEC] Next.js server bundle loaded: memory=412MB, duration=1820ms
2026-06-12T03:14:26.930Z [FC_EXEC] SSR render completed: page=/article/detail-4192.html, total_duration=4822ms

Baiduspider 这类蜘蛛抓取引擎为了保障全网抓取吞吐,客户端设置的 HTTP 超时阈值极其严苛(通常在 3 到 5 秒之间)。一旦服务端在规定窗口内没有吐出完整首包响应,爬虫客户端便会主动掐断 TCP 连接。

前置的 API 网关在等不到函数回包时,直接向蜘蛛扔回 504 Gateway Timeout:

# Nginx 接入层抓取日志片段
220.181.108.95 - - [2026-06-12T03:14:25+08:00] "GET /article/detail-4192.html HTTP/1.1" 504 584 "-" "Baiduspider+(+https://www.baidu.com/search/spider.htm)" 5.002
220.181.108.77 - - [2026-06-12T03:14:29+08:00] "GET /article/detail-3881.html HTTP/1.1" 504 584 "-" "Baiduspider+(+https://www.baidu.com/search/spider.htm)" 5.001

在百度和谷歌的搜索引擎健康度模型里,单日内持续出现大比例 504 属于致命故障,表明目标源站已不可用或承载力枯竭。爬虫调度器会自动启动“过载避让”,成倍削减对该域名的每日抓取配额,大量本该更新快照的文章直接被移出活跃抓取队列。


坑二:突发脉冲式抓取打穿并发配额,网关大批量抛出 429

第二个隐蔽深坑出在并发限制与脉冲流量的错配上。

为了防止恶意刷量或者死循环调用把账单打爆,云函数在创建时通常会配置一个“函数最大并发实例数(Function Concurrency Limit)”,比如默认给配了 60 或 100。

平峰时期网站同时在线几十个用户,这些并发额度绰绰有余。但搜索引擎爬虫从不按常理出牌。为了降低对正常业务白天的网络干扰,蜘蛛很喜欢在凌晨两点到清晨六点之间,突然对网站发起脉冲式高密抓取。

凌晨三点多,数十个爬虫抓取线程同时涌入,几十个冷启动实例由于启动耗时较长(坑一),导致单个请求挂在运行中的时间变长。旧请求还没来得及释放,新请求源源不断涌来,函数并发实例池在几秒钟之内被瞬间占满。

当并发数撞上设定的硬阈值后,API 网关不再将请求派发给底层引擎,而是直接拦截并返回 429 Too Many Requests:

{
  "code": 429,
  "message": "ResourceExhausted: Function concurrency limit reached, please try again later."
}

翻看网关日志统计,在连续四天的清晨抓取高峰里,爬虫收到的响应状态分布触目惊心:200 成功率只有不到 35%,剩余请求被 504 和 429 瓜分殆尽。

对于搜索爬虫而言,429 意味着源站已明确拒绝服务。Googlebot 在连续收到 429 时会将抓取降速周期拉长数周,而百度爬虫则会迅速把出现 429 的长尾页面判定为不可用链接,原本积累多年的老页面权重被直接剥离。


坑三:无状态容器各自建连,数百微实例瞬间榨干 MySQL 连接池

在传统常驻服务器模式下,单台 ECS 部署一个 Node.js 进程,代码里配一个大小为 10 到 15 的连接池,就能通过事件循环与连接复用轻松支撑成百上千的并发请求。全集群 4 台机器加起来,占用数据库物理连接不过几十个。

但 Serverless 模式的底层执行单元是相互隔离的独立微容器。

每个函数实例都是一个独立的微型沙箱,内存完全不共享。当 Next.js 代码里执行到数据库查询时,每个实例都会在冷启动期间试图去连一次 RDS:

// 迁移前未做改造的数据库连接模块
import mysql from 'mysql2/promise';

// 每个独立的 Serverless 容器冷启动都会执行一次连接池创建
export const db = mysql.createPool({
  host: process.env.DB_HOST,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_NAME,
  connectionLimit: 5, // 每个实例分配 5 个连接
});

当蜘蛛瞬间打出上百个并发冷启动时,底层迅速弹起了 80 到 100 个容器实例。每个实例就算只申请 3 个空闲连接,总量也会瞬间飙升到三四百个。

当时我们 RDS MySQL 购买的规格默认最大连接数(max_connections)只有 250。

结果可想而知:连接数瞬间触顶打满,数据库疯狂报错 ERROR 1040 (08004): Too many connections。

后续的函数实例不仅无法连接数据库,连正常用户的结账与登录接口也跟着全部连环暴毙:

2026-06-14T03:22:15.892Z Error: connect ECONNREFUSED
    at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1494:16)
    --------------------
    code: 'ER_CON_COUNT_ERROR',
    errno: 1040,
    sqlState: '08004',
    sqlMessage: 'Too many connections'

数据库被打崩后,Next.js 服务端渲染全部降级抛出 500 异常。搜索引擎蜘蛛连续几天抓回来的都是 500 错误页或者残缺的错误提示信息,直接引发了全站快照大面积死链标记。


坑四:网关透传标头缺失,Canonical 泄露云函数内部测试域名

这个坑极其隐蔽,但破坏力直接关系到域名权重。

把 Next.js 部署到函数计算后,云厂商的 API 网关或者触发器通常会分配一个默认的内部测试域名,例如:

https://my-blog-ssr.ap-southeast-1.fcapp.run

为了接入正式线上流量,我们在最外层的 Nginx 上配置了反向代理,把用户的 www.llbbs.cn 请求转发给云函数的 HTTP 触发器域名:

# 踩坑的初期 Nginx 反代配置
location / {
    proxy_pass https://my-blog-ssr.ap-southeast-1.fcapp.run;
    # 漏掉了透传真实客户端域名的核心标头
    proxy_ssl_server_name on;
}

由于缺少了 Host 和 X-Forwarded-Host 的显式传递,云函数内部收到的 HTTP 请求头中,Host 字段默认变成了云厂商的子域名 my-blog-ssr.ap-southeast-1.fcapp.run。

在 Next.js 的页面渲染模板中,前端工程师动态取了当前请求的 Host 来拼接 SEO 规范链接(Canonical Tag)与 OpenGraph 分享卡片:

// pages/_app.tsx 或 layout.tsx 里的规范网址逻辑
export async function getServerSideProps({ req }) {
  const host = req.headers['x-forwarded-host'] || req.headers.host;
  const canonicalUrl = `https://${host}${req.url}`;
  return { props: { canonicalUrl } };
}

因为网关丢掉了 x-forwarded-host,取出来的 canonicalUrl 赫然变成了:

<!-- 爬虫抓取到的 HTML 头部源码 -->
<link rel="canonical" href="https://my-blog-ssr.ap-southeast-1.fcapp.run/article/detail-4192.html" />

搜索引擎看到整站每个页面上都赫然写着把主规范地址指向外部测试域,爬虫逻辑立刻将 www.llbbs.cn 上的正文视作镜像搬运内容,把全部权重转移给了这个临时测试子域名。由于临时子域名后续又受到访问频次和证书限制,最终导致两头权重彻底落空。


彻底重构:解耦爬虫抓取与函数计算的系统级方案

摸清四处深坑的根源后,我们没有慌乱把架构切回昂贵的传统 ECS,而是重构了网络接入与运行时架构。

核心认知只有一条:Serverless 极度适合做按需计算,但绝不能让爬虫的突发流量直接穿透到函数计算实例上。

1. Nginx 边缘层接入 SWR(Stale-While-Revalidate)缓存,阻断爬虫直穿

爬虫只需要抓取确定性的 HTML 文本,根本不在乎页面是不是这一毫秒刚刚在服务器内存里新鲜出炉的。

我们在前置的 Nginx 代理层配置了微缓存体系,重点启用 proxy_cache 搭配 stale-while-revalidate 机制:

# /etc/nginx/conf.d/ssr_cache.conf

proxy_cache_path /data/nginx/cache levels=1:2 keys_zone=ssr_cache:100m max_size=10g inactive=7d use_temp_path=off;

server {
    listen 443 ssl http2;
    server_name www.llbbs.cn;

    location / {
        proxy_cache ssr_cache;
        proxy_cache_key $scheme$host$request_uri;

        # 针对 200 状态码缓存 1 小时
        proxy_cache_valid 200 1h;
        proxy_cache_valid 404 10m;

        # 核心:后端冷启动超时或报错时,直接使用旧缓存响应爬虫
        proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
        proxy_cache_background_update on;
        proxy_cache_lock on;
        proxy_cache_lock_timeout 5s;

        # 补全代理标头,锁死真实域名
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Host $host;

        proxy_ssl_server_name on;
        proxy_pass https://my-blog-ssr.ap-southeast-1.fcapp.run;

        add_header X-Cache-Status $upstream_cache_status;
    }
}

这套配置起到了定海神针般的作用:

  • proxy_cache_lock on:当冷启动突发时,只放行一个请求去唤醒底层函数,其余几十个并发请求排队等待,彻底杜绝瞬间打爆实例数;
  • proxy_cache_use_stale:只要缓存池里有该页面的旧版本快照,就算底层函数冷启动拖了 6 秒,Nginx 也能在 20 毫秒内秒级给蜘蛛返回旧快照,并在后台静默更新缓存。

爬虫抓取看到的始终是 200 OK,响应时间从数千毫秒压到了 50 毫秒以内,504 错误彻底归零。

2. 爬虫活跃窗口配置弹性预留实例(Provisioned Concurrency)

虽有边缘缓存护体,但冷门长尾页面首次落入缓存依然需要调用函数。

我们结合 Nginx 日志发现,Baiduspider 和 Googlebot 的集中排查期高度集中在每天凌晨 1:30 到 5:30 之间。

针对这个规律,我们在云函数后台配置了潮汐预留规则:

  • 白天非抓取窗口:预留实例数保留 1 到 2 个常驻热底,应对常规散客;
  • 凌晨 1:00 至 6:00:通过定时弹性策略自动把预留并发拉高到 15 个实例。

预留实例是常驻内存的,容器与运行时环境预先完成装载,冷启动时间被彻底抹平成 0。凌晨突发的蜘蛛抓取命中常驻热实例,不再触发实例池挤压,429 报错彻底消失。

3. 部署 RDS Proxy 数据库代理,收拢微实例连接风暴

针对 MySQL 连接池被击穿的问题,我们在 RDS 前方开启了云厂商自带的数据库连接池代理(RDS Proxy / 数据链路网关):

[80 个 Serverless 容器] ---> (高并发短连接) ---> [RDS Proxy 代理集群] ---> (复用 10 个长连接) ---> [MySQL 物理实例]

代码中的数据库连接串直接指向 Proxy 地址,由 Proxy 在中间层负责握手保持、连接池智能复用与事务路由。

即使底层突发扩展出 100 个函数容器,它们向 Proxy 建立的只是轻量级会话,Proxy 与后端 MySQL 之间始终保持在稳定的 10 到 15 个健康物理长连接上。数据库连接监控曲线彻底恢复平缓,连接数占用从 250 悬崖式下降到 20 以内,Too many connections 报错再未出现。

4. 规范 Canonical 绝对域名锁定

为了斩断临时测试域名外泄的隐患,我们在服务端代码中彻底弃用动态读取不可靠网关标头的做法,改为通过全局配置强制锁定官方主域名:

// 规范化 SEO Metadata 构造逻辑
const SITE_CANONICAL_DOMAIN = process.env.NEXT_PUBLIC_SITE_DOMAIN || 'https://www.llbbs.cn';

export function getCanonicalUrl(pathname: string): string {
  // 严格剥离 query 参数,只保留纯净路由路径
  const cleanPath = pathname.split('?')[0];
  return `${SITE_CANONICAL_DOMAIN}${cleanPath}`;
}

并在 Nginx 配置中补齐了 proxy_set_header X-Forwarded-Host $host。双重保险之下,输出的 HTML 快照中 Canonical 标签与 OpenGraph 地址 100% 锁死在 www.llbbs.cn 上,避免了权重被分散稀释。


改造效果与反思

系统级重构方案上线三周后,各平台抓取数据开始全面回血。

百度搜索资源平台的“抓取频次”曲线在第四天直接拉出陡峭上升线,每日成功抓取量反弹了 400% 以上,平均抓取耗时从 3800ms 暴跌至 45ms。此前因为 504 和 429 被打断收录的两万多篇长尾技术文章,在四周内陆续重新被编入索引库,核心词排名稳固回升到了事故前的排位。

这次踩坑经历给我们的技术教训非常深刻:

很多人误以为 Serverless 就是“免运维、全自动伸缩”,把传统 SSR 代码打个包扔上去就万事大吉。但技术选型从来没有免费的午餐。搜索引擎爬虫的高并发、低容忍、长尾离散与高频抓取特征,天然与 Serverless 实例的冷启动、并发硬限制和短生命周期无状态特性存在尖锐矛盾。

真正可靠的架构,绝不是把后端算力裸奔暴露在公网爬虫面前。用边缘缓存做缓冲盾牌,把热点与长尾抓取阻隔在毫秒级缓存层,让函数计算专注在真正需要动态计算的后端处理上,才能在守住服务器低成本红利的同时,稳稳接住搜索引擎的宝贵流量。

评论一下?

OωO
取消