Spring Boot监控实战:Micrometer与JVM指标解读及Prometheus接入
最近在把一个老项目从 Spring Boot 2.3 升级到 2.7,又顺带整理监控体系时,发现团队里不少人对 Micrometer 和 JVM 指标的理解还停留在“能 ping 通、能看内存条”的水平。做后端时间久了就会明白,应用监控这件事,绝不是上线后装个 Agent 就完事。真正靠谱的监控,是从 JVM 内存、GC 行为、线程状态这些底层指标一路追到业务接口的调用量、错误率、响应耗时。这篇主要围绕 Spring Boot 应用监控里的两个核心角色——Micrometer 和 JVM 指标——讲清楚它们是怎么配合工作的、关键指标该怎么解读、以及实际接入 Prometheus 时我会怎么落配置。适合刚接触 Spring Boot Actuator 的开发者,也想给在集成监控但被各种指标名绕晕的同学一份“指路图”。
1. 为什么做应用监控:从“能跑”到“跑得稳”
1.1 监控到底解决什么问题
很多人以为监控就是出问题时看一眼日志。其实日志和指标是两个维度的东西。日志描述的是“某时刻发生了什么”,指标描述的是“系统整体处于什么状态”。Spring Boot 应用跑得好不好,不能用“进程还在”来定义。进程活着但线程池打满、接口超时、GC 停顿导致请求阻塞,这些靠日志很难提前发现。指标监控的意义,是把 JVM 内部状态和业务吞吐量量化,让你在故障发生前就看出趋势。比如 jvm.threads.live 持续上涨,说明线程没释放;jvm.gc.pause 秒级飙升,说明堆压力大。这些数字比一句“最近系统有点卡”要精确得多。
1.2 监控体系的分层
监控体系我习惯分成三层。最底下是基础设施监控,也就是 CPU、内存、磁盘、网络,这层用云监控或者 Prometheus + node_exporter 就能覆盖。中间层是 JVM 和应用框架监控,包括堆内存用量、GC 次数、线程数量、HTTP 连接池、数据库连接池等。最上层是业务指标,比如订单量、支付成功率、消息队列积压量。Spring Boot 应用监控同时覆盖中间层和上层:Actuator 提供端点,Micrometer 负责把 JVM 指标和自定义业务指标统一采集,再通过注册表推送到监控系统。这三层缺一不可,但很多团队只做了第一层,导致应用被流量打死时,CPU 和内存看着还正常,非常尴尬。
2. Micrometer:指标采集的门面与桥接层
2.1 Micrometer 是什么,为什么 Spring Boot 选它
Micrometer 是一个指标门面库,作用有点像日志界的 SLF4J。应用代码里只面向 Micrometer 的 API 写指标,不关心最终打到 Prometheus、InfluxDB、Graphite 还是 Datadog。Spring Boot 从 2.0 开始把 Micrometer 深度集成进 Actuator,默认的 JVM 指标、Tomcat 指标、数据源指标都是通过它暴露的。选它的理由很实在:一套 API,多端输出,避免你在业务代码里硬编码某个监控系统的客户端。而且 Spring Boot 自动配置帮我们做了大部分事,引入 spring-boot-starter-actuator 后,Micrometer 核心就自动进来了,大部分 JVM 指标是开箱即用的。
2.2 Meter 类型:Counter、Gauge、Timer、DistributionSummary
Micrometer 里有四种最常用的 Meter 类型,选错类型会导致指标含义混乱。
Counter 是单调递增计数器,适合统计累计值,比如请求总数、错误总数。它只增不减,哪怕重启也会从上次的存储值或从 0 重新开始计数。Gauge 表示瞬时值,适合线程数、内存用量、队列长度这种“当前多少”的指标。Timer 用来测量短耗时操作的次数和总耗时,既能统计请求次数又能算平均耗时、P95、P99。DistributionSummary 用来分析事件分布,适合记录响应体大小、订单金额这类可分布的值,和 Timer 的差别在于它只记录分布,不自动算时间。
我踩过的一个典型坑:有人用 Counter 存线程数,结果线程数下降时计数不会降,因为 Counter 不可减,导致监控图上线程数只涨不跌。线程数必须用 Gauge。所以初学 Micrometer,先弄清楚 Meter 类型,再写指标,比直接抄代码重要得多。
2.3 如何在 Spring Boot 中引入 Micrometer
如果只做 JVM 监控,引入 spring-boot-starter-actuator 就够了。要输出为 Prometheus 格式,再加一个 micrometer-registry-prometheus。Maven 的坐标大致是:
然后配置暴露端点:
这里有个关键点:Spring Boot 2.x 默认只暴露 health 端点,其他端点需要手动指定 include。Spring Boot 3.x 也一样。很多新人以为引了包就能访问 /actuator/prometheus,实际上没配置直接 404。另外 exposer 配置里别把 prometheus 漏了,否则数据出不来。
3. JVM 指标解读:内存、GC、线程、类加载
3.1 JVM 内存模型快速回顾
JVM 内存不是一块,而是分区域管理。堆内存主要存对象实例和数组,里面又分新生代、老年代,新生代再分 Eden、Survivor From、Survivor To。非堆内存里,Metaspace 存类元数据,Code Cache 存 JIT 编译产物,还有线程栈、直接内存。这些区域在监控里的表现完全不同。比如 Metaspace 上涨通常和动态生成类有关,比如 CGLIB 代理、反射生成类;线程栈上涨和线程数相关,可能导致 native memory 耗尽。理解内存模型,才能在看到 jvm.memory.used.bytes 时知道是哪个区域在涨,而不是一味调大堆内存。
3.2 关键 JVM 指标与含义
在 /actuator/metrics 里,Spring Boot 自动注册了一组以 jvm. 开头的指标。我整理一份最常用的参考表:
| 指标 | 含义 | 常见异常信号 |
|---|---|---|
| jvm.memory.used.bytes | 已用内存,标签 area=heap/nonheap,id=区域名 | 接近最大值 |
| jvm.memory.max.bytes | 区域最大内存 | 与配置有关 |
| jvm.memory.committed.bytes | JVM 已向 OS 申请的内存 | committed 过高说明扩容 |
| jvm.gc.pause | GC 停顿时间和次数 | 平均/最大耗时突增 |
| jvm.gc.live.data.size | GC 后老年代存活对象大小 | 持续上涨说明有内存泄漏 |
| jvm.threads.live | 活跃线程数 | 持续上涨形成“锯齿” |
| jvm.classes.loaded | 加载类数量 | 异常上涨考虑类加载泄漏 |
实际用 Prometheus 查询时,特别关注这两条:jvm_gc_pause_seconds_sum 和 jvm_gc_pause_seconds_count。注意 Micrometer 输出到 Prometheus 后,时间单位会转换为秒,所以指标名和原始字节数不同。别拿着文档里的 jvm.gc.pause 去 Prometheus 搜,搜不到很正常的,Prometheus 底下会变成 jvm_gc_pause_seconds_sum。
3.3 如何通过 Actuator 查看 JVM 指标
不用任何可视化工具,直接访问 /actuator/metrics 就能看到当前应用有哪些指标。想看某个指标的具体维度,比如 jvm.memory.used.bytes,可以访问:
返回 JSON 会带 availableTags,里面有 area、id、name。想过滤某个区域,还可以继续加 tag:
这一步排查时特别有用。比如怀疑堆内存泄漏,只过滤 heap 区域看数据;怀疑元空间问题,过滤 nonheap。直接看完整 JSON 会被几十个序列刷屏。
4. 把指标接出去:Registry 与 Prometheus 示例
4.1 为什么需要一个注册表(Registry)
老手看代码会发现,Micrometer 的 API 都挂在 MeterRegistry 上。Registry 是 Meter 的容器与生命周期管理器。它负责创建、缓存、聚合 Meter,并定期把数据上报到后端。Spring Boot 默认会注入一个复合注册表,把多个后端注册表集合在一起。你写代码时只要注入 MeterRegistry,不用关心当前后端是 Prometheus 还是 InfluxDB。这也是门面模式的精髓。理解这一点,就不会在自定义指标时去手动 new Counter,而是优先从注册表里拿 Meter。
4.2 以 Prometheus 为例配置指标暴露
接 Prometheus 时,除了加 micrometer-registry-prometheus 依赖,还要在最外层配置 management.endpoints.web.exposure.include 时带上 prometheus。然后 Prometheus 的配置里抓取路径写:
这里要注意,/actuator/prometheus 输出的不是 JSON,而是 Prometheus 文本格式,Prometheus 原封不动抓走即可。接着用 PromQL 查 jvm_memory_used_bytes{area="heap"},能看到堆内存趋势。如果希望请求路径包含应用名,可以通过 management.metrics.tags.application 加公共标签:
这样每行指标都会带 application="xxx",多应用接入时好区分。
4.3 对接 Grafana 看板的基本思路
数据进了 Prometheus,后面接 Grafana 就是画图问题。我在实践中通常建三个面板:JVM 总览、GC 面板、线程与类加载。JVM 总览放堆、非堆、Metaspace 用量,计算堆使用率可以用:
GC 面板放 jvm_gc_pause_seconds_sum 的增量,可以用 rate(jvm_gc_pause_seconds_sum[5m]) 看每秒 GC 停顿时间。如果结果有接近 1 秒甚至几秒的尖峰,说明应用出现了长时间停顿,要关注 GC 参数或堆大小。线程面板用 jvm_threads_live_threads 和 jvm_threads_daemon_threads,两者差距过大会有底层线程泄漏的风险。
5. 实战:构建一个带监控的最小 Spring Boot 项目
5.1 项目依赖与配置
我一般用 Spring Initializr 生成项目,只勾选 Spring Web、Actuator、Prometheus 三个依赖。版本用当前稳定版,比如 Spring Boot 3.2.x,Java 17。生成后再手动加一个 Prometheus 依赖的坐标,其实 Initializr 里“Observability”分类下也有 Micrometer Prometheus。配置 application.yml:
启动后先访问 /actuator/health 确认启动正常,再访问 /actuator/prometheus,看到一个长文本输出,name 以 jvm_ 开头,说明 JVM 指标已经出来了。到这步,项目本身的 JVM 指标监控就通了,全程没写一行业务代码。
5.2 自定义业务指标:计数器和响应时间
监控不能只靠 JVM 自带指标,业务指标才是连接“用户体验”和“系统状态”的桥梁。我第一次做自定义指标时写了一个订单服务,统计查询接口调用次数和耗时。代码大致这样:
用 Counter.builder 和 Timer.builder 创建指标,注册进 MeterRegistry,再在请求路径里调用。这里我建议不要在业务代码里每秒都 new 指标,而是在构造器里一次性注册好,避免重复注册导致指标覆盖或报错。
5.3 验证指标端点与 Prometheus 抓取
刷新几次 /order/123 接口后,访问 /actuator/prometheus,搜索 order_query_count,会看到类似:
Prometheus 抓取时自动加了 _total 后缀,查询要用 order_query_count_total。Timer 的耗时指标会输出 order_query_duration_seconds_sum、order_query_duration_seconds_count,还有基于百分位直方图的 bucket。用 Grafana 画响应时间,我习惯直接画:
这个就是近 5 分钟的平均响应时间。注意 rate 一定要加,否则累计值会只涨不降,看不出削峰趋势。
6. 常见问题与排障实录
6.1 actuator 端点不显示 metrics
最常遇到的问题:访问 /actuator/metrics 返回 404。原因几乎都是没配置 exposure.include。Spring Boot 默认只暴露 health,必须显式把 metrics、prometheus 加进去。还有个情况是 Spring Security 拦截了 /actuator/**,此时要么放行,要么给监控端点加权限。我做多项目接入时会把 /actuator/prometheus 放行给内网 Prometheus 抓取,其他端点仍做权限保护,别把 beans、env 这类敏感端点点通到外网。
6.2 指标不生效 / 注解无效
有人喜欢用 @Timed 给接口加监控,省得手写 Timer。这个注解不是自动生效的,需要注册 TimedAspect。方法是在配置类里加一个 Bean:
没有这个 Bean,注解只会静默忽略,接口跑很多次也看不到指标。这个是 Micrometer 使用里最容易踩的坑。我更推荐手写 MeterRegistry 代码,逻辑更直白,也方便加 tag 区分维度。
6.3 指标过多导致存储成本高
指标标签用多了会产生基数爆炸。典型错误是给业务指标加用户 ID、订单 ID 这种高基数的 tag,每次请求都生成一个新序列,Prometheus 存储和查询都会变慢。我一般只加应用名、接口名、状态码、区域这类枚举值可控的标签。如果确实需要按用户维度分析,建议用日志或者单独的表,不要全塞进监控系统。
6.4 时区与命名规范问题
Grafana 面板时间默认用浏览器时区,而 Prometheus 存的是 UTC。查数据时感觉“少了 8 小时”,其实不是数据丢了,是时区显示问题。调整 Grafana 面板的 timezone 即可。命名规范方面,Micrometer 建议用点号分隔,Prometheus 会自动把命名转成下划线,原有点号变成下划线,查找指标时心里要有数。比如 jvm.gc.live.data.size 到了 Prometheus 会变成 jvm_gc_live_data_size_bytes,不带 bytes 不会想到它是字节单位。
6.5 可视化看板指标对不上的排查方法
看板里某个指标查不到,先确认 /actuator/prometheus 里有没有这个指标名,确认后检查 Prometheus target 状态是否 UP。接着看指标里是否带了过滤条件能匹配上的标签。我用 Grafana 变量做下拉筛选时,常因为变量值名称和实际标签值不一致导致图表空白。一个更快的排查技巧:在 Prometheus 的查询框里输入指标名,点“Execute”,然后看返回的标签列表,再回到 Grafana 的查询里对齐标签名,基本能解决九成问题。
最后说点实操体会
这几套组合拳打下来,我最大的感受是:Spring Boot 应用监控的核心不在配置多花哨,而在于把 JVM 状态和业务指标放在同一张时间轴上看。JVM 内存涨得快,就去看 GC 面板和代码分配;GC 停顿多了,再结合线程面板看是否有锁竞争或者频繁创建大对象。Micrometer 的好处是它能把 JVM 指标和业务指标统一成同一套 API,你不需要为了看一个线程数去写 JVM 工具脚本,也不用为了业务指标单独搭一套上传链路。我个人习惯是把 /actuator/prometheus 作为标准出口,先埋好 JVM 指标,再逐步把核心接口的调用量、耗时和错误数补上,等数据攒够了,再回过头优化阈值和告警规则,这时候监控才是真正能落地的东西。