容器内 200,线上 404:商品图片全部失联的一次排查
上传接口返回 200、容器内直连对象存储也返回 200,只有从边缘入口访问图片是 404。记录我用二分法把范围压到反代层、最终定位到 nginx location 匹配顺序的排查过程。
- 线上排查
- nginx
- 对象存储
- 部署
这是校园二手交易平台上线的验收阶段。平台对外只保留一个边缘入口:/ 交给 Flutter Web 静态产物、/api 反代后端、/img 反代对象存储;数据库、缓存与 MinIO 都不暴露公网。图片走的就是 /img 这条路。
这段经历留给我的最大一条经验是:先确认哪一层是好的,比先猜原因更快。下面是这次从三个现象到根因的完整过程;事实与测量方式来自项目记录,机制解释会标注为「分析」。
问题:三个现象同时成立
- 上传返回 200 —— 接口层看起来完全正常。
- 容器内直连 MinIO 也返回 200 —— 存储里有文件,也能取到。
- 从边缘访问图片是 404 —— 用户看到的商品图全部是坏的。
我把这三个现象对齐之后,问题反而更清楚了:坏的只有一条路径,而不是整条链路。
排查:我把范围压到反代层
排查只用了一个动作:对比「容器内直连」和「边缘访问」。同一个对象,前者 200、后者 404,说明问题不在存储、也不在上传,而在中间的反代层。范围一下子从「整条链路」缩到了一层配置 —— 这是这次排查最省时间的一步。
根因:nginx location 的匹配顺序
把范围压到反代层之后,我才去读这一层的规则,定位到 nginx 的 location 匹配顺序:正则 location 的优先级高于前缀 location。项目后来新增过一条用正则写的静态资源规则,它把 /img/ 的请求也截走了 —— 于是这些请求没有去反代 MinIO,而是去找本地文件,自然 404。
修复:给前缀 location 加 ^~
修复是在 /img/ 与 /api/ 上加上 ^~ 前缀修饰符,让它们优先按前缀匹配、不再被后续的正则规则截走。复验结果:返回 200 image/png。
这个代价,我在做决策时就写过
当初为什么选择「边缘 nginx 反代 /img」而不是让对象存储直接对外、也不用预签名 URL?我的理由是:对外只剩一个入口,缓存、鉴权与可信 IP 的配置都集中在一处,对象存储的攻击面不暴露。同一份记录里,我也把它的代价写了进去:
图片流量全部经过边缘网关,网关的静态与反代规则必须写对,否则会出现「上传成功但访问 404」。
这句话当时只是风险提示,后来真的发生了。这件事之后我更在意把「代价」写清楚:它不是免责声明,而是下一次出事时的第一份线索。
验证:线上真实验收
| 项目 | 结果 | 测量方式 |
|---|---|---|
| 线上验收覆盖 | 15 个业务域、40+ 接口、60+ 断言全部通过;过程中发现 1 个真实缺陷,已修复并复验 | 对生产实例发真实 HTTP,按「注册 → 登录 → 认证 → 发布 → 交易 → 评价 → 举报 → 治理」完整闭环执行(2026-09-22) |
| 后端测试 | 263 项全绿 | mvn -B test,由 Testcontainers 提供真实 PostgreSQL 16 / Redis 7.4 / MinIO |
| 前端测试 | 210 项全部通过,flutter analyze 无问题 | flutter test 与 flutter analyze(2026-09-23) |
| 线上环境 | 腾讯云 CVM 4 核 / 3.6G,Ubuntu 24.04;edge nginx + app + minio + postgres + redis 五个容器全部 restart=unless-stopped | 线上部署实测 |
这张表也是我判断「修复没有带坏别处」的依据:图片缺陷是在这轮完整闭环验收里被抓到的,修好之后我按同一套断言复验,其他业务域继续通过。
复盘:这次真正带走的方法
- 我把「先二分定位层次,再看该层规则」变成了默认动作:容器内正常、边缘失败,这一个对比就把问题从存储层锁到了反代层。
- 我不再用「大多数路径正常」判断「系统正常」:三条路径里坏一条,用户看到的就是全部图片坏掉。
- 决策的代价要留在记录里:它会在未来的某次排查里变成第一线索。
同一条交付链上的其他真实问题
这个项目记录下来的线上问题不止这一个,它们的共同点是「现象指向的地方,往往不是根因所在」:
- 测试环境自洽:门禁脚本如果加载
.env,会把开发环境的 Redis 口令带进临时容器,导致 AUTH 失败 —— 所以脚本刻意不加载.env。 - 发布成功却提示失败:跳转写在
try块里,跳转失败会把「发布成功」误报为失败,用户重复点击,同一件商品被发到 9 件;修复是把跳转移出try并加发布锁,同时把反馈统一成「进行中 / 成功 / 失败」三态。 - 管理端列表崩溃:后端
total在某种序列化路径下是字符串,前端按整数解析直接崩溃;修复是统一容错解析。 - 数据库约束演进:历史数据有脏值时直接加唯一索引会失败,于是用 NOT VALID 外键 + 分批清理,并把清理步骤写进上线前清单。
- 并发处置一致性:两个管理员同时点同一条审核时,用条件更新(
WHERE status = 'PENDING')+ 受影响行数判断谁先生效,并写审计日志。
完整的架构(单端口边缘网关、双通道校园认证、交易状态机与信用沉淀)与验收数据在项目页里:查看 CampusTrade 项目详情。
韦坤计算机科学本科生 · 后端开发 / AI 应用开发方向
相关项目
面向高校的二手交易平台:校园身份认证(邮箱 / 学号双通道)作为发布硬前置,注册到评价的完整交易闭环与 100 分初始信用档案,单端口边缘网关交付,线上 40+ 接口 / 60+ 断言验收。
- Java 21
- Spring Boot 3.3.4
- MyBatis-Plus 3.5.7
- 还有 25 项技术
相关文章
忠实度 0.25 → 0.99:一次先怀疑评测、再怀疑模型的排查
复盘约 3 分钟忠实度只有 0.25,与人工观察明显不符。这是一次把嫌疑人先放在评测口径上的排查复盘:引用预览被截断污染了判分上下文,以及为什么我坚持给 RAG 项目先建可信的评测,再谈优化。