先给一个有条件成立的结论:当同一域名在不同网络路径下返回的页面版本不一致时,问题通常不在源站,而在某一层缓存对“什么算同一个资源”的判断与你不同。这个结论成立的前提是你能确认源站只输出一个版本,且各层缓存的键值规则可以分别观察。如果源站本身就按请求头或地域输出多版本,那么后面所有缓存排查都是白做,必须先把源站收敛成单一输出,再谈缓存一致性。
多层缓存的一致性排查有一个常被跳过的前置条件:源站必须是确定性的。很多站点在域名层做了地域跳转、按 Accept-Language 返回不同语言、或对移动端 UA 返回简化页,这些都属于源站主动多版本,不是缓存故障。
验证方法是绕开所有中间层直接请求源站,并固定请求头。假设你用同一路径、同一 UA、同一 Accept-Language 连续请求源站三次,得到完全相同的响应体与响应头,才能进入下一步。如果三次结果不同,先解决源站的输出不稳定,例如后端多台机器配置不一致、灰度发布残留、或模板缓存未刷新。此时去查 CDN 只会得到互相矛盾的结论。
这一步的实际动作是记录源站响应的 ETag 或 Last-Modified,作为后续比对的基准值。基准值不稳定,后面的比对就失去参照。
“多层”通常指浏览器缓存、CDN 边缘节点、CDN 中间层、反向代理或应用缓存。它们在响应头里留下的痕迹不同,能帮你判断不一致发生在哪一跳。
Age:数值明显不同,说明命中的是不同边缘节点或不同缓存时长。X-Cache、CF-Cache-Status 一类自定义头:能区分 HIT、MISS、EXPIRED,但字段名和取值因服务商而异,需以实际响应为准。Vary:如果 Vary 包含 UA 或 Accept-Encoding,不同客户端会天然落到不同缓存对象,这不是故障而是设计。Cache-Control 与 s-maxage:共享缓存与浏览器缓存的有效期不一致时,同一时刻不同用户看到的版本就会不同。把每个节点的响应头并排记录,比反复刷新页面更能定位问题。刷新本身会改变缓存命中状态,反而掩盖差异。
返回不同版本有两种成因,处理方式完全相反。
第一种是缓存键不同。请求被分到了不同的缓存对象,各自存了不同时间抓取的副本。典型证据是同一 URL 在不同节点返回不同的 Age 和 ETag,但每个节点内部是自洽的。此时需要统一缓存键规则,例如规范查询参数、统一 Vary 维度。
第二种是内容过期不同步。缓存键相同,但各层刷新时间不一致,旧副本还没被淘汰。证据是同一节点短时间内先返回旧版本再返回新版本,且 ETag 变化。此时需要主动清理或缩短 TTL,而不是改缓存键。
一个反例会让上面的判断失效:如果源站在发布时只更新了部分后端实例,那么即使缓存键和 TTL 都正确,各层抓到的“源站版本”本身就不同。这种情况下缓存层是无辜的,问题回到发布流程。判断依据是源站多实例响应不一致,而不是缓存头异常。
定位一致性问题的实际动作是构造一组可重复的请求,并记录响应。假设你固定同一 URL、同一 UA、同一 Accept-Encoding,分别从直连源站、CDN 边缘、以及带缓存穿透参数的请求三条路径取样,比较 ETag、Age、Vary 和响应体哈希。如果直连源站一致而 CDN 路径不一致,问题在 CDN 层;如果三条路径都不同,先怀疑源站。
这个动作的结果直接决定下一步:源站不一致就修发布;缓存键不一致就规范键;TTL 不一致就调整过期策略。不要在没分清层次之前就批量清缓存,那只会让问题暂时消失又复现。
不同缓存服务商对 Vary、stale-while-revalidate、以及自定义缓存键的支持程度不同,必须分别核查实际行为,不能假设一套配置在所有层都等效。另外,robots.txt 的抓取限制不等于可靠的索引移除,它和缓存一致性是两件事,不要混在一起排查。站点地图不保证收录,HTTPS 也不保证安全无漏洞或排名,这些都不构成缓存版本不一致的解释。
最后注意:某次请求返回旧版本,不能单独证明缓存处理有误,它也可能是源站发布延迟、DNS 解析到不同机房、或客户端本地缓存未过期。把这些可能性逐一排除后剩下的那一层,才是真正要动手的地方。下一步动作是先固定源站输出并记录基准 ETag,再逐层比对响应头,直到找出第一个与基准不一致的节点。