先确认一件事:你看到的“不同版本”是同一份内容在CDN、反向代理、应用缓存和浏览器缓存之间不一致,还是抓取工具拿到了旧副本。定位顺序应从最靠近源站的一层开始,逐层比对响应头与正文指纹,而不是先清空所有缓存。清缓存只能暂时掩盖问题,无法告诉你哪一层写入了错误版本。
第一种做法是自底向上:先固定源站输出,再逐层向外核对。它适合源站仍在频繁发布、缓存命中率较高的站点,因为源站一旦漂移,外层缓存只会放大差异。第二种做法是自顶向下:先看边缘节点返回什么,再回源比对。它适合源站稳定、但边缘节点数量多或配置刚变更的情况,能快速判断问题是否集中在某一区域或某一类节点。
选择依据可以压缩成两个判断:如果同一URL在短时间内多次请求结果就不同,优先自底向上,先排除源站动态渲染或灰度发布;如果同一URL在固定区域稳定返回旧版本,而在其他区域正常,优先自顶向下,先查边缘缓存键与回源策略。两种做法都要保留对照请求,否则无法区分“缓存没更新”和“源站本来就返回了两个版本”。
选一个出现问题的收录网址,记录完整请求:主机名、路径、查询参数、请求头中的Accept-Encoding与User-Agent,以及是否带Cookie。用同一组条件分别请求边缘节点和源站,保存响应头中的Age、Cache-Control、ETag、Last-Modified和Vary,再对正文做哈希。哈希相同但响应头不同,说明内容一致、元数据不一致;哈希不同,才进入版本差异排查。
这一步的实际动作是建立一张对照表,而不是只看页面肉眼是否一样。假设某个页面在边缘返回的正文哈希为A,回源返回为B,且Age大于零,那么下一步应先确认源站是否在缓存有效期内改过内容。如果源站已改而边缘未过期,问题属于缓存刷新策略;如果源站本身在两次回源间就返回了A和B,问题属于源站或应用层,继续清边缘缓存没有意义。
多层缓存返回不同版本,常见原因不是“缓存坏了”,而是不同层用了不同的缓存键。检查边缘节点是否把查询参数、Cookie、语言头或设备类型纳入缓存键;检查反向代理是否按Vary指定的请求头区分副本;检查应用层是否根据登录态或灰度标记输出不同模板。三者只要有一层判断条件不同,就会让同一URL在不同请求下命中不同副本。
可操作的做法是构造最小差异请求:只改一个请求头或一个查询参数,观察正文哈希是否变化。如果改动Cookie后哈希变化,而页面本应对未登录用户一致,说明缓存键过宽或应用层错误地把状态写进了公共缓存。此时应调整缓存键或Vary,而不是简单缩短TTL。缩短TTL会让不一致窗口变短,但请求量上升后源站压力增加,问题仍会以更低概率出现。
例外情况是内容本身就应随地域或语言变化。这时不同版本是预期行为,需要确认的是各层是否对同一地域返回了同一版本,以及收录网址是否因参数不同被当成多个入口。处理方式应转向规范化与参数收敛,而不是追求所有请求返回同一份正文。
把边缘响应头与应用回源响应头按时间排列,重点看Date、Age、Last-Modified和ETag。如果边缘的Last-Modified早于源站当前版本,而Age尚未超过Cache-Control中的max-age,说明边缘仍在合法使用旧副本;如果Age已超过max-age却仍返回旧副本,说明刷新链路或缓存清除没有生效。如果ETag在两层之间不一致,即使正文相同,也可能导致条件请求反复回源或命中失败。
这里的动作是记录一次发布前后的时间线,而不是凭感觉判断“缓存没刷”。假设发布发生在T时刻,边缘在T+5分钟仍返回旧哈希,而回源已是新哈希,且max-age为10分钟,那么这属于正常缓存窗口;若T+30分钟仍如此,才需要检查清除指令是否覆盖了所有节点和所有缓存键变体。下一步应针对未更新的节点单独排查,而不是全站清缓存。
定位完成后,修改应指向唯一一层。若是缓存键过宽,收敛键或补充Vary;若是源站输出漂移,先修复发布流程或灰度开关;若是刷新链路遗漏节点,补齐清除范围并保留回执。每次修改后,用同一组基线请求复测,确认正文哈希、ETag和Age的关系符合预期,再观察一段时间内是否再次分叉。
需要提醒的是,抓取工具或监测脚本看到旧版本,不一定等于收录状态异常。它可能只是命中了尚未过期的边缘副本,也可能是请求头与真实抓取不同导致命中了另一份缓存。把缓存一致性问题和收录结果分开记录,才能避免把一次缓存滞后误判为索引层故障。先让同一请求在所有层返回可解释的版本,再讨论收录网址的表现,顺序不能颠倒。