CDN 缓存把旧页面喂给爬虫两周:边缘缓存头对收录更新的影响与配置
一、问题是怎么被发现的
今年 6 月初,我们接手一个做工业配件出口的独立站(Magento 架构,前面套了一层 Cloudflare 免费版)。客户 5 月 20 日完成了一次大改版:商品页的 SEO 标题全部重写、十几款主力商品调了价格、新增了 60 多个 SKU 页面。按以往经验,Google 对这种量级的站点,标题更新一般一周内就能在收录里反映出来。

结果到了第 10 天,用 site: 语法抽查了 30 个商品页,收录快照里的标题还是旧版。Google Search Console(GSC)里「网页索引编制」报告显示这些页面状态是「已编入索引」,但「上次抓取时间」大多停在 5 月 22 日前后——也就是说抓取确实发生了,抓到的却是旧内容,Google 自然认为页面没变,不值得重新收录。
真正定位到问题是在 6 月 3 日。我用 GSC 的「网址检查」功能对其中一个商品页点了「测试实际网址」,实时抓取返回的 HTML 里,<title> 和价格字段全是改版前的旧值。源站当时已经确认是新版内容,那么中间只有一个嫌疑人:CDN 边缘缓存。
结论先说:这不是 Google 的问题,是边缘节点把旧页面当成新页面,连续两周喂给了爬虫。
二、排查过程:从响应头到日志
2.1 curl 看缓存命中状态
排查缓存问题,curl -I 是第一工具。对同一个商品页连续请求两次:
# 环境要求:任意带 curl 的终端(Windows 10 自带 curl.exe 即可)
# 第一次请求,观察缓存命中标记与缓存头
curl -sI "https://www.example-parts.com/products/flange-bolt-m12" \
-H "User-Agent: Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"
# 第二次请求同样的 URL,对比 CF-Cache-Status 是否变为 HIT
curl -sI "https://www.example-parts.com/products/flange-bolt-m12"
两次响应头里的关键信息:
| 响应头 | 实际返回值 | 说明 |
|---|---|---|
| CF-Cache-Status | HIT | 边缘节点命中,内容来自缓存而非源站 |
| Cache-Control | public, max-age=604800, s-maxage=2592000 | HTML 被允许缓存 7 天,CDN 边缘存 30 天 |
| Last-Modified | 缺失 | 爬虫无法发起条件请求校验新旧 |
| ETag | 缺失 | 同上,连 304 协商缓存的基础都没有 |
| Age | 86423 | 这份缓存已经在边缘节点存活近 24 小时 |
问题很清楚了。max-age=604800 配在 HTML 上,意味着边缘节点最长 7 天才会回源取一次新内容;而源站响应里居然没有 Last-Modified 和 ETag,即使边缘缓存过期回源,也无法利用条件请求(Conditional Request)机制。更糟的是,Cloudflare 命中缓存时直接返回 200 加缓存体,Googlebot 拿到的永远是旧快照,连发起 If-Modified-Since 验证的机会都没有。
2.2 X-Cache 与 Nginx 场景的对照
我们另一批客户的站点是 Nginx proxy_cache 自建的边缘层,排查思路一样,只是命中的判断标记换成 X-Cache-Status:
# Nginx 自建缓存层的站点,检查 X-Cache-Status 头
# 需要在 nginx.conf 的 add_header 中暴露 $upstream_cache_status
curl -sI "https://shop.another-site.com/products/valve-actuator" | grep -iE "x-cache|cache-control|etag|last-modified"
返回里 X-Cache-Status: HIT,同样配着 proxy_cache_valid 200 7d,同样没有给 HTML 关闭缓存。两套架构,同一个病根。
2.3 GSC 实时抓取与日志的 304 占比
第二个证据来自服务器访问日志。我写了个简单的统计脚本,把 5 月 20 日到 6 月 3 日的日志里 Googlebot 的请求按状态码分组:
| 时间段 | Googlebot 请求总数 | 200 占比 | 304 占比 | 收录标题更新延迟 |
|---|---|---|---|---|
| 5 月 20 日 - 6 月 3 日(修复前) | 3,182 | 97.4% | 2.6% | 约 12 天 |
| 6 月 4 日 - 6 月 17 日(修复后) | 3,405 | 41.8% | 58.2% | 1 天以内 |
注意这是单站观测数据,样本只有这一个站点,不代表行业普遍水平。但方向很有说服力:修复前 97% 的爬虫请求都拿到 200 全量响应——全是从边缘缓存吐出来的旧 HTML;修复后 304 占比冲到 58%,说明爬虫开始大量用条件请求校验,页面没变化就省掉传输,页面变了(收到 200 或新校验结果)就立刻更新收录。
抓取配额(Crawl Budget)没有变多,但有效抓取的质量完全不同了。这也是网站优化里一个常被忽视的点:让爬虫拿到正确的新鲜内容,比单纯追求抓取频率重要得多。
三、原理剖析:抓取、缓存、索引之间的更新时延链路
搜索引擎更新一条收录,链路是「调度器决定抓取 → 抓取器请求页面 → 解析内容与旧快照比对 → 有实质变化才进入重索引队列」。这条链路里每一步都依赖一个前提:抓取器请求到的内容必须能代表源站的当前状态。
CDN 缓存介入后,这个前提被破坏了。梳理一遍完整链路:
sequenceDiagram
participant G as Googlebot
participant E as CDN 边缘节点
participant O as 源站
G->>E: GET /products/xxx
alt 缓存命中且未过期(max-age 内)
E-->>G: 200 + 旧 HTML(不回源)
else 缓存过期
E->>O: GET(带 If-Modified-Since/If-None-Match)
O-->>E: 304 或 200 + 新 HTML
E-->>G: 200(可能仍是 TTL 内的旧内容)
end
G->>G: 与索引快照比对(无变化则不重索引)
关键在第一个分支:只要 TTL 没过期,爬虫的请求根本到不了源站。HTML 的 max-age=604800 意味着在最长 7 天的窗口里,无论源站改了多少次内容,所有访问者——包括爬虫——拿到的都是同一份旧页面。改版日 5 月 20 日写入缓存的旧 HTML,会一直存活到 5 月 27 日左右;下一个 7 天窗口又是同样的循环。两周看不到更新,时间线完全对得上。
再看条件请求为什么失效。HTTP 缓存协商依赖 ETag(实体标签,Entity Tag)或 Last-Modified:爬虫第二次请求时带上 If-None-Match 或 If-Modified-Since,源站比对后如果内容没变就返回 304,只回头部不回正文。但这里有两个坑同时存在:
- 源站响应缺
ETag和Last-Modified,爬虫没有凭证可带,每次只能全量请求; - 边缘缓存命中时直接返回 200,校验动作被边缘层短路了,
Cache-Control: public又允许缓存器自作主张跳过回源验证。
这就是「缓存命中掩盖内容更新」的机制本质:缓存层替爬虫做了「内容没变」的判断,但它的判断依据是时间(TTL),不是内容本身。 TTL 是站长配的,一旦配得比内容更新周期长,爬虫的世界就比真实世界滞后了。
MDN 的 HTTP 缓存文档里对 stale-while-revalidate 的描述恰好给出了折中方案:允许先返回旧内容保证速度,同时异步回源刷新——对爬虫场景来说,这能把「旧内容窗口」压缩到秒级。
四、修复配置:HTML 短缓存 + 静态资源长缓存指纹化
修复思路是分层的:HTML 是收录的载体,必须新鲜;CSS/JS/图片变化频率低且带指纹,可以放心长缓存。两层策略分开配。
4.1 Nginx 源站配置
# 环境:Nginx 1.24,作为 Magento 源站的前置层
server {
listen 443 ssl;
server_name www.example-parts.com;
location / {
proxy_pass http://magento_backend;
# HTML 与动态路径:禁止源站输出长缓存头
# 交给 CDN 用 s-maxage 控制边缘短缓存
add_header Cache-Control "public, s-maxage=120, max-age=0, must-revalidate" always;
# 保留实体校验头,让条件请求(304)能够生效
# etag on 与 last_modified 系列是 nginx 默认值,显式写出防止被覆盖
etag on;
last_modified compiling;
}
# 静态资源:带内容指纹(版本号/哈希文件名),可放心长缓存
location ~* \.(js|css|png|jpg|jpeg|webp|woff2)$ {
proxy_pass http://magento_backend;
# 一年缓存 + immutable:浏览器和 CDN 都不回头验证
add_header Cache-Control "public, max-age=31536000, immutable" always;
}
}
这里有个容易踩的细节:s-maxage 只对共享缓存器(CDN、代理)生效,max-age=0, must-revalidate 约束的是浏览器端不要缓存 HTML。这样即使边缘出了问题,用户浏览器也不会本地留存旧页面。
4.2 Cloudflare 面板配置对照
Cloudflare 免费版默认不缓存 HTML(只缓存静态扩展名),我们的旧缓存头之所以生效,是前任运维加了一条「Cache Everything」页面规则,TTL 还被「Edge Cache TTL」设置顶到了 30 天。修改动作:
| 配置项 | 修复前 | 修复后 | 作用 |
|---|---|---|---|
| Cache Everything 页面规则 | 全站启用 | 移除,改用 Cache Rules | 避免无差别缓存 HTML |
| Edge Cache TTL | 30 天 | 遵循源站(Respect Existing Headers) | 边缘 TTL 跟随 s-maxage=120 |
| Browser Cache TTL | 4 小时 | 遵循源站 | 浏览器侧不再叠加缓存 |
| 源站 Cache-Control | max-age=604800 | s-maxage=120 + must-revalidate | HTML 两分钟内过期回源 |
| 静态资源规则 | 默认 | max-age=31536000 + 指纹文件名 | 长缓存不回源 |
改完后我又在 Cache Rules 里对 /products/* 显式加了一条:缓存状态可用但必须 stale-while-revalidate=60,即过期后允许先回旧内容撑 60 秒,同时后台回源刷新。爬虫的高频抓取由此几乎全部命中边缘,源站压力没涨,新鲜度却上来了。
另外建议保留两个验证手段:一是 Cloudflare 的 CF-Cache-Status 头改版后应当看到 MISS → HIT 的快速交替(两分钟 TTL 下正常流量就能观察到循环);二是 Nginx 场景下 $upstream_cache_status 配合 REVALIDATED 状态,能确认条件请求真的在工作。
五、修复后的缓存策略分层
把最终生效的策略画成一张分层图,方便对照检查自己的站点:
flowchart TD
A[用户 / 爬虫请求] --> B{请求的是 HTML 还是静态资源?}
B -->|HTML 动态页| C[边缘缓存 s-maxage=120]
B -->|JS/CSS/图片 指纹文件| D[边缘+浏览器长缓存 max-age 一年 immutable]
C --> E{缓存是否命中?}
E -->|未命中| F[回源带条件请求 If-None-Match]
E -->|命中| G[返回 200 缓存内容 Age 小于 120 秒]
F --> H{源站内容变了吗?}
H -->|没变| I[返回 304 边缘继续用旧缓存]
H -->|变了| J[返回 200 新 HTML 刷新边缘]
G --> K[TTL 过期后 stale-while-revalidate 异步刷新]
J --> L[爬虫解析新内容 进入重索引 收录更新]
修复一周后复查:GSC 里抽查的 30 个商品页,标题全部更新为新版;「上次抓取时间」集中在改版后的 24 小时内。源站回源请求量只上升了约 18%(从日志统计),因为静态资源的长缓存完全没动,只有 HTML 层多回了几次源。这笔账是划算的——收录更新延迟从约 12 天压缩到 1 天以内,对促销季频繁调价的独立站来说,这意味着价格页面的收录能跟上实际售价。
六、误区澄清与收尾
三个最常见的误区,写在最后提醒一下:
- 「CDN 缓存只影响用户,不影响 SEO」——错。Googlebot 和普通访客走的是同一条边缘链路,缓存喂给谁旧内容,收录就滞后谁的。
- 「把 HTML 缓存全部关掉最安全」——过犹不及。完全禁缓存会让抓取配额消耗在全量回源上,高频抓取反而变慢。短缓存加条件请求才是兼顾新鲜度与配额的做法。
- 「配了 Cache-Control 就完事了」——还要验证
ETag/Last-Modified真的输出去了,且中间层没有剥离它们。有些安全插件和旧的页面规则会顺手把这些头删掉,304 就永远触发不了。
Cache-Control 配置属于网站优化(SEO)里偏基础但收益稳定的工程项,它的价值不在单点排名,而在保证「源站改了什么,搜索引擎就能多快看到什么」。这个能力往后也是 AI 搜索场景的地基——生成式引擎优化(Generative Engine Optimization, GEO)同样依赖引擎能抓到最新页面,缓存头配错了,面向 AI 引擎做再多语义优化也是白搭。这一层先夯实,后面的路才好走。
如果你也在做外贸独立站,欢迎在评论区聊聊你们站点 HTML 的缓存策略是怎么配的,尤其是 Cloudflare 免费版用户的 Cache Rules 实践。
参考与延伸
- Cloudflare 官方文档:Cache Control(缓存控制头与 Edge Cache TTL 行为说明)—— https://developers.cloudflare.com/cache/concepts/cache-control/
- Nginx 官方文档:ngx_http_proxy_module 的 proxy_cache 指令(含 proxy_cache_valid 与 $upstream_cache_status)—— https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_cache
- MDN Web Docs:HTTP 缓存(Cache-Control、ETag、条件请求与 stale-while-revalidate 详解)—— https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Caching
SEO、网站优化、独立站自然流量、CDN 缓存、Cache-Control、ETag、收录更新延迟、条件请求