多层缓存返回不同版本,通常不是“缓存坏了”,而是某一层把带查询参数、带 Cookie、带设备差异的响应当成了同一份资源。定位时先别清全部缓存,而是固定一个 URL 样本,逐层比对响应头和正文指纹,确认差异从哪一层开始出现。若差异只影响收录判断,先修缓存键和 Vary;若差异已经影响用户看到的价格、库存或登录状态,则应先回退到单一版本,再排查。
两种情况的处理顺序完全不同。内容分叉指源站本身对不同请求返回不同正文,例如移动端和桌面端模板不同;缓存键分叉指源站只返回一份正文,但缓存层因为键设计过粗,把 A 请求的响应给了 B 请求。判断依据是:绕过所有缓存直连源站,用同一组请求头重复请求。若源站响应就不同,问题在源站逻辑;若源站一致、经过缓存后不同,问题在缓存键或 Vary。
这里有一个容易误判的点:CDN 边缘节点、反向代理、应用内对象缓存可能各自保留旧副本,清掉其中一层后差异暂时消失,并不证明那一层就是根因。更可靠的做法是给每层响应加一个可观测标记,例如在响应头里写入节点标识和缓存状态,再用同一 URL 连续请求,观察哪个标记先变化。
如果源站会根据 User-Agent、Accept-Encoding 或 Cookie 返回不同正文,缓存层必须知道这些维度。此时要检查两件事:响应是否声明了足够的 Vary,以及缓存键是否把这些维度纳入。只声明 Vary 而缓存键仍按路径归一,仍可能串版本;只扩缓存键而不声明 Vary,中间共享缓存也可能误用。
实际动作可以这样设计:选一个会触发差异的 URL,分别用两组请求头请求,记录每层返回的正文长度、关键字段值和缓存命中状态。若第一层就命中同一份缓存,说明键过粗;若第一层分开、第二层合并,说明合并发生在第二层。这个结果直接决定下一步改哪一层的配置,而不是盲目刷新。
假设某商品页对未登录用户返回标准价,对特定地区用户返回含税价,源站通过 Cookie 区分。若 CDN 缓存键只按路径,未登录用户可能拿到含税价版本。此时把 Cookie 纳入缓存键会碎片化缓存,更稳妥的是让源站对未登录和已登录分别输出可缓存的独立 URL,或对个性化部分改用客户端获取。这个取舍的依据是:个性化维度越多,共享缓存的命中率越低,需要评估回源成本与一致性哪个优先。
如果直连源站多次请求正文完全一致,但经过缓存后出现不同版本,优先排查压缩与字符集。不同层可能对同一正文做 gzip、br 或不做压缩,导致字节层面不同但语义相同;也可能因 Content-Type 缺少字符集,让不同层按不同默认编码解析,中文或特殊符号出现乱码。这类差异不一定影响收录,但会让抓取端把同一页面当成不同内容。
动作上,先固定 Accept-Encoding 为单一值重复请求,确认差异是否随压缩方式变化;再检查各层是否改写了 Content-Length、ETag 或 Last-Modified。若 ETag 在每层都不同,条件请求可能永远无法命中,缓存层会反复回源,放大版本不一致的概率。此时统一 ETag 生成规则比继续加缓存层更有效。
确认差异层之后,修复顺序建议按影响面排序:
修复后不要只看一次请求结果。用同一 URL 在多个节点、多组请求头下重复取样,并记录缓存命中状态;若差异只在特定节点出现,说明配置下发未完全生效,下一步应核对配置同步而非继续改源站。需要说明的是,抓取量或某层命中数归零,并不能单独证明版本已经一致,它也可能来自请求减少、节点切换或统计口径变化,仍需以逐层响应比对为准。
移动端与桌面端返回不同模板、A/B 测试返回不同模块,属于有意分叉。此时目标不是消灭差异,而是让每个版本有稳定且可区分的 URL 或响应标识,避免缓存把版本随机分配。若无法拆分 URL,至少应保证同一用户在同一会话内看到同一版本,并让抓取端能通过响应头识别当前版本。对于 robots.txt 限制抓取、站点地图提交或 HTTPS 部署,都不能替代这里的一致性修复:抓取限制不等于索引移除,站点地图不保证收录,HTTPS 也不保证缓存层不会串版本。
最终判断标准很简单:同一请求在任意层、任意节点得到同一正文指纹,且不同请求的差异能被缓存键或 Vary 解释。达到这一点后,再去看收录表现才有意义。