CDN 缓存详解:原理、策略与命中率优化实践
CDN 缓存详解:从边缘节点到命中率的完整实践
打开一个网页只需要几百毫秒,但你或许很少想过:这些内容究竟是从哪里来的?如果每一个用户的每一次请求都要直接打到源站服务器,那么无论带宽多充裕,物理距离和并发压力都会成为难以回避的瓶颈。CDN 缓存在其中扮演的角色,就是把内容提前搬到离用户更近的地方,让绝大部分请求在边缘节点就被"就地解决",根本不需要惊动源站。
很多团队对 CDN 的理解停留在"加个加速域名、改个 CNAME"这一步,结果上线之后发现加速效果有限,甚至出现内容更新不及时、旧图片更新不掉的问题。问题往往不在 CDN 本身,而在于缓存这件事没有被认真设计过。
一、CDN 缓存到底缓存了什么
CDN 的本质是一张由大量边缘节点组成的分布式网络。当用户请求经过调度被分配到距离最近、负载较低的节点后,该节点会先检查自己本地是否已经保存了这份内容的副本。如果有,也就是缓存命中,节点直接把内容返回给用户,响应时间通常只有几毫秒到几十毫秒;如果没有,也就是缓存未命中,节点需要向上一级节点或源站发起请求,这个过程叫回源,拿到内容后再按照规则决定是否留存在本地,供后续请求复用。
需要强调的是,CDN 缓存的是"响应",而不是"文件"。节点记住的是某个请求对应的完整响应结果,包括状态码、响应头和响应体。理解这一点非常关键,因为它解释了后面很多看起来奇怪的现象:为什么改了源站文件却不生效(响应还没过期)、为什么带参数的 URL 会各自缓存一份(缓存键不同)、为什么错误页面也会被缓存(500 状态码同样可以被缓存)。
二、一次请求的完整旅程
1. 调度与接入
域名解析阶段,CDN 的智能 DNS 会根据用户所在区域、运营商线路和节点健康状态,返回一个合适的边缘节点 IP。这一步决定了用户"就近"到什么程度,也是不同 CDN 服务商体验差异的主要来源之一。
2. 边缘节点查缓存
节点用缓存键去本地存储中查找对应记录,并检查这份记录是否还在有效期内。两者都满足才算是有效命中。
3. 逐级回源
未命中时,不少 CDN 并不会直接冲回源站,而是先向中间层(有的厂商称为二级节点、父层节点或中间源)请求,只有中间层也没有,才会真正回源。这种分层结构能把大量长尾请求的重复回源合并掉,对源站保护作用明显,尤其适合多节点同时缓存失效的场景。
4. 响应与回填
拿到响应后,节点会根据缓存规则、响应头中的缓存指令以及状态码,判断这份内容是否可缓存、缓存多久,然后写入本地存储。用户拿到的是完整响应,同时这份内容也完成了"回填"。
三、缓存键:什么才算"同一份内容"
缓存键是 CDN 缓存的判定依据。默认情况下,它通常由域名、路径和查询字符串组成。也就是说,/logo.png 和 /logo.png?v=2 会被视为两份不同的内容,各自占用一份缓存空间。
这就带来一个很常见的配置项:忽略 URL 参数。营销投放、短信推送、第三方统计经常会在链接后面追加 utm_source 之类的参数,如果这些参数参与缓存键计算,同一个页面可能被缓存成几十上百份,命中率被严重稀释。把不影响的参数(如跟踪参数、时间戳)排除在缓存键之外,是提升命中率最立竿见影的手段之一。
反过来,如果某个参数确实会影响返回内容,比如 ?lang=en 或者 ?width=750,就必须保留在缓存键中,否则会把不同语言、不同尺寸的图片混在一起返回,造成严重的串内容事故。此外,当源站根据 Accept-Encoding、User-Agent 等请求头返回不同内容时,需要通过 Vary 响应头声明,CDN 才会把它们区分开来。
四、TTL 与缓存新鲜度
TTL(Time To Live)决定了一份缓存能用多久。过期之后,节点不会立刻丢弃内容,而是带上校验请求回源确认一下,源站若返回 304,节点可以直接续期;返回 200,则用新内容替换。
控制 TTL 的优先级通常是:CDN 控制台的缓存规则配置 > 源站响应头中的 Cache-Control(s-maxage 优先于 max-age)> Expires > CDN 的默认策略。很多"缓存不生效"的工单,最后都指向源站响应里带了 Cache-Control: no-cache 或 private,而这类指令的优先级往往高于控制台的部分配置。
进阶一点的用法会用到 stale-while-revalidate 和 stale-if-error。前者的意思是:即使缓存已经过期,也先把旧内容返回给用户,同时在后台异步回源更新;后者则是在回源失败时,允许继续用旧内容兜底。这两个指令对源站抖动场景特别友好——用户感知不到等待,源站也拿到了喘息的空间。Cloudflare、Akamai 以及国内的阿里云 CDN、腾讯云 CDN 等产品都在不同程度上支持这类能力。
五、不同资源,策略应该分开定
静态资源:长缓存,配指纹
带有内容哈希的 JS、CSS、字体、图片,文件名本身随内容变化,因此可以放心地设置很长的 TTL,甚至加上 immutable。这是现代前端构建体系的标配做法,收益也最大。文件名变了,缓存键自然变化,天然绕开了刷新问题。
HTML 入口:短缓存或不缓存
HTML 通常承载着资源引用关系,一旦被长时间缓存,发版之后用户会持续加载旧的 JS 路径,出现白屏或样式错乱。稳妥的做法是给 HTML 设置很短的 TTL,或者直接不缓存,让每次访问都回源校验。
图片与视频:按热度分层
图片体积大、访问集中度高,是缓存收益最明显的类型。但要注意封面图、缩略图这类"一改就要立刻生效"的资源,必要时配合目录刷新使用。
接口响应:谨慎对待
公开的、与用户身份无关的读接口可以短时间缓存,比如排行榜、配置项、商品分类。带用户信息的接口必须避免共享缓存,否则会出现 A 用户看到 B 用户数据的严重事故。这类接口应当在源站明确返回 private,并在 CDN 侧配置对应规则。
六、刷新与预热
刷新(Purge)用于让已缓存的内容提前失效,常见形式有 URL 刷新、目录刷新和全站刷新。URL 刷新精确但数量受限,目录刷新适合批量更新,全站刷新代价最大,一般只在极端情况下使用。需要注意的是,刷新是"删除"而不是"重新拉取",刷新之后的第一个请求仍然要回源,如果此时源站压力较大,配合预热会更稳妥。
预热(Prefetch)则是在发布前主动把内容推送到边缘节点,避免上线瞬间大量用户同时触发回源。对于有明确发版节奏的业务,把预热纳入发布流程是非常值得的一步。
七、命中率:最值得盯住的指标
缓存命中率通常有两种口径:请求命中率(命中请求数 / 总请求数)和流量命中率(命中流量 / 总流量)。两者差距可能很大——如果大文件命中率高、小请求命中率低,流量命中率会很漂亮,但回源的 QPS 依然很高。判断源站压力,更应该看回源请求数和回源带宽,而不是只看一个百分比。
命中率偏低时,可以从几个方向排查:URL 参数是否过于分散、TTL 是否设置过短、源站是否返回了不可缓存的响应头、是否存在大量带 Cookie 的请求、热门内容是否被均匀打散到过多节点。也不要盲目追求 100%,登录态页面、实时数据本就应当回源,命中率只是手段,不是目的。
八、几个高频问题
缓存不生效
依次确认:响应头是否允许缓存、控制台规则是否被更高优先级的规则覆盖、请求 URL 是否与配置的路径匹配、源站是否返回了 Set-Cookie 导致 CDN 主动放弃缓存。
错误页面被缓存
源站抖动时返回的 5xx 页面如果被缓存,可能在故障恢复后仍持续返回给用户。应当在 CDN 侧限制 5xx 状态码的缓存时间,或者干脆不缓存。
缓存雪崩
大量内容在同一时刻过期,会让回源请求瞬间暴涨。解决办法是给 TTL 加随机抖动,或者启用分层回源,让中间层承担合并请求的压力。
一致性焦虑
缓存必然带来一定程度的延迟,与其追求绝对实时,不如在设计上区分"允许延迟"和"必须实时"的内容,前者用缓存,后者走动态加速或直接回源。把刷新接口接入发布流水线,也能把人为疏漏降到最低。
九、CDN 缓存与浏览器缓存的分工
很多人会把两者混为一谈。浏览器缓存是"私有缓存",只服务单个用户;CDN 缓存是"共享缓存",服务一个区域内的所有用户。浏览器缓存命中时,请求根本不会发出;CDN 缓存命中时,请求到达了边缘节点但没到源站。两者层层递进,共同决定最终的加载速度。合理的组合是:资源文件名带指纹、允许浏览器长时间缓存,同时在 CDN 侧保留一份可管理的副本,需要时通过刷新来干预。
十、边缘计算带来的新可能
随着边缘函数和边缘键值存储的普及,缓存的边界正在向前延伸。过去只能对静态文件做缓存的场景,现在可以在节点上执行轻量逻辑——比如根据地理位置改写响应、做 AB 分流、对接口结果做短时缓存、拼装页面片段。像 Cloudflare 这类平台已经把边缘计算和缓存整合到同一套体系里,国内厂商也在跟进类似能力。这意味着"缓存什么"不再完全由源站的响应头决定,而是可以在边缘动态决策。
写在最后
CDN 缓存不是一个开关,而是一套需要持续调优的机制。它的核心问题始终只有三个:这份内容能不能缓存、能缓存多久、什么时候必须让它失效。把资源的缓存属性按类型梳理清楚,把缓存键设计得既不过宽也不过窄,把刷新和预热纳入发布流程,再配合对回源指标的持续观察,缓存命中率的提升往往是水到渠成的事。真正难的从来不是配置本身,而是对业务内容特性的理解。





