先给一个有条件的结论:当同一 URL 在不同网络路径下返回的 HTML 版本不一致时,问题通常出在“缓存键”设计上,而不是源站内容本身。只有当你能用同一请求头、同一出口 IP、同一时间窗口复现差异,这个判断才成立;否则差异可能只是 CDN 边缘节点回源时间不同造成的短暂现象。下面给出可核对的证据链,帮你区分这两种解释。
多层缓存(浏览器缓存、CDN 边缘、反向代理、应用层对象缓存)叠加时,最容易误判的一步是:拿两次不同时刻、不同工具的抓取结果直接对比。浏览器会带上自己的 Cache-Control 行为,命令行工具默认不带 Cookie,两者本来就可能拿到不同响应。
可操作的动作是固定变量:用同一台机器、同一出口 IP,在短时间内连续请求同一 URL,只改变一个维度(比如加不加 Accept-Encoding,或带不带登录 Cookie)。如果差异只在某个维度下出现,说明缓存键里包含或遗漏了该维度对应的字段,问题定位到这一层即可,不必继续往下查源站。这一步的结果直接决定下一步:差异可复现就进入缓存键分析,不可复现则应先怀疑采集方法本身。
定位时最有价值的证据是响应头,而不是页面内容本身。重点看这几个字段的组合:
Age 明显大于 0,说明响应来自共享缓存而非源站。X-Cache、CF-Cache-Status、X-Served-By 一类字段能指出命中还是回源,以及命中的是哪个节点。Vary 字段告诉你缓存键包含了哪些请求头。如果它只写了 Accept-Encoding,而实际版本差异来自 User-Agent 或 Cookie,那不同版本互相覆盖就是必然结果。ETag 或 Last-Modified 在两个版本间是否相同。相同却内容不同,说明上游有节点忽略了校验;不同则说明确实是两份独立内容。把这几项按“请求 → 命中的缓存层 → 返回的校验标识”列成一张对照表,比反复刷新页面更能说明问题。
上面的结论在一种情况下会失效:源站本身对同一 URL 返回了不同内容,缓存只是忠实放大了这个差异。典型信号是 Age: 0、缓存状态显示回源,但两次回源拿到的 ETag 不同。这时把责任推给缓存层会浪费时间。
区分方法很简单:绕过所有缓存直接请求源站(例如通过内网地址或临时关闭 CDN),连续请求多次。如果源站自身就不稳定,先修源站;如果源站稳定而经过缓存后不稳定,才回到缓存键分析。这一步是整条证据链的分叉点。
假设一个站点同时服务桌面版和移动版 HTML,靠 User-Agent 区分。CDN 缓存键默认只按 URL 和 Accept-Encoding 计算,没有把 User-Agent 写进 Vary。那么第一个访问者(假设是移动端)触发回源后,移动版 HTML 被缓存;之后桌面端用户请求同一 URL,直接命中这份移动版缓存。
此时你看到的“不同版本”并不是缓存损坏,而是缓存键漏掉了区分版本的维度。验证动作:分别用桌面和移动 User-Agent 请求,观察返回内容与 Vary 字段是否匹配。如果 Vary 没有声明该维度,结论成立。修复方向是补全缓存键或改用 URL 区分,而不是清空全部缓存——清空只能短暂掩盖问题,下一次回源后同样会互相覆盖。
定位完成后,按以下顺序推进,每步都有明确的验证标准:
Vary 与缓存键配置,作为修复前基线。Vary,使其覆盖实际产生版本差异的请求头。Age 增长时内容不再变化。需要提醒的是,抓取量或某个节点命中率的变化不能单独证明修复正确,因为回源策略调整、流量波动都会带来类似现象;只有“固定请求维度下版本稳定”这一条才是可核对的判据。把这个判据写进验证清单,比依赖单次刷新结果更可靠。