当 CDN 缓存不刷新时,如何强制更新 CDN 缓存

你刚部署了一项关键变更。源站服务器上已经是新文件了,但访客看到的仍然是旧版本。这正是 cdn cache not refreshing 的典型表现。解决方法是清除各个 CDN 边缘节点上的陈旧副本。这个过程会强制边缘节点重新回源抓取最新数据,而且通常几分钟内就能完成。
出现陈旧副本是 CDN 的正常行为,并不代表服务器坏了。每个 PoP 都会各自保留一份自己的缓存版本。某个节点可能已经更新,而另一个节点还在滞后。一次 purge 请求会很快传播到所有边缘节点,通常几分钟内就能解决这个问题。
这个清除过程不需要复杂配置。你只需发出一条 purge 命令,然后验证返回结果即可。
CDN 中的缓存失效是什么意思
缓存失效,是指通知缓存:它保存的副本已经过期、不再可用。放到 CDN 里,缓存失效就是把同样的逻辑应用到边缘服务器上。每个边缘服务器都持有自己的一份内容副本。失效操作会告诉这份边缘副本不要再继续提供陈旧数据。随后下一次请求就会从你的源站拉取更新后的版本。
可以把它想象成一份复印件。你修改了原始文档,但复印件上的文字还是旧的。复制件已经和源文件不一致了。你的边缘节点保存的就是这份“复印件”。它们会一直提供旧内容,直到有某种机制强制它们刷新。
为什么源站和边缘节点会不一致
你的 CDN 边缘缓存是独立于源站服务器的一层。当源站内容发生变化时,边缘缓存仍然可能保留着此前抓取到的旧副本。于是,源站显示的是更新后的文件,而边缘节点提供的却还是旧版本。除非缓存过期、被清除,或者被强制刷新,否则这种差异会一直存在。
在源站和用户之间,往往还存在多层缓存。非原子化部署也可能导致访客看到“部分更新”的状态。比如构建错误时,资源更新了但清单文件没更新,或者反过来。
Purge、Invalidation 和 Eviction 的区别
这三个术语描述的是三种不同动作。Purge 指手动移除某个缓存对象。Invalidation 指将某个对象标记为陈旧,使下一次请求重新拉取。Eviction 指当缓存空间不足,或生存时间到期时,系统自动将对象移除。
TTL,也就是 time-to-live(生存时间),本质上是一种基于时间的驱逐触发机制。它与基于空间的驱逐相互补充。缓存满了时,系统可能会在 TTL 尚未到期前,就先驱逐掉一些内容。一个条目一旦过期,就会变成 cache miss,从而强制回源抓取内容。
机制 | 对缓存条目的影响 |
|---|---|
基于容量的驱逐 | 移除较旧条目,为新内容腾出空间 |
TTL 到期 | 在 TTL 结束后移除该条目 |
理解 cache invalidation、eviction 和 purge 三者的区别,有助于你选择正确工具。Cache purging 是按需移除内容。Invalidation 是把内容标记为已过期。Eviction 则是自动发生的。
为什么会出现 CDN 缓存不刷新
每个被缓存的对象都会带有一个过期计时器,也就是 time-to-live(TTL)。你的源站通过 cache-control header 来设置这个计时器。TTL 如果很长,CDN 边缘节点就会持续数小时提供旧副本。你的 cdn cache 层会把响应存放在靠近访问者的位置。cdn cache not refreshing 这个现象,往往就是从这里开始的。
TTL、边缘节点与陈旧副本
一个 CDN 往往横跨数百个 PoP。每个 PoP 都会保存自己的缓存副本。某个节点可能会立刻抓取到新版本,而另一个节点却会在整个 TTL 周期内继续保留旧版本。这种差异正解释了为什么 cdn serves outdated content。
TTL 的衰减表现并不完全一致,因为不同 CDN 运营方会对这些计时器做不同调优。每个边缘节点也会在本地独立计算计时。有些节点会提前清除陈旧条目,有些则会一直保留到真正到期。这种不均匀的时序,会让不同地区混杂出现新旧版本。正因为如此,cdn caching issues 往往很难稳定复现。只要某个节点存活时间超过预期,cdn cache not refreshing 的模式就会再次出现。
为什么缓存失效这么难
业内一直把 cache invalidation 视为著名难题之一。不存在完美的 cache invalidation solution。从理论上说,也无法保证绝对“即时”的失效。陈旧副本会残留,cache invalidation 的结果也始终带有不确定性。这种时间上的不确定性,正是失效问题的核心。
而分布式 CDN 会进一步放大这个难度。Cache invalidation 需要让数百个边缘服务器同时删除内容。在删除传播过程中,正在进行中的请求仍可能抓到旧副本。大量回源重取还会触发 thundering herd(惊群)问题。重新抓取到的对象又可能引发下一轮失效波动。每一次 invalidation 请求都带有传播成本。要在成千上万个节点之间协调失效,本身就是问题的本质。Purge 的时效性,也同样带有这种 invalidation 的不确定性。
可用的缓存失效策略主要有三种:
等待 TTL 到期:缓存副本会在过期后自动刷新,但对紧急更新来说,这个办法并不适合。
发起显式 purge:通过 API 在所有边缘节点上删除对应条目。这个过程通常需要几秒到几分钟。
修改 URL:例如使用
logo-v2.jpg这样的新地址,从根本上绕过 invalidation。然后在 HTML 中更新所有引用。
长期存在的 cdn caching issues 往往需要双管齐下。对日常内容,要调优 ttl 和 cache-control headers。对热修复场景,则保留 cache purging。Invalidation 不是一个简单开关,而是一种权衡。你应该把 invalidation 当作一个明确的操作来对待。按需 purge 能带来控制力;但频繁 purge 则会浪费资源。合理的 cache invalidation 能帮助降低 源站流量。手动 cache invalidation 更适合在低流量时段进行。有效的 invalidation,核心是在内容新鲜度与成本之间取得平衡。
如何强制刷新内容
你可以采用多种方式来强制刷新内容。建议先从手动 purge 流程开始,然后再选择适合自己技术栈的方法。
按 URL 或标签清除 CDN 缓存
手动 purge 通常从服务商控制台开始。你打开缓存面板,输入要移除的文件路径,然后确认执行。之后边缘节点会丢弃这些对象,并在下一次请求到来时重新回源抓取。
Azure CDN 在其控制台中提供了 Purge 按钮。你可以输入像 /images/* 这样的通配路径,一次性清除整个目录。Cloudflare 既支持在控制台中手动 flush,也支持通过 purge api 实现自动化。你可以用一次 API 调用来 purge cdn cache by URL:
curl -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/purge_cache" \
-H "X-Auth-Email: $EMAIL" \
-H "X-Auth-Key: $APIKEY" \
-H "Content-Type: application/json" \
--data '{"files":["https://example.com/cf-logo-on-white-bg.svg"]}'请求完成后,文件状态会从 cf-cache-status: HIT 变为 cf-cache-status: MISS。这个变化说明你成功只清除了目标 URL。
可通过指定 URL 的方式,精细化地从 Cloudflare 缓存中移除一个或多个文件。所有套餐都支持按 URL 清除。
全局 purge 会一次性清空所有内容。它是最快的方式,但也是影响最大的一种。它会迫使所有边缘节点重新抓取全部内容,从而显著增加源站负载。相比之下,按 URL 或标签 purge 更精细。你只移除发生变化的内容,其余缓存保持不动。
版本化 URL、TTL 调整与 Surrogate Keys
版本化 URL 可以彻底绕开 invalidation。常见方式有两种:文件指纹和查询参数。文件指纹是把哈希写进文件名里,例如 app.9f3c2.js。查询参数则是在 URL 后附加版本号,例如 style.css?v=42。这两种方式都会让资源拥有一个新地址,因此不需要进行 cache purging。
Surrogate keys 则提供了另一条路径。它们通常是附加在缓存响应上的 HTTP headers,例如 x-surrogate-key,用于给内容打标签。内容更新时,你的后端无需按整个 URL 去 purge,而是可以针对特定 key 发起精确的 BAN 请求。缓存会把这些标签保存在内存中,并在所有相关路由上即时使对应缓存对象失效,而不需要刷新整个缓存。这种方式尤其适合你在配置 CDN 边缘缓存以进行精准失效时,以及在高流量、读密集型 API 场景下做全球扩展优化时使用。
顺带提醒一下:浏览器缓存和 CDN 缓存是两回事。打开 DevTools,进入 Network 标签页,保持 “Disable cache” 不勾选,筛选 Doc,然后刷新页面。你看到的将是浏览器自身保存的内容。不要把这一层和边缘缓存层混淆。
传播、自动化与最佳实践
等待边缘节点完成同步
Purge 请求并不会在同一时刻到达所有边缘节点。它会随着时间在整个 CDN 网络中逐步传播。你可能会在某个地区看到新内容,而另一个地区仍在提供旧版本。这种延迟叫作传播延迟,并不代表 purge 失败。Cache invalidation 传播到所有节点所需的时间,会因服务商不同,以及持有该对象的节点数量不同而有所差异。
在真正依赖某家 CDN 的 purge 能力之前,先测试它的典型传播时长。然后在你的工作流中,加入与你测得结果相匹配的等待时间。如果跳过这一步,你就可能把正常的传播延迟,误判为 cdn cache not refreshing。接着你又会再次发起 purge,结果只是增加负载,而不会真正解决问题。很多 cdn caching issues,与其说是靠重复 purge 解决,不如说是靠耐心和验证解决的。
把 Purge 接入部署流水线
应当在开始部署之前先 purge CDN 缓存,而不是在部署完成之后。这样的顺序可以避免访客在上传窗口期间命中陈旧内容。尽量使用选择性 purge,针对具体文件、目录或 URL 模式操作。这样未发生变化的内容仍能保留缓存带来的性能收益。像 /css/* 或 /js/* 这样的通配 purge,在你一次更新同一目录中的大量相关文件时会很好用。
你可以通过部署脚本中的 purge api 来自动化这一步。把 purge 命令纳入发布流程,等待确认结果,为失败请求加入重试逻辑,并记录全部日志。再将 purge 与文件指纹、查询字符串版本号这类版本化策略结合起来。这样更新后的资源会直接绕过旧缓存条目。采用原子化部署也很关键,比如先上传到带版本号的目录,再统一切换引用,这样就能消除新旧资源混杂出现的窗口期。
大多数反复出现的问题,都来自三个错误。只 purge 单个 URL,而整个资源包其实都发生了变化,会导致大部分资源仍旧陈旧。忘记轮换 API token,会让自动化静默失效。热修复场景下完全依赖 TTL,则会让用户在数小时内继续看到旧内容。应当把 cache invalidation best practices 视为流水线中的必要环节,而不是事后补救。部署后,还要跨多个边缘地区测试缓存行为,并在测试时清理浏览器缓存。养成这些习惯后,cache invalidation 就会从反复爆发的紧急故障,变成一项日常流程。
Purge 能强制刷新。预防措施则能避免问题再次发生。你可以先选一种“强制更新”方法开始:紧急修复时使用 API purge,静态资源则优先使用版本化 URL。版本化 URL 更适合作为主要防线,因为文件名一旦变化,就会创建新的缓存条目,不再需要失效旧条目。然后再增加一种预防习惯:在每次发布前,把 purge 接入部署流水线。把 API purge 自动化视为一种次级、响应式工具。只要这两种做法协同运行,CDN 就不会再频繁“给你惊喜”。陈旧内容是一个常见的 CDN 问题,它有明确的解决方式,并不神秘。
常见问题
为什么我 purge 之后,CDN 仍然在提供旧文件?
Purge 请求会随着时间逐步传播到各个边缘节点。某个地区可能已经显示新内容,而另一个地区还保留着旧副本。这种差异是传播延迟,不代表 purge 失败。先等待请求完成,再检查响应头进行验证,然后再决定是否需要再次 purge。
一次 CDN purge 需要多久才能传播到所有边缘节点?
具体时间取决于服务商,以及持有该对象的节点数量。在真正依赖它之前,先测量你的服务商通常需要多长时间。然后把这个等待时间纳入工作流中。否则,你很容易把正常传播延迟误判为缓存拒绝刷新。
我应该清空全部缓存,还是只清除我改动过的文件?
全局 purge 会一次性清空所有内容。它虽然是最快的方法,但会迫使所有边缘节点重新抓取全部内容,从而增加源站负载。按 URL 或标签 purge 更加精细。只移除发生变化的内容,把其他缓存保留下来。
版本化 URL 能否完全替代 purge?
版本化 URL 会给每个资源分配一个新地址,因此这些文件不需要 purge。文件指纹和查询参数都属于这种做法。不过,API purge 仍然应该保留,作为那些来不及改文件名的紧急修复场景下的后备手段。
为什么 purge 成功了,我看到的页面还是旧的?
因为你的浏览器也有自己的缓存层,它和 CDN 边缘缓存是分开的。打开 DevTools,进入 Network 标签页,保持 “Disable cache” 不勾选,筛选 Doc,然后刷新页面。这样你看到的就是浏览器自身缓存了什么。不要把浏览器缓存和边缘缓存混为一谈。
