对象存储 + CDN 分发静态资源:缓存怎么设才既快又能更新
客户端把资源包放在对象存储上按需下载,是个很省事的架构。麻烦出在缓存策略上: 缓存时间设短了,每次启动都在重新校验,慢且费流量;设长了,改完资源发上去, 用户手上还是旧的。
这两件事其实不矛盾,前提是把文件分成两类来对待。
核心思路:不可变文件 + 一个可变的入口
把所有资源文件按内容哈希命名,比如 scene_a_8f3c2d1b.bundle。
内容一变,文件名就变;文件名一样,内容就必然一样。这类文件是不可变的,
可以放心设一年的强缓存:
Cache-Control: public, max-age=31536000, immutable
然后单独留一个小文件作为入口,记录"当前版本是哪一批文件"。这个文件是可变的, 必须不缓存:
Cache-Control: no-cache
客户端启动流程就变成:拉入口文件(永远是新的)→ 得到版本号 → 拉该版本的清单 → 对比本地已有文件 → 只下载文件名没见过的那些。已有文件因为名字带哈希,命中的一定是对的内容, 不存在"缓存到了旧内容但名字一样"的可能。
no-cache 设对了,中间还可能有
运营商缓存、系统网络栈缓存、客户端框架自己的缓存。稳妥做法是请求入口文件时附加时间戳:
version?t=1786977379。很多资源框架默认就带这个行为,接入前值得确认一下,
别自己又加一层导致缓存全废。
批量设置 Cache-Control
对象存储的元信息是按对象存的,上传时不指定就没有。命令行工具批量刷一遍即可 (以下用 ossutil 的写法,其他厂商工具同理):
# 上传时就带上(-u 增量,只传有变化的)
ossutil cp -r -u ./dist/ oss://your-bucket/assets/ \
--meta "Cache-Control:public, max-age=31536000, immutable"
# 入口文件单独覆盖
ossutil set-meta oss://your-bucket/assets/version \
"Cache-Control:no-cache" --update
验证不要靠猜,直接看响应头:
curl -sI https://your-domain/assets/version | grep -i cache-control
教训一:默认域名不能拿来当网页站点
对象存储会给你一个默认访问域名,测试期很好用。但它不适合承载正式的网页内容:
- 国内厂商普遍限制用默认域名直接浏览 HTML,行为可能是强制下载而不是渲染。
- 更实际的问题是,正式上线要走备案,而备案要填域名和服务器 IP, 默认域名和它背后的地址都不是你能填的东西。
所以架构上应该一开始就规划成:自己的域名 → CDN 加速域名 → 源站指向对象存储。 默认域名只在开发期用。我因为图省事一路用默认域名开发, 到要上线才发现整条链路都得重配一遍,还牵连到客户端里写死的地址、 构建脚本里的上传路径、平台后台的域名白名单,一共四处。
凡是"上线前一定要改"的配置,就别指望上线前才改。把它做成一个变量, 从第一天起就走正式路径。
教训二:欠费十五天,桶会被删掉
这条是真金白银买的:账户欠费满十五天,存储桶被自动删除,数据不可恢复。 更糟的是桶名释放后立刻被别人抢注了,同一个名字再也拿不回来—— 而客户端里写死的下载地址正是那个名字。
当时的排查过程也值得一说。匿名访问返回 403,我第一反应是权限被改成了私有; 但带密钥的命令行工具访问同样 403,这就说明不是访问控制的问题, 而是这个桶已经不属于我了。列一下账户下的桶,果然只剩新建的那个。
NoSuchBucket 是桶不存在,
InvalidAccessKeyId 是密钥失效,AccessDenied 是有东西挡着你——
可能是权限、可能是防盗链,也可能是这个名字已经属于别人了。别看到 403 就去查权限。
现在的防线有三条:账户设余额预警、给项目设月度预算告警、 重要资源的地址通过配置下发而不是编译进客户端。第三条最关键, 它让"换存储位置"从一次发版变成一次配置变更。
小结
- 文件名带内容哈希,配一年强缓存;入口文件不缓存并附时间戳。这一组合让长缓存和即时更新共存。
- 上传时就写好 Cache-Control,事后靠响应头验证,别靠"应该没问题"。
- 从第一天就用自己的域名走 CDN,默认域名只留给开发期。
- 存储服务的余额和预算告警是基础设施的一部分,不是财务问题。