Spring Boot 中基于 Actuator 与 Micrometer 的应用监控实战:从指标采集到告警落地
很多团队一开始做监控,都是“先把 /actuator/prometheus 暴露出来再说”。结果过几周就会发现几个现实问题:
- 指标很多,但不知道哪些值得看
- 业务接口明明变慢了,Grafana 面板却没法直接定位
- 告警规则写了一堆,不是误报就是漏报
- 线上出了问题,才发现健康检查、线程池、JVM、数据库连接池数据根本没接全
这篇文章我想换一个更“落地”的视角来讲:不是只教你把指标采出来,而是从 Spring Boot + Actuator + Micrometer 出发,串起“采集、暴露、展示、告警”这一整条链路。你可以把它当成一篇可直接照着做的教程。
背景与问题
在 Spring Boot 应用里,监控常见痛点主要有三类:
- 系统层问题看不到
- CPU 飙升、内存抖动、GC 频繁、线程池堆积
- 应用层问题看不清
- 某个接口慢、某类异常暴涨、某个下游依赖超时
- 告警层问题不可靠
- 只看“服务是否存活”远远不够
- 没有基于趋势和阈值的组合告警
Actuator 解决的是“暴露运行信息”,Micrometer 解决的是“统一指标采集与输出模型”。两者配合后,Spring Boot 应用就能比较顺畅地接入 Prometheus,再由 Grafana 展示、Alertmanager 告警。
一句话概括:
Actuator 负责把门打开,Micrometer 负责把数据组织好。
前置知识与环境准备
本文默认你已经具备这些基础:
- 会写基本的 Spring Boot Web 应用
- 知道 Maven/Gradle 的依赖管理
- 对 Prometheus、Grafana 有基础认知
- 使用 JDK 17+、Spring Boot 3.x
本文示例环境:
- Spring Boot 3.3.x
- Micrometer + Prometheus Registry
- Prometheus
- Grafana
核心原理
先别急着上代码,我们先把链路理顺。实际运行时,大致是这样:
flowchart LR
A[Spring Boot App] --> B[Actuator Endpoints]
A --> C[Micrometer MeterRegistry]
C --> B
B --> D[/actuator/prometheus]
D --> E[Prometheus 拉取]
E --> F[Grafana 展示]
E --> G[Alertmanager 告警]
这条链路里有几个关键角色:
1. Actuator:暴露运行时能力
Actuator 提供很多端点,比如:
/actuator/health/actuator/info/actuator/metrics/actuator/prometheus
其中:
/actuator/metrics更适合人肉查看某个指标/actuator/prometheus更适合 Prometheus 定时抓取
2. Micrometer:统一指标模型
Micrometer 是 Spring Boot 监控体系里的核心抽象,它定义了常见指标类型:
Counter:只增不减,适合请求次数、异常次数Gauge:瞬时值,适合队列长度、缓存大小Timer:耗时与调用次数,适合接口 RTDistributionSummary:分布统计,适合请求大小、订单金额这类数值
它还有一个很重要的概念:tag(标签)。
例如一个 HTTP 请求指标可能长这样:
- 指标名:
http_server_requests_seconds_count - 标签:
method=GETuri=/api/orders/{id}status=200outcome=SUCCESS
标签能让你按维度分析,但也很容易失控。后面讲“常见坑”时我会重点说这个问题。
3. Prometheus:拉模型采集
Prometheus 默认是pull 模式,它会定时去拉你的 /actuator/prometheus。
这意味着:
- 应用自身不用主动推送
- 每个实例都要能被 Prometheus 访问到
- 指标端点不能随便暴露给公网
4. 告警不是“有指标就行”,而是“指标要可解释”
监控成熟度提升的关键,不在于你采了多少指标,而在于:
- 指标是否能和业务动作对应
- 告警是否能指导排查
- 阈值是否符合系统真实负载特征
监控链路分层设计
我比较推荐把应用监控分成三层看:
flowchart TD
A[基础设施层] --> A1[CPU/内存/磁盘/网络]
B[运行时层] --> B1[JVM GC 线程 类加载]
C[应用层] --> C1[HTTP接口 数据库 线程池 缓存 业务指标]
D[告警层] --> D1[可用性 延迟 错误率 饱和度]
对应到 Spring Boot 项目里,通常至少要覆盖:
- JVM 指标
- HTTP 请求指标
- 数据库连接池指标
- 线程池指标
- 关键业务指标
实战代码(可运行)
下面我们从零开始搭一个可运行示例。
第一步:创建项目并添加依赖
Maven 依赖
<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 http://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.3.2</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>
<!-- 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>
</project>
第二步:开启 Actuator 与 Prometheus 端点
application.yml
server:
port: 8080
spring:
application:
name: boot-monitor-demo
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: when_authorized
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
这里有两个特别关键的配置:
percentiles-histogram.http.server.requests=true- 让 HTTP 请求指标生成 Prometheus 可聚合直方图
slo.http.server.requests=...- 设定分桶边界,后续算 P95/P99、慢请求占比会更靠谱
如果你少了这部分,后面做延迟告警会很难受。我早期就踩过这个坑:图能画出来,但算分位数时精度和可解释性都很一般。
第三步:启动类
package com.example.monitor;
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);
}
}
第四步:写一个业务接口,并模拟慢请求与异常
package com.example.monitor.web;
import jakarta.validation.constraints.Min;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;
@RestController
@RequestMapping("/api/orders")
@Validated
public class OrderController {
@GetMapping("/{id}")
public Map<String, Object> getOrder(@PathVariable @Min(1) Long id,
@RequestParam(defaultValue = "false") boolean slow,
@RequestParam(defaultValue = "false") boolean fail) throws InterruptedException {
if (slow) {
Thread.sleep(ThreadLocalRandom.current().nextLong(300, 1200));
}
if (fail) {
throw new IllegalStateException("simulate order query failure");
}
return Map.of(
"id", id,
"status", "PAID",
"amount", 199.00
);
}
}
这个接口的意义很简单:
slow=true:制造慢请求fail=true:制造错误请求
后面我们就靠它验证监控面板和告警规则。
第五步:自定义业务指标
系统指标只能告诉你“程序不太对劲”,但真正帮你判断业务状态的,通常还是业务指标。
例如我们统计订单创建次数和金额分布。
package com.example.monitor.service;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.DistributionSummary;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.stereotype.Service;
@Service
public class OrderMetricsService {
private final Counter orderCreatedCounter;
private final DistributionSummary orderAmountSummary;
public OrderMetricsService(MeterRegistry meterRegistry) {
this.orderCreatedCounter = Counter.builder("biz_order_created_total")
.description("Total created orders")
.register(meterRegistry);
this.orderAmountSummary = DistributionSummary.builder("biz_order_amount")
.description("Order amount distribution")
.baseUnit("yuan")
.register(meterRegistry);
}
public void recordCreatedOrder(double amount, String channel) {
orderCreatedCounter.increment();
orderAmountSummary.record(amount);
}
}
这里我先保留了 channel 参数,但没有直接打成 tag,不是漏写,而是故意这样设计。原因是:
- 如果
channel值域稳定,比如app/web/mini-program,可以作为 tag - 如果值域不稳定,贸然加 tag 很容易造成指标基数爆炸
这一点后面会展开。
补一个创建订单接口
package com.example.monitor.web;
import com.example.monitor.service.OrderMetricsService;
import jakarta.validation.constraints.DecimalMin;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/api/order-create")
@Validated
public class OrderCreateController {
private final OrderMetricsService orderMetricsService;
public OrderCreateController(OrderMetricsService orderMetricsService) {
this.orderMetricsService = orderMetricsService;
}
@PostMapping
public Map<String, Object> create(@RequestParam @DecimalMin("0.01") double amount,
@RequestParam(defaultValue = "web") String channel) {
orderMetricsService.recordCreatedOrder(amount, channel);
return Map.of(
"success", true,
"amount", amount,
"channel", channel
);
}
}
第六步:给线程池加监控
线上问题里,线程池堆积是特别常见的一类。只盯 HTTP RT,很容易漏掉根因。
package com.example.monitor.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.*;
@Configuration
public class ExecutorConfig {
@Bean(destroyMethod = "shutdown")
public ExecutorService businessExecutor(MeterRegistry meterRegistry) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
4,
8,
60,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100),
new ThreadPoolExecutor.CallerRunsPolicy()
);
return ExecutorServiceMetrics.monitor(
meterRegistry,
executor,
"biz_executor",
"pool", "order"
);
}
}
有了这个之后,你就能在 Prometheus 中看到类似指标:
executor_active_threadsexecutor_pool_size_threadsexecutor_queued_tasks
这比“感觉线程池可能有问题”靠谱得多。
第七步:查看指标是否真的出来了
启动应用后,先手工验证。
查看健康检查
curl http://localhost:8080/actuator/health
查看指标列表
curl http://localhost:8080/actuator/metrics
查看 Prometheus 格式指标
curl http://localhost:8080/actuator/prometheus
压测制造一些数据
正常请求:
curl "http://localhost:8080/api/orders/1"
慢请求:
curl "http://localhost:8080/api/orders/1?slow=true"
失败请求:
curl "http://localhost:8080/api/orders/1?fail=true"
创建订单:
curl -X POST "http://localhost:8080/api/order-create?amount=299.9&channel=web"
如果一切正常,你应该能在 /actuator/prometheus 里看到这些指标中的一部分:
http_server_requests_seconds_count
http_server_requests_seconds_bucket
http_server_requests_seconds_sum
jvm_memory_used_bytes
jvm_gc_pause_seconds_count
system_cpu_usage
process_uptime_seconds
biz_order_created_total
biz_order_amount_count
biz_order_amount_sum
executor_active_threads
第八步:接入 Prometheus
prometheus.yml
global:
scrape_interval: 15s
scrape_configs:
- job_name: 'boot-monitor-demo'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['host.docker.internal:8080']
如果你不是 Docker 环境,直接写宿主机 IP 或域名即可。
启动 Prometheus 后,打开:
http://localhost:9090
试几个查询:
http_server_requests_seconds_count
rate(http_server_requests_seconds_count[1m])
sum(rate(http_server_requests_seconds_count{status=~"5.."}[5m]))
histogram_quantile(0.95, sum(rate(http_server_requests_seconds_bucket[5m])) by (le, uri, method))
最后这个 P95 查询是最常用的一个。
第九步:Grafana 看板最小可用方案
如果你不想一上来就做特别复杂的大盘,我建议先把这几类图补齐:
- 应用可用性
- 每分钟请求量
- 5xx 错误率
- 应用延迟
- P50/P95/P99
- 慢请求占比
- JVM
- 堆内存使用
- GC 次数与暂停时间
- 线程数
- 线程池
- active
- queue size
- 业务指标
- 订单创建次数
- 订单金额总和或均值
下面是一个典型的指标流转时序:
sequenceDiagram
participant U as User
participant A as Spring Boot App
participant M as Micrometer
participant P as Prometheus
participant G as Grafana
participant Al as Alertmanager
U->>A: 发起HTTP请求
A->>M: 记录Timer/Counter/Gauge
P->>A: 定时拉取 /actuator/prometheus
A-->>P: 返回指标文本
G->>P: 查询 PromQL
P-->>G: 返回时序数据
P->>Al: 触发告警规则
Al-->>U: 发送告警通知
第十步:从指标到告警落地
监控最终要落到告警,否则只是“会看图”。
下面给一组比较实用的 Prometheus 告警规则。
alert-rules.yml
groups:
- name: spring-boot-demo-alerts
rules:
- alert: ApplicationDown
expr: up{job="boot-monitor-demo"} == 0
for: 1m
labels:
severity: critical
annotations:
summary: "应用实例不可达"
description: "Prometheus 连续 1 分钟无法抓取应用指标"
- alert: HighHttp5xxRate
expr: |
sum(rate(http_server_requests_seconds_count{job="boot-monitor-demo",status=~"5.."}[5m]))
/
sum(rate(http_server_requests_seconds_count{job="boot-monitor-demo"}[5m]))
> 0.05
for: 5m
labels:
severity: warning
annotations:
summary: "HTTP 5xx 错误率过高"
description: "过去 5 分钟 5xx 占比超过 5%"
- alert: HighP95Latency
expr: |
histogram_quantile(
0.95,
sum(rate(http_server_requests_seconds_bucket{job="boot-monitor-demo"}[5m])) by (le)
) > 0.8
for: 5m
labels:
severity: warning
annotations:
summary: "应用 P95 延迟过高"
description: "过去 5 分钟整体 P95 超过 800ms"
- alert: HighJvmHeapUsage
expr: |
(
sum(jvm_memory_used_bytes{job="boot-monitor-demo",area="heap"})
/
sum(jvm_memory_max_bytes{job="boot-monitor-demo",area="heap"})
) > 0.85
for: 10m
labels:
severity: warning
annotations:
summary: "JVM 堆使用率过高"
description: "堆内存持续 10 分钟超过 85%"
- alert: ThreadPoolQueueBacklog
expr: executor_queued_tasks{job="boot-monitor-demo"} > 50
for: 3m
labels:
severity: warning
annotations:
summary: "业务线程池队列积压"
description: "线程池排队任务数超过 50,持续 3 分钟"
为什么这些规则比较实用?
因为它们覆盖了“四个最小生存指标”:
- 可用性:应用是否还活着
- 错误率:请求是否失败变多
- 延迟:请求是否明显变慢
- 饱和度:资源是否快耗尽
这其实就是业界常说的几个核心观测维度,只不过我们在 Spring Boot 落地时,把它们映射成了具体 PromQL。
逐步验证清单
建议你按下面顺序验证,不要一口气全上。
验证 1:应用端点是否可访问
curl http://localhost:8080/actuator/prometheus
如果这个都不通,后面就不用看了。
验证 2:Prometheus 是否抓取成功
在 Prometheus 页面检查:
up{job="boot-monitor-demo"}
结果应该是 1。
验证 3:HTTP 指标是否增长
连续访问几次接口后检查:
sum(rate(http_server_requests_seconds_count[1m]))
验证 4:错误率告警是否可触发
多次访问失败接口:
for i in {1..30}; do curl "http://localhost:8080/api/orders/1?fail=true"; done
然后看:
sum(rate(http_server_requests_seconds_count{status=~"5.."}[5m]))
验证 5:延迟告警是否可触发
多次访问慢接口:
for i in {1..50}; do curl "http://localhost:8080/api/orders/1?slow=true"; done
检查:
histogram_quantile(0.95, sum(rate(http_server_requests_seconds_bucket[5m])) by (le))
验证 6:业务指标是否正常记录
for i in {1..10}; do curl -X POST "http://localhost:8080/api/order-create?amount=88.8&channel=web"; done
查询:
biz_order_created_total
常见坑与排查
这一节非常重要。很多“监控没效果”并不是技术不会,而是踩了几个典型坑。
1. /actuator/prometheus 没暴露出来
现象
- Prometheus 抓取失败
- 浏览器访问
/actuator/prometheus返回 404
排查
确认配置中是否包含:
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
如果你只配了 health,Prometheus 当然拉不到。
2. 指标有了,但 PromQL 查询不到想要的结果
常见原因
- 时间窗口太短
- 没有先制造流量
- 指标名记错
- Spring Boot / Micrometer 版本不同导致指标标签略有差异
排查建议
先从最原始查询开始:
http_server_requests_seconds_count
再逐步加条件,而不是一上来就写一个很复杂的 histogram_quantile + sum by + rate。
3. 自定义 tag 导致指标基数爆炸
这个坑我真的见过太多次了。
错误示例
Counter.builder("biz_order_created_total")
.tag("userId", userId.toString())
.register(meterRegistry);
如果 userId 是高基数数据,每个用户都会生成新的时序。用户一多,Prometheus 内存会迅速上涨。
正确思路
tag 只用于低基数、稳定枚举值,例如:
channel=web/appregion=cn-east/cn-northresult=success/fail
不要把下面这些字段直接打成 tag:
- 用户 ID
- 订单号
- 请求参数原值
- URL 中动态 ID
- 异常 message 原文
4. P95 看起来不准
原因
- 没开 histogram
- 没配置合理分桶
- 请求量太小,分位数不稳定
建议
management:
metrics:
distribution:
percentiles-histogram:
http.server.requests: true
slo:
http.server.requests: 100ms,200ms,500ms,1s,2s
另外,低流量服务不要过度解读 P99。样本太少时,P99 很容易“看起来很吓人,但没有统计意义”。
5. 健康检查通过,但服务其实已经不可用
原因
默认 health 只代表部分组件状态,不等于你的业务功能完整可用。
建议
把健康检查分层:
- liveness:进程是否存活
- readiness:是否可对外接流量
- 业务健康检查:关键依赖是否正常
不要把所有下游依赖都塞进一个重型健康检查里,否则健康检查本身也会拖垮系统。
6. 告警太多,最后没人看
典型原因
- 阈值拍脑袋
- 没有
for持续时间 - 把单次抖动当故障
- 告警没有分级
我的建议
先从少而稳开始:
critical:实例不可达、错误率暴涨warning:延迟高、堆使用率高、队列堆积
先保证告警可信,再逐步扩展覆盖面。
安全/性能最佳实践
监控是基础设施,但它本身也会带来安全和性能成本。
1. 不要把 Actuator 全量暴露到公网
建议只暴露必要端点:
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
不要图省事直接:
include: "*"
这在测试环境可能无所谓,在线上风险很大。
2. 为 Actuator 端点加访问控制
如果项目接了 Spring Security,建议对 Actuator 端点做单独鉴权,或者仅允许 Prometheus 所在网段访问。
最简单的思路:
- 内网暴露
- 网关层限制
- mTLS 或基础认证
- Kubernetes 中结合 NetworkPolicy
3. 控制标签数量与指标数量
监控不是越多越好。每新增一个高频指标、每多一个标签维度,都会增加:
- 应用内存开销
/actuator/prometheus输出体积- Prometheus 抓取与存储压力
经验建议:
- 先做“少量高价值指标”
- tag 控制在业务可解释范围
- 避免高基数维度
4. 对热点接口开启直方图,对全部接口谨慎评估
直方图很有用,但会增加时序数量。
如果你的服务接口很多、实例很多,可以优先:
- 对关键接口保留细粒度分桶
- 对普通接口降低标签维度
- 通过公共
uri模板归类,而不是原始 URL
5. 告警要和排障动作绑定
一个好的告警,至少应该回答三个问题:
- 哪个服务出问题了?
- 出的是哪类问题?
- 值班同学第一步该查什么?
比如:
- 高延迟告警 → 先看线程池、GC、数据库连接池
- 高错误率告警 → 先看异常日志、下游依赖状态
- 堆内存高告警 → 先看对象增长、GC、缓存是否失控
如果告警只写“系统异常,请及时处理”,那和没写差不多。
6. 给业务指标定义边界与口径
例如“订单创建量”要说清楚:
- 是下单请求到达数?
- 还是创建成功数?
- 是否包含重试?
- 是否按最终一致结果计数?
如果口径不清,监控面板再漂亮也会误导决策。
一个更实用的监控落地思路
如果你现在就要在项目里落地,不妨按这个顺序推进:
flowchart TD
A[先接 Actuator + Prometheus] --> B[确认 JVM/HTTP 基础指标]
B --> C[补线程池/数据库连接池指标]
C --> D[增加 2~3 个关键业务指标]
D --> E[建立最小告警集]
E --> F[按告警噪音持续调优]
这条路线的好处是:
- 起步快
- 反馈快
- 不容易一上来就过度设计
总结
Spring Boot 里的监控落地,真正有价值的不是“把 Actuator 开起来”,而是把下面这套能力打通:
- 用 Actuator 暴露运行信息
- 用 Micrometer 统一采集系统、应用、业务指标
- 用 Prometheus 抓取与存储时序数据
- 用 Grafana 可视化趋势
- 用 Alertmanager 把关键异常及时通知出去
如果你让我给一个中级开发者最实用的建议,我会说三点:
- 先抓核心指标,不要贪多
- 可用性、错误率、延迟、饱和度,先覆盖这四类
- 业务指标必须补上
- 只看 JVM 和 HTTP,很多业务故障是看不出来的
- 控制标签基数
- 这是 Micrometer/Prometheus 体系里最容易被忽略、但代价最大的坑
最后给一个可执行的落地边界:
- 如果你的应用还是单体、流量不高:先把基础指标 + 关键告警做稳
- 如果你的应用已经多实例、微服务化:尽快统一指标规范、标签规范和告警分级
- 如果你已经有 tracing/logging:监控指标最好和日志、链路追踪互相跳转,排障效率会提升一个量级
监控从来不是装个依赖就结束,它更像是给系统建立“体检、诊断和报警”的能力。Actuator 和 Micrometer 只是起点,但这个起点非常值得认真搭好。