·资源分发

对象存储 + 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
version no-cache + 时间戳 manifest 该版本的文件清单 *_8f3c2d1b.bundle immutable,缓存一年 每次启动都拉,永远是新的 对比本地已有文件名 只下没见过的名字 文件名带内容哈希 → 名字一样,内容就一定一样 → 命中缓存永远是对的 所以「长缓存」和「改了立刻生效」并不矛盾:可变的只有那个入口文件。
不可变文件 + 一个可变入口。整套策略的全部要点就在这张图里。

教训一:默认域名不能拿来当网页站点

对象存储会给你一个默认访问域名,测试期很好用。但它不适合承载正式的网页内容:

所以架构上应该一开始就规划成:自己的域名 → CDN 加速域名 → 源站指向对象存储。 默认域名只在开发期用。我因为图省事一路用默认域名开发, 到要上线才发现整条链路都得重配一遍,还牵连到客户端里写死的地址、 构建脚本里的上传路径、平台后台的域名白名单,一共四处。

凡是"上线前一定要改"的配置,就别指望上线前才改。把它做成一个变量, 从第一天起就走正式路径。

教训二:欠费十五天,桶会被删掉

这条是真金白银买的:账户欠费满十五天,存储桶被自动删除,数据不可恢复。 更糟的是桶名释放后立刻被别人抢注了,同一个名字再也拿不回来—— 而客户端里写死的下载地址正是那个名字。

当时的排查过程也值得一说。匿名访问返回 403,我第一反应是权限被改成了私有; 但带密钥的命令行工具访问同样 403,这就说明不是访问控制的问题, 而是这个桶已经不属于我了。列一下账户下的桶,果然只剩新建的那个。

错误码能区分故障类型:NoSuchBucket 是桶不存在, InvalidAccessKeyId 是密钥失效,AccessDenied 是有东西挡着你—— 可能是权限、可能是防盗链,也可能是这个名字已经属于别人了。别看到 403 就去查权限。

现在的防线有三条:账户设余额预警、给项目设月度预算告警、 重要资源的地址通过配置下发而不是编译进客户端。第三条最关键, 它让"换存储位置"从一次发版变成一次配置变更。

小结

← 返回文章列表