先给结论:不要从日志里直接找“哪个版本对”,而是把同一个URL在每一层缓存的返回结果分别固定下来,再比对差异出现在哪一层。做法是选一个你已经知道存在版本不一致的页面,分别记录源站、CDN边缘节点、中间代理和日志中爬虫实际收到的响应特征,然后逐层排除。样本少时看不出规律,规模化后例外变多,往往不是缓存“坏了”,而是不同层的键规则、刷新时机或回源策略不一致。
多层缓存下,同一个URL可能同时存在多个副本:源站应用缓存、反向代理缓存、CDN边缘缓存,以及浏览器或爬虫本地缓存。每一层都有自己的缓存键和过期规则。爬虫日志里记录的是它最终拿到的响应,但这个响应经过了哪些层,日志本身通常不会完整告诉你。
实际操作时,先做一次分层取样:对同一个URL,用带唯一查询参数的请求打源站,确认源站当前输出;再请求CDN边缘地址,看返回是否与源站一致;最后对照爬虫日志中同一时间窗口的抓取记录。如果源站和边缘一致、日志不一致,问题更可能在日志采集或爬虫侧的缓存;如果源站和边缘就已经不同,问题在回源或缓存键配置。
这一步的结果决定下一步:差异只出现在某一层,就集中查那一层的缓存键和刷新策略;差异跨层出现,说明至少有一个共享的键或回源规则在起作用。
缓存键通常由URL、查询参数、请求头中的部分字段共同决定。如果缓存键没有包含某个实际影响输出的请求头,而源站又根据这个头返回不同内容,就会出现同URL不同版本。常见的相关头包括Accept-Encoding、User-Agent、Accept-Language,以及某些情况下用于区分移动端和桌面端的自定义头。
检查方法是:对同一个URL,只改变一个请求头,观察响应是否变化。如果响应变化,但缓存键不包含这个头,那么缓存层会把两个版本当成同一个资源,谁先被缓存,后续请求就可能拿到谁的版本。反过来,如果缓存键包含了一个爬虫每次都不同的头,缓存命中率会下降,回源次数增加,日志里看起来像“抓取量异常”,但根因是缓存碎片化,不是抓取问题。
Vary头告诉缓存层哪些请求头会影响响应。如果源站根据User-Agent返回不同内容,但响应里没有正确声明Vary: User-Agent,中间缓存就可能把桌面版返回给移动爬虫。这是定位一致性时优先检查的一项。
日志里的时间戳、源站响应时间和边缘缓存时间可能不在同一时区或同一精度上。直接按时间排序容易得出错误结论。更可靠的做法是给每次测试请求加一个唯一标识,比如临时查询参数或自定义响应头,然后记录这个标识在源站、边缘和日志中是否出现。
假设你给一个URL加上?probe=abc123,源站返回的响应头里包含这个值,边缘缓存也缓存了这个响应。如果爬虫日志中后续抓取没有这个参数,却拿到了带该参数的缓存副本,说明缓存键忽略了查询参数,或者缓存把带参数的响应错误地匹配到了不带参数的请求上。这个结果会直接改变下一步:需要修正缓存键规则,而不是继续调整爬虫频率。
如果日志中同一URL在短时间内出现多个不同内容长度或不同状态码,先不要判断为“爬虫看到多个版本”,而要确认日志是否合并了多个边缘节点的记录。多节点缓存刷新不同步时,同一URL在不同节点上返回不同版本是正常现象,规模化后尤其明显。此时要定位的是节点间同步延迟,而不是源站输出错误。
单个样本能复现,不代表规则可以照搬。样本成立通常依赖几个隐含条件:该URL的缓存键固定、该边缘节点已刷新、该时间窗口内没有并发回源。规模化后,这些条件很难同时满足。例外变多往往来自三个方向:缓存键在不同URL路径上配置不一致;部分节点刷新失败或延迟;源站对某些请求返回了不可缓存的响应,导致缓存层跳过或降级处理。
可以按下面顺序做一次收敛:
Vary,与正常URL对比。手动刷新后的结果很关键:如果刷新后一致性问题消失,说明是刷新时机或同步延迟;如果刷新后仍然出现不同版本,说明缓存键或源站输出本身存在分支,需要回到上一层继续查。
定位清楚后,处理动作要对应到具体层,而不是笼统地“清缓存”。如果问题在缓存键,就修正键规则,让影响输出的请求头进入键或通过Vary声明;如果问题在刷新同步,就调整刷新顺序或等待窗口,确认所有边缘节点完成后再观察日志;如果问题在源站输出不稳定,就先固定源站行为,再处理缓存。
处理完成后,验证方式也要分层:源站请求确认输出稳定,边缘请求确认各节点返回一致,最后再看抓取日志中同一URL的响应是否收敛。如果日志中仍出现不同版本,但源站和边缘已经一致,那么剩余差异更可能来自日志采集端或爬虫本地缓存,而不是你刚处理的那一层。这个判断会决定你是否需要继续投入,还是把注意力转到日志管道本身。