三层断点:新功能上线的"最后一百米"

园区平台的消防设备地图功能上线那天,楼层平面图一张也显示不出来。从 dev 联调一路追到生产 Nginx,最后发现这不是一个 bug,而是三层断点叠在一起:库表迁移没跑、前端 env 缺配置、Nginx 缺路由。任何一层单独修好,页面依然是坏的——这大概是”新功能第一次把文件服务暴露到浏览器侧”时最典型的事故形态。

三层断点:库表、前端 env、Nginx 路由
图 1:dev 环境里这三层各自被本地库、本地 env 覆盖文件、直连端口悄悄绕过,生产环境一个都绕不过。

故障时间线

  1. 前端的直觉:图片只有直连后端端口才能打开,同事问”不应该走网关吗?要不要配 Nginx?”——这个直觉问到了点子上,只是当时没人知道答案。
  2. 线上第一爆Table 'building_env.fire_map_plan' doesn't exist。新表压根没建,接口直接 500。
  3. 线上第二爆:表建好后图片 404,但 URL 已经变成生产入口地址——说明前端那层已经有人配过了,断点往后移到了 Nginx。
  4. 终验:Nginx 加完路由,curl -I 返回 200 image/jpeg,页面刷新可见。

第一问:这个绝对地址到底是谁拼的?

图片打不开,最忌讳一上来就改 Nginx。先分清 URL 是谁拼的——绝对地址只有三种来源:后端配置、后端代码、前端 env。逐一排除后,嫌疑落在前端组件的 computed 上:

1
2
3
4
5
planImageUrl() {
const url = this.floor.planUrl
if (url.startsWith('http') || url.startsWith('data:')) return url
return process.env.VUE_APP_IMGPATH + url // 相对路径拼前缀
}

问题就出在最后这行:VUE_APP_IMGPATH 全项目只有这一处引用,而 .env.production.env.development.env.staging 全都没定义它。生产构建出来的地址是 undefined/fs/...,必然 404。

为什么 dev 联调时没人发现?因为每位前端同事本地都有一份不进 git 的 .env.development.local,各自配了后端地址。每个人本地各配各的,恰好集体掩盖了生产配置的缺失。 这是”三层断点”得以潜伏的土壤。

第二问:后端返回的到底是相对还是绝对路径?

前端拼前缀的前提是后端给相对路径。这个前提不能想当然,要实证。追进框架 jar(仓库无源码,反编译确认)后发现两点:

  • 上传接口返回的 fileUrl 就是 /fs/... 纯相对路径,环境无关;
  • 那个看起来很关键的 ${icfg.common.fs-server} 配置,注入之后零次引用——是死配置,配了也不会有任何效果。

再用数据库闭环:表里存的确实是 /fs/firePlan/20260814/xxx.png数据链路从头到尾都是对的,断点纯粹在前端 env 与 Nginx 两处。

看到名字顺眼的配置项就想往配置中心里塞一个值,是一种本能。但”存在这个 @Value“不等于”有人读它”——本次的 fs-server 就是反编译确认过的死配置。改配置之前,先确认这行配置活着。

修复:三层各补一刀

第一层,建表。 本项目的 Flyway 不集成进服务、在服务器上以 CLI 单独执行,所以”代码发了但表没建”是常态而非意外。手动跑完迁移脚本即可,不需要重启——MyBatis 运行时查库,表一出现接口立即恢复。

第二层,前端 env。 .env.production 补上 VUE_APP_IMGPATH,让相对路径拼出生产同源前缀,重新构建。

第三层,Nginx 加路由。 生产实测拓扑是:入口是一台独立的 Nginx 机器,Java 服务在另一台机器上,跨机转发要写内网地址。在 location /gateway/ 旁边加:

1
2
3
location /fs/ {
proxy_pass http://172.16.35.96:18100; # 末尾不带斜杠!
}

nginx -t && nginx -s reload,真实请求打出 200,收工。

本次最大的坑:proxy_pass 末尾那根斜杠

proxy_pass 不带路径部分时,Nginx 把原始 URI 原样转发;一旦写成带斜杠的 http://host:18100/,Nginx 会把匹配到的 /fs 前缀剥掉再拼接到 / 后面——后端收到的是 /xxx,404。这是官方文档里写得明明白白、但几乎每个团队都要踩一次的行为。

更阴险的是:同一个配置文件里,location /gateway/ 那条是故意带斜杠的——它就是要剥掉 /gateway 前缀再交给网关。两条 location 需求恰好相反,照抄邻居的写法必踩坑。改 Nginx 配置时真正要紧的只有三件事:行尾分号、花括号闭合、proxy_pass 带不带斜杠。缩进和空格,Nginx 根本不在乎。

为什么不走网关?

<img> 标签发起的请求不带 Authorization 头,走网关会被鉴权拦截;要放行就得在鉴权白名单上开口子。而 /fs 免鉴权直连本就是框架的既有设计,Nginx 同源反代恰好是最贴合这个设计的做法——顺带还消掉了 canvas 的跨域污染问题。另外,Content-Disposition: attachment 只影响浏览器直接打开 URL 的场景(会变成下载),<img> 渲染不受任何影响。

方法论:三层断点模型

新功能第一次把文件服务暴露到浏览器侧时,”库表迁移 / 前端 env / Nginx 路由”这三层默认全是断的——dev 环境里它们各自被本地库、.env.development.local、直连端口绕过去了,生产一个都绕不过。只补一层就卡在下一层,所以上线清单要把三层一起列进去。

本次验证有效的排查顺序:

  1. 先分清 URL 是谁拼的(前端 env / 后端配置 / 框架代码),别急着动网络层;
  2. 查库,确认存的到底是相对还是绝对路径;
  3. 从最底层 curl 服务直连,证明”服务本身正常”;
  4. 沿链路逐层向上,每层用最小实验证明”这层通,还是不通”。

排障的本质不是猜,而是把一条长链路切成若干可以用一次 curl 判定生死的短段

参考资料