静态资源 CDN 回源,那些藏在 304 里面的坑

之前维护前端静态资源的时候,一直默认 CDN 是个省心组件。打包好的 js、css、图片丢上去,配置好缓存头,用户直接走节点访问,回源量很低。直到某次版本上线之后,发现源站的请求量莫名其妙涨了一大截,带宽也跟着上去,一开始以为是缓存失效了。

查 CDN 监控看命中率,整体看着还行,大概 90% 多,不算差。但细分下来,回源请求里大量都是 304 Not Modified,不是 200。304 本身不返回资源体,很多人会下意识觉得这种回源成本很低,无所谓,不用管。这次故障恰恰就是一堆 304 请求堆出来的问题。

先说当时的配置情况。前端打包脚本,没有给资源加固定的 hash 文件名,index.html 是直接覆盖更新的。缓存策略是静态资源设置 Cache-Control: max-age=86400,html 文件设置短过期,300 秒。同时开了 CDN 的 If-Modified-Since 校验。

用户浏览器本地缓存过期之后,不会直接丢弃缓存,而是发请求带上上次的修改时间去 CDN 节点。CDN 节点的缓存也刚好到期,就会带着这个时间戳回源站校验。源站对比文件 mtime,如果文件没改动,返回 304,CDN 节点更新自己的缓存时间,再把 304 传给浏览器。

这里第一个问题就暴露出来:304 只是不返回 body,但是它要走完完整的回源链路。TCP 握手、header 传输、源站读取文件判断 mtime、计算响应头,这些开销一点不少。如果并发大,大量请求只拿 304,源站 CPU、连接数照样扛不住。而且很多监控面板只统计回源 200,304 经常单独归类,很容易被忽略。

接着又发现第二个隐蔽点:mtime 不稳定。我们的发布脚本,每次发布的时候,哪怕文件内容完全没变,rsync 同步也会修改文件的 mtime。源站文件时间戳一变,CDN 校验的时候,If-Modified-Since 比对失败,直接返回 200,资源重新拉一遍。等于明明资源内容没改动,只是时间戳变了,缓存直接失效。

这个坑很容易踩,很多发布工具默认会更新修改时间。后面改了同步参数,同步的时候保留原文件 mtime,只在内容真正变更的时候才改动时间戳。

还有 ETag 和 Last-Modified 混用的问题。一开始同时开了 ETag 和 If-Modified-Since。源站是 Nginx,默认用文件 inode、大小、mtime 拼 ETag。但是我们源站是多机负载均衡,多台源服务器之间,同一个文件的 inode 是不一样的。同一个资源,用户第一次访问落到 A 源,拿到 A 的 ETag;下次 CDN 回源打到 B 源,ETag 对不上,直接返回 200,缓存命中率直接往下掉。

这里有个误区,很多教程说 ETag 比 mtime 精准。没错,但前提是多源站之间 ETag 算法的输出要一致。依赖 inode 的默认 ETag,在多源集群下基本等于废掉。要么关掉 ETag 只用 mtime,要么自己基于文件内容哈希生成 ETag,保证多台机器同一个文件输出一样的值。

再说说缓存头搭配的问题。Cache-Control 里的 no-cache,很多人理解成 "不缓存",实际意思完全相反:可以缓存,但是每次使用缓存之前,必须回源校验。这就是 index.html 最常用的配置,也是大量 304 的来源。而 no-store 才是禁止任何缓存。之前团队里新人改配置,想要浏览器不要强缓存 html,直接写了 no-cache,结果高峰期 html 的 304 回源请求暴涨。业务上 html 确实需要实时更新,这个需求没法去掉,只能换思路优化。

我们当时做的优化方案分几块。

第一,区分资源类型做两套策略。带 hash 后缀的 js、css、图片,内容一旦生成永远不变,设置超长 max-age,并且加上 immutable。immutable 的作用是告诉浏览器:在缓存有效期内,不要发任何校验请求,就算用户主动刷新,也尽量不走校验。这类资源彻底杜绝不必要的 304。

第二,入口 html 不依赖 304 校验。html 本身不缓存长时效,但是打包的时候带上版本号,配合主动刷新。版本上线之后,主动调用 CDN 接口 purge 清理 html 缓存,而不是靠过期 + 304 慢慢更新。避免集中到期之后,一波流量同时触发回源校验风暴。

第三,调整源站和 CDN 的校验开关。带 hash 资源直接关闭 If-Modified-Since 和 ETag 校验;只有入口 html 保留校验,并且单独给 html 的回源做源站限流,防止 html 集中过期打穿源站。

第四,监控指标增加维度。不再只看整体命中率,单独统计回源 304 占比、304 的 QPS。正常情况下静态资源场景,304 占比应该很低,如果持续走高,基本就是缓存策略或者发布脚本有问题。

还有一个浏览器侧的小细节:强制刷新(Ctrl+F5)会直接跳过本地缓存,不带校验头,直接回源拿 200;普通刷新会带上校验头,走 304 流程。压测的时候如果用强制刷新,是复现不出 304 堆积这个问题的,线上才看得见。

后面复盘的时候也想过,CDN 节点缓存到期,是不是节点自己可以不回源?不行,标准的协商缓存机制就是这样:缓存过期之后,想要确认资源新鲜度,就必须回源校验。协商缓存本质是用一次轻量请求,换取复用旧资源,它适合低频更新的内容,但扛不住大规模并发同时到期。

很多做业务开发的,把 CDN 当成一个 "加速盒子",配置完缓存过期时间就不管了。缓存命中率这个指标也会骗人,95% 的命中率看着很漂亮,但剩下 5% 全是 304 回源,高峰期照样能把源站打挂。缓存优化的核心,不只是尽量命中 CDN 节点,还要减少那些看起来 "轻量"、实际会累积的协商回源请求。

到现在我们的规范是:静态资源优先用内容哈希文件名,把变更绑定在 URL 上,从根源减少协商缓存的依赖。协商缓存(304)只留给少量必须实时可控、又不频繁改动的资源,并且做好到期时间打散,避免同时过期。

相关推荐
开开心心_Every2 天前
黄金价格软件国内外价格实时监控超方便
运维·服务器·游戏·智能手机·pdf·lua·perl
开开心心_Every3 天前
文件查重软件批量删除重复文件释放空间
java·开发语言·c#·计算机外设·ocr·excel·perl
开开心心就好6 天前
一元二次方程求解工具,答案带详细步骤
java·服务器·开发语言·算法·pdf·ocr·perl
AI情绪识别开源13 天前
HFpEF 心房-心室耦联效率无创监测方向(AVCEI)
typescript·laravel·perl
2501_930472441 个月前
从物理机到 CVM 全流程落地:部署脚本的 8 个云化改造点(附代码对比)
开发语言·人工智能·架构·腾讯云·perl
2601_962298671 个月前
正则表达式引擎 哪些
java·python·perl·pcre·正则表达式引擎
啊阿狸不会拉杆2 个月前
《计算机网络-自顶向下方法》1.7 计算机网络和因特网的历史 读书笔记
计算机网络·php·perl
智购科技智能售货柜2 个月前
2026自动售货机设备能耗计量系统:从功率监测到能效分析的工程实践~YH
数据库·人工智能·redis·缓存·架构·perl·symfony
黑泽明*2 个月前
云计算与服务器基础入门指南
服务器·云计算·perl