Spring Boot 中基于 Actuator + Micrometer + Prometheus 的应用监控实战与性能告警落地
很多团队的应用“能跑”不难,难的是“跑得稳、出问题时能快速定位”。
我见过不少 Spring Boot 项目,上线前接口压测做了、日志也打了,但一到线上,CPU 飙高、接口变慢、线程池堆积,大家第一反应还是登录机器 top、jstack、翻日志。这种方式不是不能用,而是太慢,且往往等你开始查时,现场已经变了。
这篇文章我想带你完整走一遍:如何在 Spring Boot 中,用 Actuator + Micrometer + Prometheus 搭建一套可运行、可观测、能告警的监控方案。重点不只是“把指标暴露出来”,还包括:
- 指标链路怎么串起来
- 代码里怎么补充业务指标
- Prometheus 怎么抓
- 告警怎么写才不容易误报
- 常见坑怎么排查
如果你已经在用 Spring Boot,这套方案基本可以低成本接入。
背景与问题
先说几个很典型的线上问题:
- 接口 RT 突然从 50ms 涨到 800ms
- 某个实例 Full GC 频繁,但日志没有明显异常
- 线程池队列堆积,应用还没挂,但已经“半死不活”
- 数据库连接池被打满,接口开始超时
- 某个业务接口错误率飙升,但业务日志不集中,很难第一时间发现
这些问题有个共同点:如果缺少结构化指标,定位会非常被动。
日志适合看“具体发生了什么”,监控指标更适合回答:
- 系统是否健康?
- 趋势是否异常?
- 哪些资源在逼近瓶颈?
- 问题是单点故障,还是整体退化?
- 需要告警的阈值在哪里?
在 Spring Boot 生态里,Actuator、Micrometer、Prometheus 这三者正好把这件事闭环了。
前置知识与环境准备
本文示例环境:
- JDK 17
- Spring Boot 3.x
- Maven 3.8+
- Prometheus 2.x
- 可选:Grafana 用于看图(本文重点放在采集与告警)
你至少需要知道:
- Spring Boot 基础使用
- 基本 Maven 依赖管理
- HTTP 接口与 YAML 配置
- Prometheus 的“拉取式采集”概念
核心原理
这套监控链路可以理解为 4 个角色:
- Actuator:暴露应用管理端点,比如健康检查、指标端点。
- Micrometer:统一指标采集门面,负责计数器、计时器、仪表盘指标抽象。
- Prometheus Registry:Micrometer 的一个指标导出实现,把指标转成 Prometheus 能识别的格式。
- Prometheus Server:定时拉取
/actuator/prometheus,存储时序数据,并执行告警规则。
一张图先看全链路
flowchart LR
A[Spring Boot 应用] --> B[Actuator]
B --> C[Micrometer]
C --> D[/actuator/prometheus]
E[Prometheus Server] -->|scrape pull| D
E --> F[Alert Rules]
F --> G[Alertmanager/通知系统]
三者分工怎么理解
很多人第一次接触时会把这三个组件混在一起,我建议这样记:
- Actuator 是门
- Micrometer 是秤
- Prometheus 是抄表员
Actuator 提供访问入口;Micrometer 定义和收集指标;Prometheus 周期性过来拉数据。
指标类型怎么选
Micrometer 常见指标类型有:
Counter:只增不减,适合请求次数、异常次数Timer:统计耗时和次数,适合接口 RTGauge:瞬时值,适合队列长度、缓存大小、线程池活跃线程数DistributionSummary:统计分布值,适合 payload 大小、批量条数等
如果你在做性能告警,最常用的是:
- JVM 内存、GC、线程
- HTTP 请求耗时与状态码
- 数据源连接池
- 自定义业务成功率、失败率、处理量
- 线程池队列积压
监控数据流时序
理解请求进来后指标如何产生,对排查很有帮助。
sequenceDiagram
participant U as 用户请求
participant S as Spring Boot
participant M as Micrometer
participant A as Actuator Endpoint
participant P as Prometheus
U->>S: 调用 /api/orders/create
S->>M: 记录 http.server.requests Timer
S->>M: 记录业务 Counter/Gauge
P->>A: GET /actuator/prometheus
A->>M: 汇总当前指标
M-->>A: Prometheus 格式文本
A-->>P: 返回指标数据
P->>P: 存储时序数据并计算告警
实战代码(可运行)
下面我们从零搭一个最小可运行示例。
第一步:创建项目并添加依赖
pom.xml 示例:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>boot-monitor-demo</artifactId>
<version>1.0.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
</parent>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<!-- Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Micrometer Prometheus 导出 -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<!-- 可选:校验 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
第二步:配置 Actuator 与指标暴露
src/main/resources/application.yml
server:
port: 8080
spring:
application:
name: boot-monitor-demo
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: always
metrics:
tags:
application: ${spring.application.name}
这里有几个关键点:
include必须包含prometheus- 给所有指标统一加上
applicationtag,后续查询和聚合很方便 - 生产环境里
health是否show-details: always要慎重,后面会讲安全问题
第三步:启动类
src/main/java/com/example/demo/BootMonitorDemoApplication.java
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class BootMonitorDemoApplication {
public static void main(String[] args) {
SpringApplication.run(BootMonitorDemoApplication.class, args);
}
}
第四步:写一个业务接口,并上报自定义指标
这一步最关键。很多人只开了默认 JVM、HTTP 指标,但业务侧发生异常时,默认指标不一定够用。
我一般都会补一层“业务指标”,比如订单创建成功次数、失败次数、处理耗时。
4.1 业务服务
src/main/java/com/example/demo/service/OrderService.java
package com.example.demo.service;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.springframework.stereotype.Service;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
@Service
public class OrderService {
private final Counter orderSuccessCounter;
private final Counter orderFailCounter;
private final Timer orderCreateTimer;
public OrderService(MeterRegistry meterRegistry) {
this.orderSuccessCounter = Counter.builder("biz_order_create_success_total")
.description("订单创建成功总数")
.register(meterRegistry);
this.orderFailCounter = Counter.builder("biz_order_create_fail_total")
.description("订单创建失败总数")
.register(meterRegistry);
this.orderCreateTimer = Timer.builder("biz_order_create_duration")
.description("订单创建耗时")
.publishPercentileHistogram()
.register(meterRegistry);
}
public String createOrder() {
return orderCreateTimer.record(() -> {
mockBusinessCost();
boolean success = ThreadLocalRandom.current().nextInt(10) < 8;
if (success) {
orderSuccessCounter.increment();
return "order created";
} else {
orderFailCounter.increment();
throw new RuntimeException("mock create order failed");
}
});
}
private void mockBusinessCost() {
try {
TimeUnit.MILLISECONDS.sleep(ThreadLocalRandom.current().nextInt(50, 300));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
4.2 Controller 暴露接口
src/main/java/com/example/demo/controller/OrderController.java
package com.example.demo.controller;
import com.example.demo.service.OrderService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@GetMapping("/api/orders/create")
public String createOrder() {
return orderService.createOrder();
}
@GetMapping("/api/ping")
public String ping() {
return "pong";
}
}
第五步:增加线程池监控
真实项目中,线程池是很容易出问题但又常被忽略的点。
比如异步任务、消息消费、批处理、报表生成,线程池积压时,接口还未必立刻报错。
5.1 线程池配置与指标绑定
src/main/java/com/example/demo/config/ExecutorConfig.java
package com.example.demo.config;
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import jakarta.annotation.PostConstruct;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@Configuration
public class ExecutorConfig {
private final MeterRegistry meterRegistry;
private ThreadPoolExecutor executor;
public ExecutorConfig(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
@PostConstruct
public void init() {
this.executor = new ThreadPoolExecutor(
4,
8,
60,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100)
);
Gauge.builder("biz_async_queue_size", executor.getQueue(), LinkedBlockingQueue::size)
.description("异步线程池队列长度")
.register(meterRegistry);
Gauge.builder("biz_async_active_threads", executor, ThreadPoolExecutor::getActiveCount)
.description("异步线程池活跃线程数")
.register(meterRegistry);
Gauge.builder("biz_async_pool_size", executor, ThreadPoolExecutor::getPoolSize)
.description("异步线程池线程数")
.register(meterRegistry);
}
public ThreadPoolExecutor getExecutor() {
return executor;
}
}
这类 Gauge 指标特别适合做容量预警。
第六步:启动应用并验证指标
启动项目后,先访问业务接口几次:
curl http://localhost:8080/api/ping
curl http://localhost:8080/api/orders/create
curl http://localhost:8080/api/orders/create
curl http://localhost:8080/api/orders/create
然后打开指标端点:
curl http://localhost:8080/actuator/prometheus
你会看到类似内容:
# HELP http_server_requests_seconds
# TYPE http_server_requests_seconds summary
http_server_requests_seconds_count{application="boot-monitor-demo",error="none",exception="none",method="GET",outcome="SUCCESS",status="200",uri="/api/ping",} 1.0
# HELP biz_order_create_success_total 订单创建成功总数
# TYPE biz_order_create_success_total counter
biz_order_create_success_total{application="boot-monitor-demo",} 2.0
# HELP biz_order_create_fail_total 订单创建失败总数
# TYPE biz_order_create_fail_total counter
biz_order_create_fail_total{application="boot-monitor-demo",} 1.0
到这里,Spring Boot 端已经完成了。
第七步:配置 Prometheus 抓取
新建 prometheus.yml:
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'spring-boot-app'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['localhost:8080']
如果你本机装了 Prometheus,直接启动:
prometheus --config.file=prometheus.yml
然后打开:
http://localhost:9090
你可以查询这些指标:
http_server_requests_seconds_count
biz_order_create_success_total
biz_order_create_fail_total
biz_async_queue_size
第八步:写性能告警规则
只有采集,没有告警,监控价值会打折。
但告警不能乱写,不然你会很快把团队“告警免疫”掉。
告警设计思路
我通常会优先落这几类:
- 应用实例不可用
- 接口错误率升高
- 接口 P95 / P99 延迟异常
- JVM 堆内存使用率过高
- GC 频繁
- 线程池队列积压
- 数据源连接池接近打满
示例告警规则
alert_rules.yml
groups:
- name: spring-boot-alerts
rules:
- alert: SpringBootInstanceDown
expr: up{job="spring-boot-app"} == 0
for: 1m
labels:
severity: critical
annotations:
summary: "Spring Boot 实例不可用"
description: "实例 {{ $labels.instance }} 已无法抓取超过 1 分钟"
- alert: HighHttpErrorRate
expr: |
sum(rate(http_server_requests_seconds_count{job="spring-boot-app",status=~"5.."}[5m]))
/
sum(rate(http_server_requests_seconds_count{job="spring-boot-app"}[5m]))
> 0.05
for: 2m
labels:
severity: warning
annotations:
summary: "接口 5xx 错误率过高"
description: "5 分钟内 5xx 错误率超过 5%"
- alert: HighHttpP95Latency
expr: |
histogram_quantile(0.95,
sum by (le) (
rate(http_server_requests_seconds_bucket{job="spring-boot-app"}[5m])
)
) > 0.5
for: 3m
labels:
severity: warning
annotations:
summary: "接口 P95 延迟过高"
description: "5 分钟窗口内 P95 超过 500ms"
- alert: HighJvmHeapUsage
expr: |
(
jvm_memory_used_bytes{area="heap"}
/
jvm_memory_max_bytes{area="heap"}
) > 0.85
for: 5m
labels:
severity: warning
annotations:
summary: "JVM 堆内存使用率过高"
description: "实例 {{ $labels.instance }} 堆内存连续 5 分钟超过 85%"
- alert: ThreadPoolQueueBacklog
expr: biz_async_queue_size > 80
for: 2m
labels:
severity: warning
annotations:
summary: "异步线程池队列堆积"
description: "线程池队列长度持续超过 80"
然后在 prometheus.yml 中引入:
global:
scrape_interval: 15s
evaluation_interval: 15s
rule_files:
- "alert_rules.yml"
scrape_configs:
- job_name: 'spring-boot-app'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['localhost:8080']
告警从“有”到“好用”的关键
这是我自己踩坑后很想强调的一点:阈值不是照抄模板就行。
比如:
- P95 500ms 是否合理,要看业务 SLA
- 错误率 5% 是不是过高,要看流量规模
- 线程池队列 80 是不是危险,要看队列上限和处理速度
- 堆内存 85% 是否异常,要结合 GC 行为看
如果你应用平时 RT 就在 300ms 左右,那 500ms 告警可能没意义。
如果你是低频核心交易接口,1% 错误率都可能必须报警。
指标关系图:从系统指标到业务指标
classDiagram
class Actuator {
+health
+metrics
+prometheus
}
class Micrometer {
+Counter
+Timer
+Gauge
+DistributionSummary
}
class DefaultMetrics {
+JVM
+HTTP
+System
+Datasource
}
class CustomMetrics {
+订单成功数
+订单失败数
+业务耗时
+线程池队列长度
}
Actuator --> Micrometer
Micrometer --> DefaultMetrics
Micrometer --> CustomMetrics
逐步验证清单
如果你想确保整条链路没问题,可以按这个顺序核对:
应用侧
- 应用已引入
spring-boot-starter-actuator - 已引入
micrometer-registry-prometheus -
/actuator/prometheus能访问 - 业务接口访问后,自定义指标有变化
- 指标 tag 数量合理,没有明显失控
Prometheus 侧
-
targets页面状态为UP - 能查到
up{job="spring-boot-app"} - 能查到
http_server_requests_seconds_count - 能查到自定义业务指标
- 告警规则已加载成功
告警侧
- 阈值不是拍脑袋设的
- 告警有
for窗口,避免瞬时抖动 - 告警文案能让值班同学看懂
- 告警项能映射到可执行动作
常见坑与排查
这部分很实用,我尽量讲“怎么查”,不是只讲“是什么”。
1. /actuator/prometheus 返回 404
常见原因
- 没引入
micrometer-registry-prometheus - 没在
management.endpoints.web.exposure.include里开放prometheus - Spring Boot 版本与依赖不匹配
排查步骤
先看依赖是否存在:
mvn dependency:tree | grep micrometer
再确认配置:
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
最后直接访问:
curl -i http://localhost:8080/actuator/prometheus
2. Prometheus 抓不到指标,Target 显示 DOWN
常见原因
- 地址写错了
- 容器网络不通
metrics_path配错- 应用有鉴权,Prometheus 没权限
- 抓取超时
排查思路
先从 Prometheus 所在机器上手工请求:
curl http://localhost:8080/actuator/prometheus
如果是 Docker / Kubernetes 环境,要特别确认:
- 容器端口是否暴露
- Service 是否配置正确
- Prometheus 是否能访问该网段
- 是否被网关/Ingress 拦截
3. 自定义指标有了,但 PromQL 查不到
常见原因
- 指标名称写错
- 指标还没被触发
- 查询范围太短
- tag 条件过滤错了
排查方法
先模糊搜:
{__name__=~"biz_.*"}
如果能搜到,再精确查:
biz_order_create_success_total
注意 Counter 更适合配合 rate() 或 increase():
increase(biz_order_create_success_total[5m])
4. P95 查询不出值
典型原因
没有开启直方图桶。
这也是很多人第一次做延迟告警时容易卡住的地方。
像 histogram_quantile() 依赖的是 _bucket 指标,如果你的 Timer 没发布 histogram,Prometheus 端就没有桶数据。
例如:
Timer.builder("biz_order_create_duration")
.publishPercentileHistogram()
.register(meterRegistry);
另外,默认 HTTP 指标是否具备 histogram,也要结合 Spring Boot / Micrometer 配置确认。
5. 指标量暴涨,Prometheus 压力变大
这个坑我真的见过不少次,根因通常是 高基数 tag。
错误示例
Counter.builder("api_request_total")
.tag("userId", userId)
.register(meterRegistry)
.increment();
如果 userId 每次都不同,时序数量会爆炸。
正确思路
tag 应该选有限集合,例如:
method=GET/POSTstatus=200/500bizType=pay/refund/query
避免这些做 tag:
- 用户 ID
- 订单号
- 请求参数
- 完整 URL 动态路径
- traceId
6. 指标有了,但告警误报频繁
常见原因
- 没有设置
for - 阈值太敏感
- 窗口太短
- 用瞬时值替代趋势值
经验建议
不要这样:
http_server_requests_seconds_count{status=~"5.."} > 0
更推荐看一段时间内的比例:
sum(rate(http_server_requests_seconds_count{status=~"5.."}[5m]))
/
sum(rate(http_server_requests_seconds_count[5m]))
安全/性能最佳实践
监控是基础设施的一部分,既要“看得见”,也要“暴露得刚刚好”。
安全最佳实践
1. 不要把所有 Actuator 端点都暴露到公网
很多项目为了省事直接:
management:
endpoints:
web:
exposure:
include: "*"
这在生产环境非常危险。
建议只开放必要端点,例如:
management:
endpoints:
web:
exposure:
include: health,prometheus
2. 将管理端口与业务端口分离
可以单独配置管理端口:
management:
server:
port: 9091
这样做有几个好处:
- 业务流量和管理流量分离
- 更容易加防火墙或安全策略
- Prometheus 抓取路径更清晰
3. 对 Actuator 端点加鉴权或网络隔离
如果是内网环境,可以优先做网络层限制。
如果必须跨网段访问,可以配合 Spring Security 对端点加鉴权。
性能最佳实践
1. 控制 tag 基数
这是最重要的一条。
高基数会直接导致:
- 应用内存压力变大
- Prometheus 存储膨胀
- 查询速度变慢
- 告警评估成本变高
2. 优先采集“决策有用”的指标
不要为了“全面”而采一堆没人看的指标。
我建议先围绕这三层:
- 资源层:CPU、内存、GC、线程
- 服务层:QPS、RT、错误率
- 业务层:成功率、失败率、队列积压、下游依赖调用耗时
3. 告警规则用趋势,不用尖刺
例如接口 RT,尽量观察:
- 5 分钟 P95
- 10 分钟错误率
- 持续 3 分钟以上的资源过高
不要因为单次抖动就报警。
4. 对核心路径加业务指标,对边缘路径适度取舍
比如支付、下单、结算这些核心链路,一定要单独埋指标。
但如果是低频后台管理接口,未必需要每个都单独埋点。
5. 监控不是日志替代品
监控告诉你“哪里不对劲”,日志帮助你看“具体出了什么错”。
最有效的方式是:
- 监控先发现异常
- 再结合 trace / log 深挖原因
一套比较实用的落地建议
如果你准备在团队里正式推广这套方案,我建议按这个顺序推进:
第一阶段:先让应用“可见”
至少接入:
healthprometheus- JVM 指标
- HTTP 指标
第二阶段:补业务关键指标
针对核心场景补:
- 成功次数
- 失败次数
- 处理耗时
- 队列积压
- 下游依赖调用情况
第三阶段:落告警
从少而精开始,先做:
- 实例存活
- 错误率
- 延迟
- 堆内存
- 线程池堆积
第四阶段:持续调阈值
上线后一周到两周,根据真实流量修正:
- 告警阈值
- 统计窗口
- 标签维度
- 是否需要按应用、机房、实例分层告警
总结
在 Spring Boot 里做应用监控,Actuator + Micrometer + Prometheus 是一条非常成熟且工程化的方案:
- Actuator 负责暴露管理与监控端点
- Micrometer 负责统一采集和组织指标
- Prometheus 负责拉取、存储、查询与告警
真正能产生价值的关键,不只是“接了监控”,而是:
- 默认系统指标要有
- 关键业务指标要补
- 告警要围绕可执行动作设计
- 控制高基数 tag,避免监控系统反噬业务系统
如果你现在的 Spring Boot 应用还没有完整监控,我建议先把本文的最小示例跑起来,再逐步补业务指标和告警。别一上来就想做“大而全”的监控平台,先把可见、可查、可告警三件事做好,收益通常就已经很明显了。
如果只给一个最落地的建议,那就是:
先盯住错误率、延迟、堆内存、线程池队列这四类指标,它们最容易帮助你在问题扩大前发现异常。