CDN加OBS发布时我踩到缓存键坑
故障出现和我走错的路
7月18日20:37,我把官网前端 v3.14.6 发到华东-上海一的 OBS 桶 portal-static-prod,这次对应的 Git 提交是 9f2c7ab。发布链路没改,CodeArts 构建后把产物推到一台 ECS,再由 obsutil 上传到 OBS。机器是 c7.large.2,2 vCPU、4GB 内存、40GB ESSD,Ubuntu 22.04.4 LTS,出网带宽 5M。流水线分段日志里,pnpm install 41s,pnpm build 2m18s,上传 1m47s,CDN 刷新接口 20:40:56 返回 200,整条链路 6 分 11 秒。
我那天晚上手动刷了312个URL。
20:41 值班群贴了三张白屏截图,我自己刷新十次只复现三次,用户侧却持续报错。Sentry 同时冒两个错误,轮着出现,一个是 chunk 加载失败,一个是运行时拿不到模块。这个组合很烦,因为看起来像两件事,其实是同一条缓存链路导致的混版本。
ChunkLoadError: Loading chunk 812 failed. (error: https://static.team.example.com/assets/app.js?v=1721306282)
GET https://static.team.example.com/assets/chunk-812.js?v=1721306282 net::ERR_ABORTED 404
TypeError: Cannot read properties of undefined (reading 'call') at app.js:2:174392
- CDN 域名:static.team.example.com
- 源站:portal-static-prod.obs.cn-east-3.myhuaweicloud.com
- 前端:Vite 5.4.2,Node.js 20.11.1,pnpm 9.7.0
- 发布频率:工作日每天 2 到 4 次
- 旧缓存规则:*.js 和 *.css TTL 604800 秒,HTML TTL 300 秒,URL 参数设置为忽略全部
- 历史构建配置:入口文件名锁死成 app.js、vendor.js,版本只靠 query 参数
这个锅我一开始甩给了前端同事。
20:46 到 21:09 我先盯 Service Worker,怕老 SW 拿着旧清单不放。无痕窗口、禁用缓存、清站点数据都试了,还是复现。控制台跑 navigator.serviceWorker.getRegistrations().then(r => console.log(r.length)),输出是 0,这条路白走了 23 分钟。接着我怀疑 OBS 上传不完整,做对象清点,构建目录 684 个对象,桶里也是 684 个,文件总大小差 0 字节。为了确认不是本地代理污染,我还在 ECS 上直接 curl 同一个 URL 两次,结果一个 200 一个 404,时间间隔不到 30 秒。
obsutil ls obs://portal-static-prod/ -s | grep -E 'app.js|vendor.js'
obsutil ls ./dist/assets -s | wc -l
curl -I 'https://static.team.example.com/assets/app.js?v=1721306282'
curl -I 'https://static.team.example.com/assets/chunk-812.js?v=1721306282'
21:12 我做了第一个无效改动,把 CDN 的 *.html TTL 从 300 秒改成 30 秒,规则优先级填了 30,平台提示预计 5 到 10 分钟生效,实际 6 分钟生效。监控里白屏率从 28.4% 到 27.9%,几乎没动。21:20 我又发起全站预热,684 个对象跑了 23 分钟,命中率从 95.8% 掉到 91.7%,工单没少。更尴尬的是那 312 条手动刷新 URL,任务详情写着提交 312、受理 2、重复 310,我那时还没反应过来平台按“忽略参数”做缓存键,不同 v 值在它眼里就是同一个路径。
改动细节、费用和还没做完的事
我把两个版本号请求头并排看,才回到正路。两次请求的 Age 都是 516233,节点都在 hcdn-sha-03,这已经很直白了,边缘缓存键没吃 query 参数。我又把响应体拉下来算哈希,两个文件完全一致,说明 v=1721306282 和 v=1721307000 实际命中的是同一个缓存对象。
HTTP/1.1 200 OK
Content-Length: 913245
Cache-Control: public,max-age=604800
Age: 516233
X-Cache: HIT from hcdn-sha-03
ETag: 0037B9A6F4A7F1C2
curl -s 'https://static.team.example.com/assets/app.js?v=1721306282' -o /tmp/app_old.js
curl -s 'https://static.team.example.com/assets/app.js?v=1721307000' -o /tmp/app_new.js
sha256sum /tmp/app_old.js /tmp/app_new.js
4f19f5e2f7d3b9f1d9e7a4df8e7ce6f3b2f2c1c5f2f60a8c0af0f6dd4b0db792 /tmp/app_old.js
4f19f5e2f7d3b9f1d9e7a4df8e7ce6f3b2f2c1c5f2f60a8c0af0f6dd4b0db792 /tmp/app_new.js
当晚止血我只改一处,CDN 的 URL 参数策略从“忽略全部”改成“保留全部参数”,保存时间 21:24,业务侧 21:32 观测到生效。白屏告警在 9 分钟内从每分钟 17 次降到每分钟 1 到 2 次。我中间还有一次失败尝试,21:27 给 /assets/app.js 单独加 TTL 0 规则,想着只让入口脚本回源。结果规则优先级配成了 60,*.js 规则是 20,平台按数字小的先命中,等于白改;我用 curl -I 看到还是 Cache-Control: public,max-age=604800,当场确认没生效。
第二天我把发布方式彻底改掉,不再赌刷新缓存。旧 Vite 配置是固定文件名,这个坑就是我自己之前图省事埋的。我先只改了 entryFileNames,没改 chunkFileNames,预发跑了 7 分钟又出现一次 chunk 404,立刻回退。第二版才一次改全:入口、chunk、静态资源都改 hash 文件名,HTML 走 no-cache,发布只刷新两个 HTML。下面是我最终落地的配置和命令。
// vite.config.ts 旧
entryFileNames: 'assets/app.js'
chunkFileNames: 'assets/[name].js'
assetFileNames: 'assets/[name].[ext]'
// vite.config.ts 新
entryFileNames: 'assets/[name]-[hash].js'
chunkFileNames: 'assets/[name]-[hash].js'
assetFileNames: 'assets/[name]-[hash][extname]'
pnpm build
obsutil cp dist/assets/ obs://portal-static-prod/assets/ -r -f -meta='Cache-Control:public,max-age=31536000,immutable'
obsutil cp dist/index.html obs://portal-static-prod/index.html -f -meta='Cache-Control:no-cache,max-age=0,must-revalidate'
obsutil cp dist/admin.html obs://portal-static-prod/admin.html -f -meta='Cache-Control:no-cache,max-age=0,must-revalidate'
这次返工我自己花了 1 小时 12 分钟,联调和回归 18 分钟,最花时间的是核对懒加载路由和 sourcemap 路径。7月18日 21:00 到 24:00,CDN 命中率从 95.8% 掉到 82.6%,OBS 回源多了 11.4GB,账单多 6.3 元。7月19日到7月25日换成 hash 文件名后,平均命中率回到 96.2%,同等访问量下回源从 74.8GB 降到 21.7GB,URL 刷新量从每天 200 到 400 条降到 2 条。
预发环境还在用固定文件名,我还没把多环境脚本合并,线上和预发缓存行为依旧不一致,这周我准备把同一套 Rollup 输出和 CDN 参数策略同步过去,再补一条“发布后抽样比对 Age 和 ETag”的自动检查。
- 点赞
- 收藏
- 关注作者
评论(0)