Spring Boot 配置改了还是旧值?从配置生效链判断卡在哪一层
15:28 配置中心已经把 `order.submit.timeout-ms` 改成了 800,`/actuator/env` 看起来也是新值,但业务日志里还在按 3000ms 超时。遇到这种现场,我不会先争 refresh 还是 restart,而是先找旧值到底停在了哪一层。
配置问题最磨人的,不是值没改,而是你明明已经看到新值了,业务却还在按旧值跑。
15:28 那次现场就是这样。
- 配置中心里,
order.submit.timeout-ms已经从3000改成了800 /actuator/env返回的也是800- 但业务日志里,调用下游支付服务还是在
3000ms才超时
如果这时候只围着“refresh 到底成没成功”转,很容易越查越乱。因为你会不停地看到互相矛盾的证据:
- 页面是新的
env也是新的- 行为却是旧的
碰到这种现场,我更愿意先做一件事:别先问为什么没生效,先问旧值现在停在哪一层。
旧值停在哪,决定你接下来该往哪一层查
我通常会把配置生效链粗暴地拆成下面几层:
| 层级 | 你会看到什么 | 如果这里还是旧值,通常意味着什么 |
|---|---|---|
| 配置中心 / 配置仓库 | 页面或文件内容还是旧的 | 还没真正发布到正确位置 |
Spring Environment | /actuator/env 还是旧值 | 客户端没收到、没加载,或 source 被覆盖 |
@ConfigurationProperties / configprops | env 是新的,但绑定对象还是旧的 | 绑定或 refresh 没真正发生 |
| 业务 Bean / 底层资源对象 | 绑定对象是新的,但行为还是旧的 | 对象初始化时就把旧值写死了,需要重建 |
| 实际业务行为 | 指标、日志、超时边界仍像旧值 | 上面某层看似更新,真正跑的那层没变 |
这张表不是为了显得系统,而是为了避免最常见的误判:
看到某一层是新的,就以为整条链都已经更新了。
线上最怕的就是这种半更新状态。
那次现场里,旧值其实卡在了底层对象
先看 env:
{
"property": {
"source": "configserver:order-service-prod.yml",
"value": "800"
}
}
再看 configprops,OrderTimeoutProperties 也已经是 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 漂移,其实就都比较好判断了。