Spring Boot 环境不一致怎么查?从 profile、挂载文件到 JVM 参数逐项核对
两个 Pod 跑着同一个镜像,返回结果却像两套服务。后来真正把差异找出来的,不是仓库里的 `application.yml`,而是容器里那几行 `SPRING_PROFILES_ACTIVE`、挂载文件和 `-D` 参数。
“同一套代码,怎么会跑出两套结果?”
这类问题最容易让人一头扎进仓库配置,反复翻 application.yml、bootstrap.yml、配置中心页面,最后越看越觉得自己没问题。
但线上真把问题查明时,决定性证据经常不在仓库里,而在运行现场。
有次 billing-api 就是这样。两个 Pod 用的是同一个镜像 digest,发布批次也一致,但表现完全不像一套服务:
| Pod | 现象 |
|---|---|
billing-api-6b7c8-prod-a | 账单时间按东八区切天,优惠规则正常 |
billing-api-6b7c8-prod-b | 账单时间按 UTC 切天,部分优惠规则像没生效 |
如果只看仓库配置,很容易觉得这事说不通。但我把对照拉到容器里以后,画面就清楚了。
同一镜像跑出两套结果时,我不会先去翻仓库 yml
原因很简单。
仓库里那份配置,线上未必真的在用。真正决定实例长成什么样的,通常是下面几层叠出来的结果:
- profile 组合
- 环境变量
- 挂载文件
- 启动命令
- JVM
-D参数 - JDK / 时区 / 容器资源这些基础环境
只要其中任何一层不同,实例就可能像两套系统。
所以我更愿意把问题改写成一句更具体的话:
这两个 Pod 最终启动时,到底吃到了什么不同的运行条件?
问题一旦这么问,证据就比较容易收。
那次把问题查明,靠的是四份对照
第一份:先看 profile,到底是不是同一组运行身份
第一眼我先查的不是业务配置,而是 profile。
kubectl exec billing-api-6b7c8-prod-a -- printenv | grep SPRING_PROFILES_ACTIVE
kubectl exec billing-api-6b7c8-prod-b -- printenv | grep SPRING_PROFILES_ACTIVE
结果两边果然不一样:
prod-a: SPRING_PROFILES_ACTIVE=prod,cn
prod-b: SPRING_PROFILES_ACTIVE=prod
别小看这一个逗号。它经常意味着:
- 某个区域配置没加载
- 某组 bean condition 没生效
- 挂载文件路径和 profile 约定对不上
- 配置覆盖顺序已经变了
但到这里我还不会立刻收工。因为 profile 不一致只是入口,不一定是终点。
第二份:再看挂载文件,仓库里有,不代表容器里真的有
继续进容器看 /app/config,差异就更直接了:
prod-a:
/app/config/application-prod.yml
/app/config/application-cn.yml
/app/config/rule/billing-region.json
prod-b:
/app/config/application-prod.yml
/app/config/rule/
也就是说:
prod-a不但 profile 带了cn- 对应的
application-cn.yml也挂进去了 prod-b则少了这层区域挂载
这时候“为什么有些优惠规则像没生效”已经不难理解了。它根本不是同一套运行条件。
线上这类问题最危险的一点是,大家很容易把“镜像一样”误听成“环境一样”。其实不是。
镜像只说明你带上了什么,不说明启动时到底用上了什么。
第三份:再看启动命令和 JVM 参数,很多关键差异都藏在 -D 里
真正让我彻底确认方向的,是 /proc/1/cmdline 和 JVM 自身的参数。
kubectl exec billing-api-6b7c8-prod-a -- sh -c 'cat /proc/1/cmdline | tr "\0" " "'
kubectl exec billing-api-6b7c8-prod-b -- sh -c 'cat /proc/1/cmdline | tr "\0" " "'
出来以后,两边的差异比我想象得还直白:
prod-a:
java -Xms2g -Xmx2g -Duser.timezone=Asia/Shanghai -Dspring.profiles.active=prod,cn -jar app.jar
prod-b:
java -Xms2g -Xmx2g -Duser.timezone=UTC -Dspring.profiles.active=prod -jar app.jar
这就不只是“某个配置文件没挂上”了,而是连 JVM 层的时区都不同。
为什么 prod-b 会按 UTC 切账单日,到这里已经不用再猜。
我后来越来越相信一件事:很多环境不一致,不是复杂,而是差异就明晃晃写在启动命令里,只是没人去看。
第四份:最后再补基础环境,避免把运行现实漏掉
如果前三份对照还没收住,我才会继续补基础环境:
- JDK 版本
- cgroup limit / request
- 节点标签
- DNS / hosts
- 容器时区文件
像这次,我顺手补了一眼:
| 项目 | prod-a | prod-b |
|---|---|---|
| JDK | 17.0.10 | 17.0.10 |
| request/limit | 一致 | 一致 |
| 节点池 | 同组 | 同组 |
/etc/localtime | Asia/Shanghai | UTC |
这样一来,前面的 profile 差异、挂载差异、-Duser.timezone 差异就形成了闭环,不会再停在“可能是环境问题”这种空话上。
我更愿意做对照 diff,不太相信口头确认
线上最常听到的几句话是:
- “这两个 Pod 应该一样吧。”
- “镜像就是同一个。”
- “配置中心页面没差异。”
- “启动参数应该也是统一模板。”
这些话在开会时听起来很顺,但对排查帮助很有限。
我更想看到的是这种东西:
kubectl exec bad-pod -- printenv | sort > /tmp/bad.env
kubectl exec good-pod -- printenv | sort > /tmp/good.env
diff -u /tmp/good.env /tmp/bad.env
kubectl exec bad-pod -- ls -R /app/config > /tmp/bad.mount
kubectl exec good-pod -- ls -R /app/config > /tmp/good.mount
diff -u /tmp/good.mount /tmp/bad.mount
kubectl exec bad-pod -- sh -c 'cat /proc/1/cmdline | tr "\0" " "' > /tmp/bad.cmd
kubectl exec good-pod -- sh -c 'cat /proc/1/cmdline | tr "\0" " "' > /tmp/good.cmd
diff -u /tmp/good.cmd /tmp/bad.cmd
只要把这些差异落成文本,很多原本抽象的争论会立刻停下来。
这类问题最容易错的,不是技术点,而是起手顺序
我见过太多人一上来就按下面的顺序查:
- 翻仓库里的 yml
- 看配置中心页面
- 怀疑业务代码
- 最后才进容器
而更稳的顺序往往反过来:
- 先找一台异常实例和一台正常实例
- 先进容器看 profile、env、挂载文件、启动命令、JVM 参数
- 再看
/actuator/env和/actuator/configprops - 最后才回头解释为什么仓库和线上会分叉
这样查的好处是,第一轮就能把“环境不一致”从抽象判断,压成几行可对照的差异。
如果你只能先做一件事
我会建议:
别再问“是不是环境不一致”,直接去做一份异常实例和正常实例的运行时 diff。
只要 profile、挂载文件、启动命令、JVM 参数这几层真的摆到一起,线上多数“同一版本怎么跑出两套结果”的问题,都会从一句玄学,变成几行非常具体的差异。
差异一旦具体,排查基本就不再乱了。