CDN回源OBS时的缓存返工记录
线上报错出现的那半天
10 月 18 日 09:43,Sentry 项目 team-a-web 的错误曲线在 5 分钟内从 8 条跳到 327 条,告警 ID 8f0d1c2a。我点开采样事件看到 release=v2026.10.18.2,environment=prod,主报错还是 Uncaught TypeError: Cannot read properties of undefined (reading 'tenantId')。堆栈指到 app.9e3f2.js:1:44219,source map 反解在 initTenant() 第 67 行,调用链是 bootstrapApp -> loadRuntime -> initTenant。发版流水线是 CodeArts #17432,Node.js 20.11.1,pnpm 9.12.0,构建 6 分 41 秒,286 个文件,总大小 73.4 MB,单测 412/412 通过,安全扫描没有阻断。
这个锅我一开始甩给前端了。
11:06 客服把 19 张截图丢到群里,控制台除了 TypeError 还有 GET https://static.team-a.example.com/assets/app.9e3f2.js net::ERR_ABORTED 404,其中 7 张图是旧 hash 文件 app.6c1ad.js。我们抓了 3 份用户 HAR,同一租户在 11:03 到 11:09 间拿到两版 index.html,ETag 分别是 '8f3a7-18f8-6510a0cf' 和 '8e120-18e2-650ff1b1'。UA 里有 Chrome 129、Edge 128、iOS Safari 17,这个分布和纯代码缺陷对不上,更像缓存层入口页不一致。
当时的部署和规格
静态资源在华北-北京四 OBS 桶 prod-h5-static,桶私有读,CDN 回源走鉴权参数,签名有效期 300 秒。业务日活 2.1 万,11:30 到 13:00 峰值带宽常在 180 Mbps。老发布脚本图省事,dist 整包上传并统一写 Cache-Control:max-age=86400,HTML、JS、runtime.json 一个策略,平时问题不明显,发版窗口就会把错配放大。
- OBS 上传工具:obsutil 5.5.12
- 探测机 1:ECS s7n.large.2(2 vCPU / 4 GiB),广州,Ubuntu 22.04
- 探测机 2:ECS s7n.large.2(2 vCPU / 4 GiB),上海,Ubuntu 22.04
- 两台探测机开 6 小时,费用 3.96 元,单台 0.33 元/小时
我用 obsutil stat 查了对象元数据,index.html 的 Last-Modified 是 10:12:07,runtime.json 是 10:29:31,差 17 分 24 秒,两者 Cache-Control 都是 max-age=86400。流水线 upload-dist 阶段里还出现过 PutObject retry 1/3, err=EOF,说明上传本身就有重试,先后顺序更不稳定。
我怎么确认是缓存错配
11:21 我在两台 ECS 同时跑抓头脚本,每分钟请求 index.html 和 runtime.json,连续 30 分钟,还加了两个查询参数版本。脚本在 /opt/probe/check_cache.sh,用的是 curl -sSI --http2 --connect-timeout 2,日志落盘 /tmp/cdn_probe.log。后面用 awk 统计 age,广州节点 p95=12103,上海 p95=11988,基本只增不减。
curl -I https://static.team-a.example.com/config/runtime.json?v=20261018
HTTP/2 200
cache-control: max-age=86400
age: 11791
x-cache: HIT from cdn-sh-02
etag: '5f29-7a1-6510a13d'
curl -I https://static.team-a.example.com/config/runtime.json?v=20261018-2
HTTP/2 200
cache-control: max-age=86400
age: 11853
x-cache: HIT from cdn-sh-02
etag: '5f29-7a1-6510a13d'
curl -I -H 'Cache-Control: no-cache' https://static.team-a.example.com/config/runtime.json?v=20261018-3
HTTP/2 200
age: 11907
x-cache: HIT from cdn-sh-02
三个参数拿到同一 ETag,手工 no-cache 也没触发回源。CDN 控制台里那条规则写的是忽略 URL 参数=是,前端靠 ?v 时间戳换配置这个手段直接失效,index.html 又被缓存一天,老页面继续引用旧 hash,404 和 TypeError 就一起出现了。
我在 00:37 才停掉全量刷新。
返工了两次
12:04 我做第一次返工,直接调 CDN 刷新接口清全站,脚本每批 500 条 URL,连跑 7 轮。接口是 POST /v1.0/cdn/domains/{domain_id}/refresh-files,并发参数开到 6。第 4 轮开始限频,返回 429,request-id 9f42b6d1。12:16 监控掉到 41 条/5 分钟,12:31 又反弹到 190 条/5 分钟,节点回源后继续拿到 max-age=86400,等于只把旧缓存换成了新的长期缓存。
refresh failed: status=429
body={'message':'Too Many Requests','request_id':'9f42b6d1'}
retry-after: 60
我还做过一次失败尝试,走前端热修,把 runtime 请求改成 fetch('/config/runtime.json?r='+Date.now(), {cache:'no-store'}),并在 axios 默认头补了 Pragma: no-cache。补丁发到 v2026.10.18.3,预发看着正常,线上没救回来,CDN 仍按忽略参数的缓存键命中同一对象,r 再变也没用。13:22 我回滚到 v2026.10.18.2,流水线 #17435,回滚耗时 9 分 14 秒。
第 2 次返工我白干了 27 分钟。
那次我只改了 CDN 配置,把 *.html TTL 设成 60 秒,但规则还勾着遵循源站,回来的响应头仍是 max-age=86400;而且这条规则优先级排在 /assets/* 后面,请求有一部分落到默认规则,age 继续累加,我盯日志时才意识到改动没有真正生效。
改动内容
17:20 我把发布脚本拆成两段,并明确上传顺序是 assets -> runtime.json -> index.html。原因很现实,先放齐带 hash 的静态文件,再放配置,收尾才发布入口页,避免入口先可见但依赖文件还没到位。流水线多了 43 秒,从 6 分 41 秒变成 7 分 24 秒,我愿意付这点时间成本。
# 旧脚本
obsutil cp dist/ obs://prod-h5-static/ -r -f -meta=Cache-Control:max-age=86400
# 新脚本
obsutil cp dist/assets/ obs://prod-h5-static/assets/ -r -f -meta=Cache-Control:max-age=2592000,immutable
obsutil cp dist/config/runtime.json obs://prod-h5-static/config/runtime.json -f -meta=Cache-Control:max-age=30,must-revalidate
obsutil cp dist/index.html obs://prod-h5-static/index.html -f -meta=Cache-Control:no-cache
obsutil setmeta obs://prod-h5-static/config/runtime.json -meta=Content-Type:application/json;charset=utf-8
CDN 规则也重排了,优先级 10 匹配 *.html,TTL=0 且遵循源站;优先级 20 匹配 /config/*,TTL=30 秒并强制校验 ETag;优先级 30 匹配 /assets/*,TTL=30 天,URL 参数参与缓存键。17:48 到 19:32 我做 10% 灰度,只放华南和华东。19:41 我手滑把 runtime.json 设成 no-store,OBS GET QPS 从 38 冲到 226,5 分钟后撤回,20:03 才稳定。10 月 21 日全天数据,CDN 命中率从 63.2% 到 93.7%,OBS GET 从 1,942,311 次/天降到 318,044 次/天,请求费用从 21.37 元/天降到 3.86 元/天,回源流量从 52.6 GB/天降到 8.9 GB/天。同类报错从 327 条/5 分钟降到 6 条/5 分钟,剩下 6 条里 4 条是用户插件改写脚本,2 条来自公司内网旧代理缓存。
我还没解决移动端弱网下首屏 CSS 1.8 秒空白,下一步准备把关键 CSS 内联到 6 KB 以内,再把 CDN Brotli 只对 text/css 和 application/javascript 开 5% 灰度,连续观察 48 小时的 LCP p75、回源带宽和 4xx 比例。
- 点赞
- 收藏
- 关注作者
评论(0)