Java

Spring Boot 配置改了还是旧值?从配置生效链判断卡在哪一层

15:28 配置中心已经把 `order.submit.timeout-ms` 改成了 800,`/actuator/env` 看起来也是新值,但业务日志里还在按 3000ms 超时。遇到这种现场,我不会先争 refresh 还是 restart,而是先找旧值到底停在了哪一层。

  • Spring Boot
  • 配置刷新
  • 配置中心
  • 环境变量
  • 故障排查
18 分钟阅读

配置问题最磨人的,不是值没改,而是你明明已经看到新值了,业务却还在按旧值跑。

15:28 那次现场就是这样。

  • 配置中心里,order.submit.timeout-ms 已经从 3000 改成了 800
  • /actuator/env 返回的也是 800
  • 但业务日志里,调用下游支付服务还是在 3000ms 才超时

如果这时候只围着“refresh 到底成没成功”转,很容易越查越乱。因为你会不停地看到互相矛盾的证据:

  • 页面是新的
  • env 也是新的
  • 行为却是旧的

碰到这种现场,我更愿意先做一件事:别先问为什么没生效,先问旧值现在停在哪一层。

旧值停在哪,决定你接下来该往哪一层查

我通常会把配置生效链粗暴地拆成下面几层:

层级你会看到什么如果这里还是旧值,通常意味着什么
配置中心 / 配置仓库页面或文件内容还是旧的还没真正发布到正确位置
Spring Environment/actuator/env 还是旧值客户端没收到、没加载,或 source 被覆盖
@ConfigurationProperties / configpropsenv 是新的,但绑定对象还是旧的绑定或 refresh 没真正发生
业务 Bean / 底层资源对象绑定对象是新的,但行为还是旧的对象初始化时就把旧值写死了,需要重建
实际业务行为指标、日志、超时边界仍像旧值上面某层看似更新,真正跑的那层没变

这张表不是为了显得系统,而是为了避免最常见的误判:

看到某一层是新的,就以为整条链都已经更新了。

线上最怕的就是这种半更新状态。

那次现场里,旧值其实卡在了底层对象

先看 env

{
  "property": {
    "source": "configserver:order-service-prod.yml",
    "value": "800"
  }
}

再看 configpropsOrderTimeoutProperties 也已经是 800

如果只看到这里,很容易得出结论:

  • refresh 没问题
  • 配置层已经对了
  • 问题不在应用内部

但我当时没急着下这个结论,而是继续看业务侧证据:

15:31:08 WARN  submit order timeout after 3000ms traceId=5d7e...
15:31:08 INFO  payment client readTimeoutMillis=3000 client=okhttp-payment

这一行日志非常值钱。它说明:

  • Spring 层面看到的是新值
  • 但真正发请求的那个 HTTP client,仍然拿着旧值 3000

也就是说,旧值不是停在 Environment,也不是停在 ConfigurationProperties,而是停在已经初始化完成的底层对象里

这时再讨论“配置中心发没发出去”,意义已经不大了。方向应该立刻切到:

  • 这个 client 是不是只在启动时初始化一次
  • refresh 以后有没有重建它
  • 它是不是根本不支持运行时刷新

所以我第二步才会问:这类配置,理论上到底该靠 refresh 还是 restart 生效

不是所有配置都适合同一个生效方式。

更适合 refresh 的

通常是这些:

  • 明确走 @ConfigurationProperties 重新绑定的普通业务参数
  • 某些可在运行时切换的开关值
  • 没有深度写入底层资源对象的轻量配置

更适合 restart 的

很多人最容易低估的是这一类:

  • 连接池大小
  • HTTP client timeout / 连接参数
  • 线程池核心参数
  • 某些初始化时就固化的 SDK 配置
  • 需要在 Bean 构造时一次性建立的底层资源

上面那次 order.submit.timeout-ms 就属于第二种。配置本身能 refresh 到 Spring 容器里,但底层 okhttp-payment client 在启动时已经按旧值构造好了,后面并不会因为你看到 env=800 就自动重建。

这也是为什么我不太喜欢一上来就问“refresh 成没成功”。

这个问题太大了。更准确的问法应该是:

你想改动的这个值,真正控制的是哪一层对象?那一层对象支持怎样的生效方式?

如果 restart 和 refresh 看起来都像对的,我才会去翻 source 漂移

当然,也不是所有“改了还是旧值”都卡在底层对象。

还有一类很常见:你以为自己改的是那份配置,实际实例根本没在用。

这时我会特别去翻下面几种 source 漂移:

环境变量覆盖

ORDER_SUBMIT_TIMEOUT_MS=3000

配置中心改成了 800,结果容器里环境变量还顶着 3000,那你看到的“旧值”就完全不奇怪。

挂载文件覆盖

仓库和配置中心都对,但 Pod 里挂着一份旧的 /app/config/order-prod.yml,优先级还更高。

启动参数写死

--order.submit.timeout-ms=3000
-Dorder.submit.timeout-ms=3000

这种差异不去看 /proc/1/cmdline,会一直卡在“明明页面是新值”的困惑里。

部分实例读的是不同 namespace / profile

这会造成更阴的现场:

  • A 实例看到的是新值
  • B 实例仍然读旧 namespace
  • 整体行为像“部分请求还是旧逻辑”

这种时候,问题看起来像 refresh 不彻底,实际上是 source 根本不一致。

我会用一条很短的核对链把它收住

真到线上,我更愿意按下面这个顺序走,而不是在页面、日志、猜测之间乱跳:

第一步:先找旧值停在哪一层

  • 页面?
  • env
  • configprops
  • 底层对象?
  • 实际业务行为?

先把停留点找出来,后面才不会乱。

第二步:再判断这类配置本来该怎么生效

  • 支持运行时 refresh?
  • 还是必须重建实例、重建 Bean、重建底层连接?

别把所有配置都当成“改完就应该立刻热生效”。工程上不是这样运转的。

第三步:如果理论上该生效,却还没生效,再查 source 是否漂了

这一步才去翻:

  • 环境变量
  • 挂载文件
  • 启动命令
  • profile / namespace
  • 部分实例差异

这样排,不一定最酷,但很少让我在第一轮里绕远路。

这类现场里,我最不信的一句话是“我已经看到新值了”

因为“看到新值”这五个字,信息量太低了。

你到底看到的是:

  • 页面上的新值
  • Environment 里的新值
  • 绑定对象里的新值
  • 还是业务真正用上的那个值

它们完全不是一回事。

所以碰到“配置改了还是旧值”,我现在最想确认的,从来不是一句宽泛的“刷新了吗”。

而是这一句更短的话:

旧值现在到底卡在哪一层?

只要把这个停留点找出来,后面的 refresh、restart、source 漂移,其实就都比较好判断了。