HTTP 缓存与 CDN 加速实践:先定复用条件,再谈边缘加速
HTTP 缓存的核心是一个判定问题:响应能否被复用、由谁复用、复用多久、何时必须回源校验。Cache-Control 决定新鲜度与可存储范围,ETag/Last-Modified 负责过期后的校验,Vary 决定缓存键维度。CDN 边缘属于共享缓存,只负责执行这些语义;响应头写不清楚时,加速层只会把不确定性搬到离用户更近的地方。
HTTP 缓存要回答的是一个判定问题:这次响应能不能被复用、由谁复用、复用多久、什么时候必须回源校验。CDN 加速并不改变这个判定,只是把它放到离用户更近的一层执行。如果响应头没写清楚,换成更快的边缘节点也只是把不确定性前移。本文只讨论可核验的一般缓存语义;各家 CDN 产品的默认值、缓存键规则与调试字段名,本次未联网核验,需以对应文档为准。
缓存复用由两个机制分工#
按 HTTP 缓存的一般语义,复用分两步:先看新鲜度,不新鲜再走校验。
- 新鲜度:
Cache-Control: max-age=N面向所有缓存,s-maxage只约束共享缓存(CDN 边缘即属共享缓存)。Expires是绝对时间,受时钟偏差影响,通常让位于max-age。 - 校验:过期后带
If-None-Match(配合ETag)或If-Modified-Since(配合Last-Modified)回源,未变化则返回 304,不重传正文。
建议把两者看作互补而非二选一:带内容指纹的静态资源适合长 max-age 加 immutable;变化频繁但体积大的响应,更适合短新鲜度加 ETag 校验,用 304 省下正文传输。
关键响应头的分工#
| 响应头 | 决定什么 | 容易出错的地方 |
|---|---|---|
Cache-Control | 可存储范围与新鲜时长 | 把 no-cache 当成 no-store:前者仍可存储,只是每次使用前必须校验 |
ETag | 内容是否变化的校验凭据 | 按进程或节点生成,同一资源在不同边缘得到不同值,校验反复失效 |
Last-Modified | 基于时间的弱校验 | 精度只到秒,短时间多次变更可能漏判 |
Vary | 缓存键要看哪些请求头 | 维度缺失导致不同客户端拿到同一份内容;写成 * 则几乎等于不可复用 |
Age | 响应已在缓存中存放多久 | 排查“为什么边缘返回的还是旧内容”时最直接的一列 |
CDN 是共享缓存,边界要单独声明#
推断而非规范强制的一点是:既然边缘是共享缓存,private、no-store 的响应就不应被边缘存储。实际是否严格执行取决于产品实现,需要用各家的调试响应头自行确认。
- 带
Set-Cookie或按用户区分的响应,不要给长s-maxage;这类内容更适合留在浏览器缓存或回源。 Vary的维度必须被边缘正确支持。若按Accept-Encoding分流,却未在缓存键中体现,就容易出现压缩内容与未压缩内容互相串。- 写入类请求默认不走缓存;若确实要给 GET 之外的方法做缓存,必须显式声明,并先确认语义是否允许。
一次可复现的检查步骤#
下面命令是示例,本次未在真实环境执行,仅说明观察顺序;域名使用占位地址。
curl -sSI https://www.example.com/static/app.js
- 看
Cache-Control:是否区分了max-age与s-maxage,是否包含private。 - 看
ETag:连续请求两次,值是否稳定。 - 看
Age:第二次请求是否递增,递增说明命中了中间缓存。 - 改一个
Vary涉及的请求头(例如Accept-Encoding)再请求,确认正文按预期分流。
部分 CDN 会附加自己的命中标记头,字段名与取值语义各家不同,本次材料未提供,需查对应文档后使用。
什么时候不该指望边缘缓存#
强个性化接口、鉴权后的内容、实时性要求高的读接口,通常不适合在边缘长期存储。即使命中率看起来很高,也要先确认缓存键没有被写得过粗——命中率虚高往往意味着把不该共享的响应共享了。缓存策略的收益应以回源请求量、带宽与响应时间等可测量的指标评估;本次没有测试数据,不给出具体数字。
把“能不能复用、谁能复用、复用多久、何时校验”写进响应头,CDN 才只是一个执行者,而不是一个猜测者。||DSML|| parameter>