Spring Boot 2.2 + Micrometer 线程池监控导致 JVM 指标丢失问题复盘

一、背景

1.1 业务背景

系统需要对 自定义线程池 添加监控指标,并将数据:

  • 暴露至 Prometheus
  • 在 Grafana 中展示线程池运行情况

监控方式基于 Micrometer + Prometheus

1.2 技术背景

  • Spring Boot 2.2.x
  • Micrometer 1.3.x
  • Spring Actuator
  • PrometheusMeterRegistry
  • 自定义 AsyncConfigurer 线程池

1.3 目标与预期行为

在引入线程池监控后:

  • Prometheus /actuator/prometheus 端点可正常访问
  • JVM 指标(如 jvm_*process_uptime_*)依然存在
  • 新增线程池指标不影响原有监控体系

二、问题现象

2.1 正常情况下的表现(基线行为)

使用 默认线程池,未引入任何线程池监控逻辑:

curl http://localhost:8080/actuator/prometheus | grep jvm_

结果:

  • 能看到完整的 jvm_process_ 等 JVM 相关指标
  • Prometheus 抓取正常

2.2 异常情况下的表现

启用 自定义线程池,并在其构造函数中注入 MeterRegistry 后:

curl http://localhost:8080/actuator/prometheus | grep jvm_

结果:

  • 无任何输出
  • JVM 指标完全消失
  • 服务日志与功能行为均正常

2.3 异常的外在特征

  • 应用可正常启动
  • 业务逻辑无异常
  • Prometheus 端点存在
  • 仅 JVM 监控指标缺失

三、复现过程

示例代码在附录中。

3.1 场景一:默认线程池(对照组)

  • 未启用自定义 AsyncConfigurer
  • 未注入 MeterRegistry

结果:

  • JVM 指标存在
  • 行为符合预期

3.2 场景二:自定义线程池 + 构造器注入 MeterRegistry(问题场景)

此时把 AsyncTaskExecutePool 类上的 注释放开,启动服务。发现日志正常打印,且替换为了默认的线程池。
再执行

curl -XGET http://localhost:8080/actuator/prometheus | grep up
curl -XGET http://localhost:8080/actuator/prometheus | grep jvm_

结果:

  • 服务正常启动
  • Prometheus 端点正常
  • JVM 指标全部丢失

测试完成后,还原 AsyncTaskExecutePool 类。

3.3 场景三:恢复默认配置后的验证

把 AsyncConfig 类上的注释放开。启动服务,发现日志正常打印,且

curl -XGET http://localhost:8080/actuator/prometheus | grep up
curl -XGET http://localhost:8080/actuator/prometheus | grep jvm_

结果:

  • JVM 指标恢复
  • 问题可重复、可回退

四、根因分析(核心)

4.1 表面原因(直接触发点)

AsyncConfigurer构造器中注入了 MeterRegistry

public AsyncTaskExecutePool(MeterRegistry meterRegistry)

即使未使用该对象,仅注入本身就会触发问题。

4.2 深层原因(Spring 生命周期层面)

关键事实:

构造器注入 = 强制提前创建依赖 Bean

在 Spring 启动过程中:

  1. AsyncConfigurer 属于 异步基础设施 Bean
  2. Spring 会在非常早的阶段解析并实例化它
  3. 为了创建该 Bean,Spring 必须先创建 MeterRegistry
  4. 导致 PrometheusMeterRegistry提前 fully initialized

4.3 关键机制解释

4.3.1 Bean 创建顺序

  • AsyncConfigurer → 极早创建
  • MeterRegistry → 被动提前创建
  • JVM Metrics Binder → 尚未创建

4.3.2 MeterRegistryPostProcessor 的行为

MeterRegistryPostProcessor 是一个 BeanPostProcessor,其职责是:

  • MeterRegistry 初始化完成后
  • 将当前容器中已存在的 MeterBinder 绑定到 Registry 上

但它 只执行一次

4.3.3 关键时间点问题

MeterRegistryPostProcessor 执行时:

  • JVM Metrics Bean 尚未创建
  • Binder 列表为空
  • 绑定结果为空集合

之后即使 JVM Metrics Bean 创建完成,也不会再补绑定。

4.4 Micrometer 1.3 + Spring Boot 2.2 的已知缺陷

在该版本组合中:

  • MeterRegistry 一旦 early-init
  • Binder 只会 bind 一次
  • 后创建的 Binder 永久失效

这是 设计缺陷,不是业务代码错误。

五、为什么问题只在该场景出现

5.1 为什么“只是注入”也会出问题

因为构造器注入会:

  • 强制提前创建 MeterRegistry
  • 打乱 Micrometer 预期的初始化顺序

5.2 为什么默认线程池不会触发

  • 未实现 AsyncConfigurer
  • 未参与异步基础设施初始化
  • MeterRegistry 按正常顺序创建

5.3 为什么自定义 AsyncConfigurer 会触发

  • 异步配置优先级极高
  • Spring 为保证异步可用性,会尽早实例化相关 Bean

5.4 为什么日志和业务行为都正常

  • 监控缺失不会影响业务功能
  • JVM Metrics 缺失属于“无声失败”
  • 不会抛异常或警告

六、解决方案与验证

6.1 临时规避方案(当前版本可用)

避免在以下位置注入 MeterRegistry

  • AsyncConfigurer 构造器
  • 任何早期基础设施 Bean 的构造函数

可改为:

6.2 推荐修复方案(结构性)

  • 将线程池监控绑定逻辑移出异步配置类
  • 使用独立的、非基础设施 Bean 完成监控注册

6.3 根本性解决方案(推荐)

升级框架版本:

  • Spring Boot ≥ 2.5
  • Micrometer ≥ 1.6

新版本中:

  • Binder 支持补绑定
  • early-init 不再导致指标永久丢失

6.4 修复后的验证方式

curl http://localhost:8080/actuator/prometheus | grep jvm_

确认 JVM 指标存在且持续更新。

七、影响评估

7.1 功能影响

  • 不影响业务功能
  • 不影响线程池实际运行

7.2 监控风险

  • JVM 内存、GC、线程等指标完全缺失
  • Grafana 面板失真
  • SRE 无法准确判断系统健康状态

7.3 风险级别

  • 高隐蔽性
  • 高运维风险
  • 极难通过日志发现

八、经验总结(Lessons Learned)

8.1 技术教训

  • 构造器注入不是“无副作用”
  • early-init 在监控体系中是高风险行为

8.2 设计反思

  • 基础设施 Bean 不应依赖监控组件
  • 监控应尽量解耦于核心启动链路

8.3 团队级约束建议

明确约定:

Spring Boot 2.2 + Micrometer 1.3中,

  • 禁止在 AsyncConfigurer 及其他早期基础设施 Bean 的构造器中注入 MeterRegistry
  • 禁止使用 AsyncConfigurer

九、附录

9.1 相关代码

示例工程代码

9.2 相关组件

  • AsyncConfigurer
  • MeterRegistry
  • PrometheusMeterRegistry
  • MeterRegistryPostProcessor

9.3 适用范围

  • Spring Boot 2.2.x
  • Micrometer 1.3.x

9.4 结论性说明

这是一个由 Spring 生命周期顺序 + Micrometer 设计缺陷 共同触发的问题,并非业务代码错误,但必须通过架构约束或版本升级避免。

作者:张三  创建时间:2026-01-14 14:20
最后编辑:张三  更新时间:2026-01-14 17:39