Java

Spring Boot 环境不一致怎么查?从 profile、挂载文件到 JVM 参数逐项核对

两个 Pod 跑着同一个镜像,返回结果却像两套服务。后来真正把差异找出来的,不是仓库里的 `application.yml`,而是容器里那几行 `SPRING_PROFILES_ACTIVE`、挂载文件和 `-D` 参数。

  • Spring Boot
  • Profile
  • 环境变量
  • JVM 参数
  • 故障排查
18 分钟阅读

“同一套代码,怎么会跑出两套结果?”

这类问题最容易让人一头扎进仓库配置,反复翻 application.ymlbootstrap.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-aprod-b
JDK17.0.1017.0.10
request/limit一致一致
节点池同组同组
/etc/localtimeAsia/ShanghaiUTC

这样一来,前面的 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

只要把这些差异落成文本,很多原本抽象的争论会立刻停下来。

这类问题最容易错的,不是技术点,而是起手顺序

我见过太多人一上来就按下面的顺序查:

  1. 翻仓库里的 yml
  2. 看配置中心页面
  3. 怀疑业务代码
  4. 最后才进容器

而更稳的顺序往往反过来:

  1. 先找一台异常实例和一台正常实例
  2. 先进容器看 profile、env、挂载文件、启动命令、JVM 参数
  3. 再看 /actuator/env/actuator/configprops
  4. 最后才回头解释为什么仓库和线上会分叉

这样查的好处是,第一轮就能把“环境不一致”从抽象判断,压成几行可对照的差异。

如果你只能先做一件事

我会建议:

别再问“是不是环境不一致”,直接去做一份异常实例和正常实例的运行时 diff。

只要 profile、挂载文件、启动命令、JVM 参数这几层真的摆到一起,线上多数“同一版本怎么跑出两套结果”的问题,都会从一句玄学,变成几行非常具体的差异。

差异一旦具体,排查基本就不再乱了。