← 全部文章

图片全碎了,源站却全绿——一次上游故障的定责复盘

同一批图:源站 18/18 全 200,加速域名全挂。我误判了两次,最后靠「换个桶再试一次」定责

图片全碎了,源站却全绿

用户报上来时只带了一张截图:出图工作台里「最近出图」四张全是碎图,连上传参考图的预览也是碎的。 一句话是「今天很多图片无法正常显示」。

而同一批图,我从服务器上直接拉源站,18 个样本 18 个 200

这就是这类故障最费时间的地方:看起来最像我们自己的 bug,实际上不是。

先把「上游」和「我们」切开

系统里图片地址有两层身份:

库里存的永远是源站地址   https://img.webkubor.online/...
                            │  镜像回源(按需拉取 + 落盘)
                            ▼
渲染时才派生出的加速地址  https://<bucket>.s3.bitiful.net/...

库里存源站、显示时换成国内加速域名,这样数据库不被 CDN 绑定 —— 设计上没问题。 问题是换域名只做了字符串替换:加速域名一旦不可用,浏览器只会显示碎图,永远不会回源

所以第一步不是看代码,是把同一批对象在两个域名上各打一遍:

对象 源站 加速域名
今天新生成 3 张 200 / 200 / 200 424(回源失败)/ 挂死 / 挂死
8 月初老图 3 张 200 ×3 404 ×3
已缓存的老图 4 张 200 ×4 200 ×4

源站一张没坏。 这就把范围从"整个出图链路"缩到了"派生出来的那个地址"。

误判一:以为是密钥轮换

我的第一个假设很顺:源站是 R2,加速是别人的镜像桶,镜像要拿 R2 的密钥回源; 密钥库里的 R2 凭据备注写着 r2-s3-rclone-20260901,而加速桶里最后一个成功落盘的对象停在 8-31。 “9-01 轮换了密钥、忘了同步给镜像” —— 时间线严丝合缝。

然后被两件事同时推翻:

  • 账号的 API token 列表里,最近一次改动就是 9-01 那一次,之后没有任何轮换
  • 更要紧的是"昨天还好好的"。9-01 坏的话,用户 9-01 就该来报,不会等到今天。

时间线是最便宜的证伪工具。 我拿到"最后成功/首次失败"的两个时间点之后,这个假设就死了。

误判二:以为只是这个桶的配置

把时间点对齐后,我做了个对照:同一个账号下另一个项目也有个加速桶。

一个不存在的 key:

  • 那个桶:0.05 秒回 404(干净利落)
  • 我们的桶:挂死 12 秒(连接都通,就是等不到响应)

“只有我们的桶挂 → 我们桶的回源配置写错了”。这个推理看起来也没问题 —— 直到我意识到: "不存在"和"存在但没缓存"走的不是同一条路。 有回源规则的路径会去回源(挂), 没规则的路径直接 404(秒回)。我把两种不同路径的表现当成了桶与桶的差异。

决定性实验:换个桶再试一次

真正定责只用一步:往源站放一个全新对象,然后从另一个项目的桶去取。

写入 R2:hym/__probe_<ts>.txt
源站取:  200                    ← 源站有货
hym 桶取: 000 / 25 秒超时        ← 也是挂的

换桶、换项目,回源照样挂。结论落地:账号级的回源能力失效,不是我们这个桶配错了。 (探针对象用完即删,复查过。)

这一步之所以快,是因为它把"我们的配置"这个变量消掉了:同一个账号、同一套机制、 另一个我不可能配错的桶 —— 它也不工作,那就不是配置问题。

为什么"昨天还好好的"

加速的响应带 cache-control: max-age=604800 —— 7 天

已经在边缘或浏览器缓存里的图,继续正常显示;只有"缓存里没有、必须回源"的对象才暴露。 所以用户看到的是突然很多图碎了,而不是"全站立刻全碎";也所以昨晚 22:18 还正常、今早 9:07 就挂了。

部分失败是最像自己的 bug:一半好一半坏,很容易让人往"某段代码写错了"上想。 它的指纹其实很明确 —— 按需路径挂了、存量路径没事

我们这边有没有嫌疑:逐条排除

嫌疑 怎么排的 结果
代码改动 故障窗口内仓库零提交(当天两笔在故障之后,第一次部署晚 3 小时 45 分) 排除
云账号配置 审计日志查了窗口内全部变更:全是无关站点的证书与 DNS 排除
源站凭据 账号 token 列表:窗口内无改动;当前凭据实测有效 排除
源站本身 18 个样本 + 一个全新写入对象,全部 200 排除

剩下的只有一个方向:加速服务商账号侧。那不在我们手里 —— 但也不该只能等用户来报

真问题:这是用户先发现的,不是告警先发现的

翻一眼我们的巡检:接口一律 200,产品库、素材库、健康检查全绿。 接口全绿、图片全碎 —— 巡检看的是状态码和返回结构,图片根本不在它的视野里。

已补上:一次探四张,两类对象缺一不可。

探什么 为什么
稳定对象(品牌 logo)× 两个域名 验证图床域名还活着(DNS / 证书 / 边缘 / 存储)
最新一张出图(实时查库取)× 两个域名 验证回源还通不通 —— 只探稳定对象会完全漏掉这次故障

判定上分两级:源站取不到图计入飞书告警(用户会看到碎图,真故障); 加速域名只报状态不告警(它挂了不影响功能,前端有兜底),但会带在告警卡里 —— 恢复的时候一眼就能看出"可以重新打开了"。

补丁自己也有一个 bug

第一版巡检上线,我立刻打了一次线上接口:返回空。同一个 handler 在本地 3.5 秒就回来了。

差别在出网路径:那个"挂死的加速请求"从 Cloudflare 出去时等不到响应, 而我的巡检代码里没有任何超时 —— 一个挂死的请求把整个端点拖住,CI 那行 wget 自然也拿不到东西。

巡检端点自己变成静默失效,正是它要防的那类问题。 现在每个请求都带 6 秒超时, 教训直接写进注释:巡检端点自己必须有超时,否则它会把"被探对象挂死"变成"探针挂死"。

落地:改了什么

  1. 不再依赖那个加速域名:构建期不再注入,图片直接走源站。国内加载慢一点 (同一张图实测 6 秒 vs 0.4 秒)—— 拿速度换"显示得出来",值。
  2. 加回源兜底:加速地址取图失败、或 2.5 秒没加载完,自动换回源站重试一次。 用捕获阶段的资源错误事件(这类错误不冒泡)+ 超时判定,一处覆盖全部 27 个显示点, 不用改 15 个视图文件。端到端实测:挂死的图从 naturalWidth 0 变成 1104。
  3. 巡检补图片:每天早晚各一次,源站出问题直接推飞书。
  4. 恢复判据写进巡检:等「加速·最新出图」重新变绿,再把构建开关打开即可。

还没解决的,和不在我们手里的

加速服务商账号侧的回源故障仍在。他们的控制台接口只认登录态(我们试过 6 种鉴权写法, 一律"请登录后再进行操作"),所以余额、配额、账号状态都查不到 —— 这件事要么人登录去看,要么继续等。

不影响看图,只影响速度。 这条边界得说清楚,不然会让人以为"还没修好"。

三条可以复用的

  1. 先证明不是自己,再谈修。 同一批对象在两个域名/两个环境各打一遍,比读代码快得多。
  2. 换一个"我不可能配错"的同类对象再试一次。 这是最省时间的定责实验 —— 变量被消掉了,结论就没得争。
  3. 部分失败要读它的指纹。 存量好、按需挂,说明问题在按需那条路上; 顺着"是不是我们代码写错了"找,会绕很久。

还有一条是流程上的:这次能查得这么快,是因为源站的凭据在密钥库里; 而那个加速桶,谁都进不去 —— 因为当初建完,没有任何地方登记过它的凭据。 “实际在用但台账没登记”,本身就是下一次故障的加速器。