接入 CDN 的头两个月,我只做了一件事:把域名指过去。缓存规则用的是默认值,命中率一直在六成左右,源站每天仍然收到大量回源请求。
真正开始调优,是因为一次月末的流量账单。我对着报表看了一晚上,发现大部分回源请求来自同一批静态资源,而它们的内容几个月都没变过。
先看命中率,但不止看命中率
CDN 控制台给的第一个指标是缓存命中率。默认配置下大概是 62%。这个数字看起来还行,但它掩盖了一个问题:命中的是哪些请求,没命中的又是哪些。
我把请求按路径分了几类:
| 类型 | 示例 | 默认命中率 |
|---|---|---|
| 静态资源 | /css/*.css、/js/*.js |
较高 |
| 图片 | /pic/* |
中等 |
| 页面 HTML | /2026/01/.../ |
很低 |
| 带参数的请求 | ?from=rss |
几乎为零 |
页面 HTML 命中率低是正常的,因为它需要及时更新。但带参数的静态资源也几乎不命中,这就很浪费——同一张图片加了不同的查询参数,就被当成不同对象回源。
三个改动
第一,调整缓存键。 默认缓存键包含完整的查询字符串。我把静态资源路径的查询参数忽略掉,只保留路径和必要的版本参数。改完之后,同一张图片的不同来源链接会命中同一份缓存。
第二,按资源类型分级设置 TTL。
- 带内容指纹的 CSS、JS:一年;
- 图片:三十天;
- HTML:五分钟,配合发布时主动刷新;
- 接口:不缓存。
分级的关键不是“越长越好”,而是“这类资源变化时,我有没有办法让它立刻失效”。带指纹的文件内容变了,文件名也会变,所以可以放心长缓存;HTML 没有指纹,就必须留一条刷新通道。
第三,减少不必要的回源头。 默认配置会把 Cookie 和部分请求头带到回源请求里,导致缓存无法复用。我把静态资源路径的 Cookie 转发关掉,只对接口保留。
结果
调整之后,缓存命中率稳定在 90% 左右。首字节时间从平均 380ms 降到 120ms,源站带宽下降了一半多。移动网络下的首屏时间也有明显改善。
但我没有只记录好的一面。同期也出现了新的问题:
- 发布后看不到更新。 HTML 缓存五分钟,加上主动刷新有延迟,有读者在文章发布后十几分钟仍看到旧页面。
- 调试变难。 缓存命中后请求不到源站,日志里看不到,排查问题时要先手动绕开 CDN。
- 刷新变成流程的一部分。 每次发布都要记得刷新哪些路径,漏了就会出不一致。
取舍
这三个问题没有一个是“配置错误”,它们都是缓存带来的必然代价。我能做的是把代价控制在可接受范围内。
对“更新不可见”,我的处理是:发布流程里加一步主动刷新 HTML 和变更的资源,刷新后用一个带随机参数的请求验证;同时把 HTML 的 TTL 缩短到五分钟,接受一点点回源压力。
对“调试难”,我在本地 hosts 里保留了一条直连源站的记录,排查时先绕过 CDN 确认源站行为,再回来验证 CDN。
对“刷新变流程”,我把它写进了发布脚本,而不是靠记忆。脚本会输出这次刷新了哪些 URL,失败时直接报错。
一个反直觉的观察
调优之前,我以为“命中率越高越好”。调完之后我发现,命中率是一个结果指标,不是目标。
真正要回答的问题是:哪些内容应该被缓存、缓存多久、变了之后怎么让用户看到。这三个问题想清楚,命中率自然会上去;只想把数字做高,很容易做出“用户永远看到旧内容”的缓存。
现在每次改缓存规则,我都会同时问一句:如果这条规则错了,我怎么发现、怎么回滚?如果答不上来,就先不改。
最后
CDN 调优给我的最大收获,不是那几个百分点的速度,而是让我开始把“发布”和“缓存”当成同一件事来设计。缓存不是接上去就完事的加速层,它是内容生命周期的一部分。
速度提升是看得见的,取舍是看不见的。后者才决定这套配置能不能长期用下去。