2026-09-13 日报
今日主题
静态资源CDN缓存问题
BriefIntroduction
每年总有几起和 DNS / CDN 相关的线上事件😂,这次把「客户端 → 缓存 → CDN」这条链路顺手记一下。
一、常见 CDN 厂商
静态资源很少直接打源站,中间通常会过一层 CDN。国内国外常见的大概是这些(不分先后):
国内: 阿里、腾讯、百度、网宿
国外: Cloudflare、AWS CloudFront、Akamai、Fastly、Google Cloud CDN、Azure CDN
二、常见的 responseStatus
常会碰到的状态码
200:完整返回资源304:协商缓存判定没变,不传 body,继续用本地206:Range 分片,音视频、大文件常见301/302:域名或路径跳了403:防盗链、Referer 之类拦了404:路径错了,或旧资源被清了502/504:边缘回源失败 / 超时503:过载、限流、维护
比较坑的:responseStatus = 0
这不是 CDN 真回了个状态码 0。
监控里出现 0,常见是跨域资源没配 Timing-Allow-Origin,Resource Timing 把 responseStatus、transferSize、dnsTime、tcpTime、ttfb 这些敏感字段清零了;download、totalTime 这类耗时往往还在。也可能是请求被取消、被插件/CSP 拦了,或 no-cors 的 opaque 响应。
所以看到监控 0,别先当失败——很可能资源已经加载成功,只是字段读不到。
三、这次的问题:CDN 没把 Vary 透传下来
排查资源加载时,先撞到监控里 responseStatus = 0(上面那种 Timing 限制)。继续挖的时候发现另一类更隐蔽的问题:
本该重新请求的场景,客户端根本没发网,直接吃了 Disk Cache 里的旧内容。
Network 里经常是 (from disk cache)。
Vary 是干什么的
Vary 告诉浏览器:同一个 URL,如果某些请求头不一样,缓存要分开存。
比如 Vary: Accept-Encoding——支持 gzip/br 和不支持压缩的客户端,不能共用一份缓存,不然可能把压缩包发给解不了的一方。Vary: Origin 也是类似思路,按来源区分版本。
正常缓存 key 大致是:
1 | URL + Vary 里声明的那些请求头的值 |
还有一点:强缓存命中时,复用的不只是 body,当时存下来的响应头也会一起复用。第一次如果缺了 Vary,后面会一直按「残缺的头 + 旧 body」用下去。
问题怎么串起来的
- 源站其实带了
Cache-Control和Vary - CDN / 网关转发时把
Vary弄丢了,浏览器拿到的响应里没有它 - 浏览器觉得「这 URL 不用按请求头区分」,缓存 key 退化成只看 URL
- 之后请求头已经变了,本该当新版本去拉,结果只比对 URL,判定命中,直接本地复用,不发请求
因为卡在强缓存的本地判断,不联网,所以不好查。临时换个静态资源域名,等于换了一套缓存 key,现象会消失——这反而能侧面验证是客户端缓存被污染了。
结论: CDN 没原样透传 Vary → 浏览器把多个版本合成「一个 URL 一份缓存」→ 该发的请求没发出去。
排查时可以对照:源站响应头、CDN 边缘、浏览器 Network 里最终看到的头,看 Vary 还在不在。
四、客户端缓存:强缓存和协商缓存
整条链路可以先记这个顺序:
1 | 请求 |
SW 还在用户设备上,没到 CDN。它和 HTTP 强缓存不是一套东西:SW 命中时 DevTools 会标 (from ServiceWorker);HTTP 强缓存则是 (from memory cache) / (from disk cache)。
强缓存
没过期就不发请求,本地直接用。常见头:
Cache-Control: max-age=...(秒)Expires(老写法,优先级低于 Cache-Control)immutable(一般配合 hash 文件名,连过期后的校验都省)
协商缓存
强缓存过期了,或一开始就没设强缓存,浏览器才会再发一请求,问问「变了没」。不是先问再另取,一次响应里就给出结果。
两套标识,有一套就行,也可以一起带:
| 第一次响应里 | 第二次请求会带 | 比什么 |
|---|---|---|
Last-Modified |
If-Modified-Since |
服务器上的最后修改时间 |
ETag |
If-None-Match |
内容版本 / hash |
Last-Modified 例子:
第一次 200,带上 Last-Modified: Wed, 21 Oct 2025 07:28:00 GMT(服务器认为的修改时间,不是你本地时间)。浏览器把内容和这个时间一起存着。
第二次若已过强缓存期,会带 If-Modified-Since(值就是上次那个时间)。服务器对比:没变 → 304;变了 → 200 + 新文件 + 新的 Last-Modified。
ETag 同理:
第一次存 ETag;第二次带 If-None-Match。相同 → 304;不同 → 200 换新。ETag 通常比时间戳准,但不是必填。很多静态资源只靠 max-age + hash 文件名,响应里可以没有 ETag;没有 ETag 也不等于不能协商,还有 Last-Modified。
304 和 200 路径一样,差在要不要把完整 body 传回来。304 省的就是这一截带宽。
如果第二次还在强缓存期内,根本不会走到协商,直接 disk/memory 命中。