Spring Boot 中基于 Actuator + Micrometer + Prometheus 的应用监控体系实战搭建与告警优化
很多团队做完业务功能后,监控往往是“最后再补”。结果一上线就会发现几个典型问题:
- 服务慢了,但不知道是线程池满了、数据库抖了,还是 GC 频繁;
- 接口 500 飙升了,但日志太多,根本没法第一时间定位;
- Prometheus 虽然接上了,但告警全是噪音,不是“报而不警”,就是“警而无用”。
这篇文章我想带你从 Spring Boot Actuator + Micrometer + Prometheus 的组合出发,搭一套能落地的应用监控体系,并重点讲一下 告警怎么做得更靠谱。
不是只停留在“把 /actuator/prometheus 暴露出来”这一步,而是把它做成你线上真正敢依赖的一套东西。
背景与问题
在 Spring Boot 体系里,监控能力看起来很容易接入:
- Actuator:提供健康检查、指标端点、环境信息等;
- Micrometer:统一指标采集 API,负责把 JVM、HTTP、线程池、自定义业务指标组织起来;
- Prometheus:负责定时拉取指标,存储时序数据,并支持 PromQL 告警查询。
但现实里常见的问题也非常集中:
1. 只有基础指标,没有业务指标
很多项目接入后只能看到:
- JVM 内存
- CPU 使用率
- HTTP 请求数/耗时
- 线程池指标
这些当然有用,但真到线上事故时,你会发现它们不够。
比如“下单成功率下降”,如果没有业务指标,你只能从日志里扒。
2. 指标维度失控
Micrometer 支持标签(tag),这很好用,但也很危险。
我见过有人把 userId、orderId、traceId 这类高基数字段直接塞进 tag,结果 Prometheus 基数爆炸,内存一路飙。
3. 告警过于粗糙
最常见的就是:
up == 0就报警- CPU > 80% 就报警
- 5xx 大于 0 就报警
这种规则最大的问题不是“错”,而是不够贴近真实故障。
CPU 80% 不一定有问题,但错误率 3 分钟内连续升高,大概率就是用户正在受影响。
前置知识与环境准备
本文示例环境:
- JDK 17
- Spring Boot 3.x
- Maven 3.9+
- Prometheus 2.x
- 可选:Grafana(用于看图,不是本文重点)
我们会实现这些目标:
- 暴露 Spring Boot 运行指标;
- 增加自定义业务指标;
- 用 Prometheus 抓取指标;
- 编写更实用的告警规则;
- 总结常见坑和优化建议。
核心原理
先把三者关系捋顺,不然后面很容易“会配不会用”。
组件职责
flowchart LR
A[Spring Boot 应用] --> B[Actuator 端点]
A --> C[Micrometer 指标注册表]
C --> B
D[Prometheus] -->|定时抓取 /actuator/prometheus| B
D --> E[告警规则 Alert Rules]
D --> F[Grafana 可视化]
一句话理解
- Actuator:给应用开“观察窗口”
- Micrometer:在代码里定义和采集指标
- Prometheus:来“抄作业”,周期性拉走这些指标
指标的基本模型
Micrometer 常用指标类型:
Counter:只增不减,适合统计请求次数、失败次数Gauge:瞬时值,适合队列长度、缓存大小Timer:耗时统计,适合接口延迟、方法执行时间DistributionSummary:分布统计,适合请求体大小、消息大小LongTaskTimer:长任务耗时,适合批处理、导出任务
请求链路中的指标流转
sequenceDiagram
participant Client as 客户端
participant App as Spring Boot
participant Meter as Micrometer
participant Act as Actuator
participant Prom as Prometheus
Client->>App: 发起 HTTP 请求
App->>Meter: 记录请求次数/耗时/状态码
Meter->>Act: 暴露为 Prometheus 文本格式
Prom->>Act: 周期抓取 /actuator/prometheus
Act-->>Prom: 返回 metrics
Prom->>Prom: 存储时序数据并评估告警规则
实战代码(可运行)
下面从一个最小可运行例子开始。
1. 创建项目依赖
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>springboot-monitor-demo</artifactId>
<version>1.0.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.2</version>
<relativePath/>
</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>
<!-- Prometheus Registry -->
<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>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2. 配置 Actuator 和 Prometheus 端点
src/main/resources/application.yml:
server:
port: 8080
spring:
application:
name: order-service
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: always
prometheus:
metrics:
export:
enabled: true
metrics:
tags:
application: ${spring.application.name}
distribution:
percentiles-histogram:
http.server.requests: true
slo:
http.server.requests: 100ms,200ms,500ms,1s,2s
这里有两个点值得注意:
prometheus端点必须暴露,否则 Prometheus 拉不到;http.server.requests开启直方图和 SLO bucket,后面我们做延迟告警会用到。
3. 启动类
src/main/java/com/example/demo/MonitorDemoApplication.java:
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MonitorDemoApplication {
public static void main(String[] args) {
SpringApplication.run(MonitorDemoApplication.class, args);
}
}
4. 写一个业务接口
我们模拟一个下单接口,并故意制造一些随机慢请求和失败请求。
src/main/java/com/example/demo/controller/OrderController.java:
package com.example.demo.controller;
import com.example.demo.service.OrderService;
import jakarta.validation.constraints.Min;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Validated
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@GetMapping("/api/orders/create")
public String createOrder(@RequestParam(defaultValue = "1") @Min(1) int amount) {
return orderService.createOrder(amount);
}
}
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.Random;
import java.util.concurrent.TimeUnit;
@Service
public class OrderService {
private final Counter orderSuccessCounter;
private final Counter orderFailCounter;
private final Timer orderCreateTimer;
private final Random random = new Random();
public OrderService(MeterRegistry meterRegistry) {
this.orderSuccessCounter = Counter.builder("biz_order_create_total")
.description("下单请求总数(成功)")
.tag("service", "order")
.tag("result", "success")
.register(meterRegistry);
this.orderFailCounter = Counter.builder("biz_order_create_total")
.description("下单请求总数(失败)")
.tag("service", "order")
.tag("result", "fail")
.register(meterRegistry);
this.orderCreateTimer = Timer.builder("biz_order_create_duration")
.description("下单耗时")
.tag("service", "order")
.publishPercentileHistogram()
.register(meterRegistry);
}
public String createOrder(int amount) {
return orderCreateTimer.record(() -> {
simulateLatency();
if (random.nextInt(10) < 2) {
orderFailCounter.increment();
throw new RuntimeException("模拟下单失败");
}
orderSuccessCounter.increment();
return "order created, amount=" + amount;
});
}
private void simulateLatency() {
int sleepMs = 50 + random.nextInt(1000);
try {
TimeUnit.MILLISECONDS.sleep(sleepMs);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
为什么这里同时用了 Counter 和 Timer?
因为这两个维度经常要分开看:
Counter看成功/失败趋势;Timer看延迟变化。
线上排障时,经常是“错误率没明显升高,但延迟先炸了”。
如果只看一个指标,很容易误判。
5. 增加一个线程池监控示例
很多业务会有异步任务,线程池是事故高发区。
这里我们顺手把线程池指标也挂进去。
src/main/java/com/example/demo/config/ExecutorConfig.java:
package com.example.demo.config;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.binder.jvm.ExecutorServiceMetrics;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@Configuration
public class ExecutorConfig {
@Bean(destroyMethod = "shutdown")
public ExecutorService orderExecutor(MeterRegistry meterRegistry) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
4,
8,
60,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100)
);
ExecutorServiceMetrics.monitor(meterRegistry, executor, "order_executor");
return executor;
}
}
这类指标对于判断“服务慢是 CPU 问题还是线程池排队问题”非常有帮助。
6. 运行并验证指标端点
启动项目后,访问:
curl http://localhost:8080/actuator/prometheus
你应该能看到类似输出:
# HELP http_server_requests_seconds
# TYPE http_server_requests_seconds histogram
http_server_requests_seconds_bucket{application="order-service",error="none",exception="none",method="GET",outcome="SUCCESS",status="200",uri="/api/orders/create",le="0.1",} 3.0
# HELP biz_order_create_total 下单请求总数
# TYPE biz_order_create_total counter
biz_order_create_total{application="order-service",result="success",service="order",} 12.0
biz_order_create_total{application="order-service",result="fail",service="order",} 4.0
然后多请求几次接口:
curl "http://localhost:8080/api/orders/create?amount=10"
如果偶尔返回 500,这是我们故意模拟的,属于正常现象。
7. 配置 Prometheus 抓取
在 Prometheus 的 prometheus.yml 中加入:
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'spring-boot-order-service'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['localhost:8080']
labels:
app: 'order-service'
env: 'dev'
启动 Prometheus:
prometheus --config.file=prometheus.yml
打开 Prometheus UI:
http://localhost:9090
你可以先查询这些表达式:
biz_order_create_total
rate(biz_order_create_total{result="fail"}[1m])
rate(http_server_requests_seconds_count[1m])
histogram_quantile(0.95, sum(rate(http_server_requests_seconds_bucket[5m])) by (le, uri))
8. 配置告警规则
这一部分是文章重点。
我建议告警从“用户影响”角度设计,而不是只盯系统资源。
告警文件 alert_rules.yml
groups:
- name: spring-boot-alerts
rules:
- alert: InstanceDown
expr: up{job="spring-boot-order-service"} == 0
for: 1m
labels:
severity: critical
annotations:
summary: "实例不可用"
description: "{{$labels.instance}} 已经 1 分钟无法抓取"
- alert: HighHttp5xxRate
expr: |
(
sum(rate(http_server_requests_seconds_count{job="spring-boot-order-service",status=~"5.."}[2m]))
/
sum(rate(http_server_requests_seconds_count{job="spring-boot-order-service"}[2m]))
) > 0.1
for: 3m
labels:
severity: critical
annotations:
summary: "HTTP 5xx 错误率过高"
description: "{{$labels.job}} 在最近 3 分钟错误率超过 10%"
- alert: P95LatencyTooHigh
expr: |
histogram_quantile(
0.95,
sum(rate(http_server_requests_seconds_bucket{job="spring-boot-order-service",uri="/api/orders/create"}[5m])) by (le, uri)
) > 0.8
for: 5m
labels:
severity: warning
annotations:
summary: "接口 P95 延迟过高"
description: "/api/orders/create 最近 5 分钟 P95 延迟大于 800ms"
- alert: OrderFailureRateTooHigh
expr: |
(
sum(rate(biz_order_create_total{result="fail"}[2m]))
/
sum(rate(biz_order_create_total[2m]))
) > 0.2
for: 3m
labels:
severity: critical
annotations:
summary: "下单失败率过高"
description: "业务下单失败率连续 3 分钟超过 20%"
- alert: ExecutorQueuePressure
expr: executor_queued_tasks{job="spring-boot-order-service",name="order_executor"} > 80
for: 2m
labels:
severity: warning
annotations:
summary: "线程池队列积压"
description: "order_executor 队列长度超过 80,可能存在消费能力不足"
然后在 prometheus.yml 中加载规则:
rule_files:
- "alert_rules.yml"
告警优化:别只会“阈值报警”
我自己踩过最大的坑,就是把告警规则写得太“理想化”。
看起来严谨,实际上要么不报警,要么一片红。
更实用的思路是这几个:
1. 先看比例,再看绝对值
比如 5xx 数量为 10 次不一定严重,但:
- 总请求只有 20 次,那错误率就是 50%
- 总请求有 10 万次,那几乎可以忽略
所以业务和接口告警,优先看错误率。
2. 一定要加 for
瞬时抖动很常见,尤其是 JVM Full GC、网络抖动、短暂发布切流。
不加 for,很容易一分钟内炸出几十条告警。
3. 延迟告警优先用分位数,不要只看平均值
平均值非常会“骗人”。
99 个请求 10ms,1 个请求 10s,平均值看起来没那么夸张,但用户已经骂起来了。
所以接口延迟更建议看:
- P95
- P99
4. 业务指标比系统指标更值钱
CPU、内存、线程数重要,但最终还是要回答一个问题:
用户有没有受影响?
所以优先级我通常这样排:
- 业务成功率/失败率
- 核心接口延迟
- 实例存活
- 线程池/连接池/GC
- CPU/内存/磁盘
监控体系设计建议
如果你准备在团队里推广这套方案,可以按下面分层建设。
flowchart TD
A[业务层指标] --> A1[下单成功率]
A --> A2[支付回调成功率]
A --> A3[库存扣减耗时]
B[应用层指标] --> B1[HTTP 请求量]
B --> B2[HTTP 错误率]
B --> B3[P95 P99 延迟]
B --> B4[线程池队列]
C[运行时指标] --> C1[JVM 内存]
C --> C2[GC 次数与停顿]
C --> C3[线程状态]
C --> C4[类加载]
D[基础设施层] --> D1[CPU]
D --> D2[磁盘]
D --> D3[网络]
A --> E[告警分级]
B --> E
C --> E
D --> E
这张图表达一个核心观点:
不要把“监控”理解成 JVM 指标采集,而要把它理解成“面向故障定位的观测系统”。
逐步验证清单
如果你想确保整套方案真的可用,建议按下面顺序验证:
第一步:应用自身指标是否正常
检查:
curl http://localhost:8080/actuator/health
curl http://localhost:8080/actuator/prometheus
确认:
- 端点可访问
- 能看到
http_server_requests_seconds - 能看到自定义的
biz_order_create_total
第二步:Prometheus 是否抓到了数据
在 Prometheus UI 中查询:
up{job="spring-boot-order-service"}
结果应为 1。
第三步:业务指标是否随请求变化
连续调用接口:
for i in {1..20}; do curl -s "http://localhost:8080/api/orders/create?amount=10" > /dev/null; done
然后查询:
biz_order_create_total
确认成功和失败计数都有变化。
第四步:延迟直方图是否生效
查询:
http_server_requests_seconds_bucket{uri="/api/orders/create"}
如果没有 bucket 数据,通常是直方图配置没生效。
第五步:告警是否能触发
可以临时把阈值调低,比如把失败率阈值改成 > 0.01,快速制造请求,观察告警状态是否从:
- inactive
- pending
- firing
发生变化。
常见坑与排查
这部分我尽量写得接地气一点,因为这些问题真的非常常见。
1. /actuator/prometheus 返回 404
常见原因
- 没有引入
micrometer-registry-prometheus management.endpoints.web.exposure.include没包含prometheus- Spring Boot 版本和 Micrometer 依赖不匹配
排查建议
先看依赖,再看配置,再访问:
curl -I http://localhost:8080/actuator/prometheus
如果是 404,基本就是端点没暴露出来。
2. Prometheus 中 up=0
常见原因
- 应用地址写错
- 容器网络不通
metrics_path配错- 应用开启了鉴权,但 Prometheus 没带认证
排查建议
从 Prometheus 所在机器直接 curl:
curl http://目标IP:8080/actuator/prometheus
这个动作很朴素,但特别有效。
很多“Prometheus 抓不到”的问题,本质就是网络不通。
3. 自定义指标没有数据
常见原因
- 指标代码根本没执行到
- Counter 注册了,但没
increment() - Timer 只注册没
record() - 指标名写错,PromQL 查询的不是实际名字
排查建议
可以先在 /actuator/prometheus 文本里 grep:
curl -s http://localhost:8080/actuator/prometheus | grep biz_order_create_total
如果这里都没有,就别急着查 Prometheus,先查应用代码。
4. 指标数量爆炸,Prometheus 内存飙升
高危原因
在 tag 里放了高基数字段:
- userId
- orderId
- phone
- traceId
- requestId
- URL 全路径且带动态参数
错误示例
Counter.builder("biz_order_total")
.tag("userId", userId)
.register(meterRegistry)
.increment();
这会非常危险。
Prometheus 擅长处理的是有限维度,不是用户明细表。
正确思路
tag 只保留低基数维度,比如:
- service
- endpoint
- result
- region
- tenant(前提是租户数量可控)
5. 延迟指标有数据,但 histogram_quantile 算不出来
常见原因
- 没开 histogram
- 查询时聚合维度不对
- 使用了 summary 却按 histogram 的写法查
排查建议
确认有这类 bucket 指标:
http_server_requests_seconds_bucket
如果没有,检查:
management:
metrics:
distribution:
percentiles-histogram:
http.server.requests: true
6. 告警反复抖动
常见原因
- 阈值设得过于敏感
for时间太短- 窗口太小,比如只看
[30s] - 低流量服务用错误率告警,分母太小导致大起大落
优化建议
对于低流量服务,告警规则最好加一个最小请求量门槛,比如:
(
sum(rate(http_server_requests_seconds_count{status=~"5.."}[5m]))
/
sum(rate(http_server_requests_seconds_count[5m]))
) > 0.1
and
sum(rate(http_server_requests_seconds_count[5m])) > 1
这样能避免“总共 2 个请求,错了 1 个,错误率 50%”就报警。
安全/性能最佳实践
监控系统不是“开了就行”,它本身也会带来安全和性能成本。
1. 不要把所有 Actuator 端点都暴露到公网
推荐最小暴露原则,只开放必要端点:
healthinfoprometheus
像 env、beans、configprops 这类端点,生产环境要谨慎,很多信息是敏感的。
更稳妥的做法
- Actuator 只监听内网网卡或管理端口
- 通过网关/ServiceMonitor/内网策略限制访问
- 对敏感端点加认证
例如:
management:
server:
port: 9091
endpoints:
web:
exposure:
include: health,info,prometheus
这样可以把业务端口和管理端口隔离开。
2. 控制标签基数
这是性能优化里最重要的一条。
一句话原则:
标签是为了聚合,不是为了记录明细。
如果你不确定某个 tag 会不会爆炸,先问自己:
- 它的取值范围是否有限?
- 未来半年会不会线性增长?
- 我真的需要按它聚合分析吗?
3. 自定义指标宁少勿滥
不是所有业务动作都要打点。
更推荐抓住“关键路径”:
- 用户登录
- 下单
- 支付
- 库存扣减
- 消息消费
- 外部依赖调用
如果每个方法都打点,除了埋点维护成本高,还会让监控面板信息过载。
4. 为核心接口开启直方图,非核心接口按需开启
直方图很有价值,但也会带来更多时间序列。
生产环境里我建议:
- 核心接口:开启 histogram,做分位数分析;
- 非核心接口:保留基础 count/sum 即可;
- 超高吞吐服务:先评估存储压力。
5. 告警要分级,不要全部 critical
一个比较实用的分级方式:
critical:用户明显受影响,需要立即处理
例:实例全挂、核心链路失败率高warning:性能变差或风险上升,需要关注
例:P95 延迟升高、线程池积压info:观察类事件
例:发布后错误率轻微波动
这样值班同学不会一看到告警就麻木。
6. 先做“少而准”的告警集
我建议第一版生产告警先只上这几类:
- 实例不可用
- 核心接口 5xx 错误率
- 核心接口 P95 延迟
- 核心业务失败率
- 线程池/连接池积压
先把这 5 类做准,比一口气上 30 条规则更有效。
一个更贴近生产的落地思路
如果你要把它推广到实际项目,我建议按这条路径推进:
stateDiagram-v2
[*] --> 接入基础监控
接入基础监控 --> 增加业务指标
增加业务指标 --> 建立核心大盘
建立核心大盘 --> 定义告警阈值
定义告警阈值 --> 观察一周
观察一周 --> 调整噪音告警
调整噪音告警 --> 固化标准模板
固化标准模板 --> [*]
这里最容易被忽略的是“观察一周”。
很多团队一上来就把规则写死,结果不是太松,就是太紧。
更稳妥的方式是先观察真实业务波动,再定最终阈值。
总结
回到开头那个问题:
为什么很多团队明明接了监控,线上还是“不知道出了什么事”?
因为他们只完成了“采集指标”,却没有完成“构建监控体系”。
这套方案的核心可以浓缩成几句话:
- Actuator 负责暴露观测入口;
- Micrometer 负责统一采集 JVM、HTTP 和业务指标;
- Prometheus 负责拉取、存储、查询和告警;
- 真正有价值的,不只是基础资源指标,而是业务成功率 + 核心接口延迟 + 关键资源压力的组合视角。
如果你现在就要落地,我建议按这个最小闭环开始:
- 接入
spring-boot-starter-actuator和micrometer-registry-prometheus - 暴露
/actuator/prometheus - 为核心业务加 2 个自定义指标:成功/失败次数、耗时
- Prometheus 抓取并先做 3 条告警:
- 实例不可用
- 错误率过高
- P95 延迟过高
- 观察一周后再调阈值和分级
最后给一个边界条件:
如果你的服务是低流量、离线型、任务型系统,不要生搬硬套高频 HTTP 服务那套告警策略。
这类场景更适合关注:
- 任务成功率
- 任务执行时长
- 堆积量
- 最近一次成功时间
监控没有万能模板,只有是否贴合你的系统行为。
如果你把这篇文章里的实战部分先跑起来,再把业务指标补上,基本就已经迈过“有监控”和“能用的监控”之间那道最关键的坎了。