Spring Boot 实战:基于 Actuator、Micrometer 与 Prometheus 搭建应用监控告警体系
做 Spring Boot 项目时,很多团队一开始只关心“服务能跑起来”,等线上真的出问题了,才发现自己几乎是“盲飞”:
- CPU 飙高了不知道是哪个实例
- 接口变慢了,但没有可观测数据支撑
- 错误率上升时,只能靠用户反馈
- JVM 堆内存快满了,却没人提前知道
这篇文章我想换一个更偏“落地”的角度,不是单独介绍某个组件,而是直接带你搭一条完整链路:
Spring Boot 应用 → Actuator 暴露信息 → Micrometer 统一指标采集 → Prometheus 抓取并告警
如果你已经写过 Spring Boot 服务,但对监控体系还是“知道名词,不太敢上手”,这篇会比较适合你。
背景与问题
在没有监控体系之前,很多问题其实都属于“事后分析”:
- 服务挂了才知道
- 接口慢了说不清慢在哪
- 数据库连接池耗尽没有预警
- 垃圾回收频繁但没人关注
- 应用是健康还是亚健康,无法量化
而一个靠谱的监控体系,至少应该回答这些问题:
- 应用活着吗?
- JVM 健康吗?
- HTTP 请求吞吐、耗时、错误率怎么样?
- 线程池、连接池是否接近瓶颈?
- 关键业务指标有没有异常波动?
- 出现异常时能不能自动告警?
Spring Boot 生态里,这套事情其实已经有很成熟的组合:
- Actuator:负责暴露健康检查和运行信息
- Micrometer:负责统一指标模型与埋点
- Prometheus:负责拉取指标、存储时序数据、执行告警规则
前置知识与环境准备
本文示例环境:
- JDK 17
- Spring Boot 3.x
- Maven 3.8+
- Prometheus 2.x
你至少需要知道这些基本概念:
/actuator/health是健康检查端点/actuator/prometheus是 Prometheus 抓取指标的入口- 指标通常是
name + labels + value + timestamp - Prometheus 是拉模型,不是应用主动推送
核心原理
先别急着写代码,我们先把链路脑子里过一遍。
1. 三个组件分别干什么
- Actuator:把应用内部状态暴露成标准端点
- Micrometer:把 JVM、HTTP、线程池、数据库连接池等指标收集并格式化
- Prometheus:定时访问
/actuator/prometheus,把文本指标抓回去存起来
2. 监控链路图
flowchart LR
A[Spring Boot 应用] --> B[Actuator Endpoints]
B --> C[Micrometer 指标注册表]
C --> D[/actuator/prometheus]
E[Prometheus Server] -->|定时抓取| D
E --> F[告警规则 Alerting Rules]
F --> G[Alertmanager/通知渠道]
3. 一次请求的指标是怎么产生的
当一个 HTTP 请求进入 Spring Boot:
- Web 层接收到请求
- Micrometer 自动记录请求次数、耗时、状态码
- 指标被注册到 MeterRegistry
- Prometheus 下次抓取时拿到这些指标
- 规则引擎判断是否需要告警
sequenceDiagram
participant U as 用户请求
participant S as Spring Boot
participant M as Micrometer
participant P as Prometheus
U->>S: GET /api/orders
S->>M: 记录请求耗时/状态码/计数
S-->>U: 200 OK
P->>S: GET /actuator/prometheus
S-->>P: 暴露 metrics 文本
P->>P: 存储时序数据并评估告警规则
4. 常见指标分类
从实践上看,监控可以分成四类:
- 基础可用性指标:健康状态、实例存活
- 资源指标:CPU、内存、GC、线程
- 流量指标:QPS、请求耗时、错误率
- 业务指标:下单数、支付成功率、任务积压量
其中前 3 类,Micrometer 基本都能帮你打好地基;第 4 类需要你自己补业务埋点。
实战代码(可运行)
下面我们从零开始,做一个可运行的监控样例。
第一步:创建 Spring Boot 项目并添加依赖
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
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>spring-monitor-demo</artifactId>
<version>1.0.0</version>
<name>spring-monitor-demo</name>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</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 导出 -->
<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-aop</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
第二步:配置 Actuator 与 Prometheus 端点
src/main/resources/application.yml:
server:
port: 8080
spring:
application:
name: spring-monitor-demo
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
sla:
http.server.requests: 100ms,300ms,500ms,1s,3s
info:
app:
name: spring-monitor-demo
version: 1.0.0
这里有几个配置很关键:
exposure.include:暴露哪些端点prometheus.metrics.export.enabled:启用 Prometheus 导出metrics.tags.application:给所有指标统一加应用标签,后面查询非常方便percentiles-histogram:开启 HTTP 请求耗时直方图,便于做 P95、P99 分析
第三步:编写启动类
src/main/java/com/example/demo/DemoApplication.java:
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
第四步:写一个模拟业务接口
src/main/java/com/example/demo/controller/OrderController.java:
package com.example.demo.controller;
import io.micrometer.core.annotation.Timed;
import jakarta.validation.constraints.Min;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;
@RestController
@RequestMapping("/api/orders")
@Validated
public class OrderController {
@GetMapping("/{id}")
@Timed(value = "biz_order_query", description = "订单查询耗时")
public Map<String, Object> getOrder(@PathVariable @Min(1) Long id) throws InterruptedException {
int sleep = ThreadLocalRandom.current().nextInt(50, 400);
Thread.sleep(sleep);
Map<String, Object> result = new HashMap<>();
result.put("id", id);
result.put("status", "PAID");
result.put("costMs", sleep);
return result;
}
@GetMapping("/error")
public String mockError() {
throw new IllegalStateException("模拟业务异常");
}
}
这里我用了 @Timed,它会为这个方法生成一个自定义耗时指标,适合关键业务接口做精细监控。
第五步:增加自定义业务指标
光有 JVM 和 HTTP 指标还不够,真正有价值的是业务指标。
比如我们统计:
- 创建订单总次数
- 支付成功次数
- 当前待处理订单数量
src/main/java/com/example/demo/service/OrderMetricsService.java:
package com.example.demo.service;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import jakarta.annotation.PostConstruct;
import org.springframework.stereotype.Service;
import java.util.concurrent.atomic.AtomicInteger;
@Service
public class OrderMetricsService {
private final MeterRegistry meterRegistry;
private Counter createdCounter;
private Counter paidCounter;
private final AtomicInteger pendingOrders = new AtomicInteger(0);
public OrderMetricsService(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
@PostConstruct
public void init() {
this.createdCounter = Counter.builder("biz_order_created_total")
.description("订单创建总数")
.tag("module", "order")
.register(meterRegistry);
this.paidCounter = Counter.builder("biz_order_paid_total")
.description("订单支付成功总数")
.tag("module", "order")
.register(meterRegistry);
Gauge.builder("biz_order_pending_count", pendingOrders, AtomicInteger::get)
.description("待处理订单数")
.tag("module", "order")
.register(meterRegistry);
}
public void createOrder() {
createdCounter.increment();
pendingOrders.incrementAndGet();
}
public void payOrder() {
paidCounter.increment();
pendingOrders.updateAndGet(v -> Math.max(0, v - 1));
}
}
再补一个接口方便触发这些指标:
src/main/java/com/example/demo/controller/OrderMetricsController.java:
package com.example.demo.controller;
import com.example.demo.service.OrderMetricsService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
public class OrderMetricsController {
private final OrderMetricsService orderMetricsService;
public OrderMetricsController(OrderMetricsService orderMetricsService) {
this.orderMetricsService = orderMetricsService;
}
@PostMapping("/api/mock/create")
public Map<String, Object> create() {
orderMetricsService.createOrder();
return Map.of("message", "create ok");
}
@PostMapping("/api/mock/pay")
public Map<String, Object> pay() {
orderMetricsService.payOrder();
return Map.of("message", "pay ok");
}
}
第六步:启动应用并验证 Actuator 端点
启动项目:
mvn spring-boot:run
验证健康检查:
curl http://localhost:8080/actuator/health
预期输出类似:
{"status":"UP"}
查看 Prometheus 格式指标:
curl http://localhost:8080/actuator/prometheus
你会看到大量指标,比如:
# HELP jvm_memory_used_bytes The amount of used memory
# TYPE jvm_memory_used_bytes gauge
jvm_memory_used_bytes{area="heap",id="G1 Eden Space",application="spring-monitor-demo",} 1.23456E7
# HELP http_server_requests_seconds
# TYPE http_server_requests_seconds histogram
http_server_requests_seconds_count{application="spring-monitor-demo",exception="none",method="GET",outcome="SUCCESS",status="200",uri="/api/orders/{id}",} 3.0
第七步:安装并配置 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
你可以先在应用侧多打几次请求:
curl http://localhost:8080/api/orders/1
curl http://localhost:8080/api/orders/2
curl -X POST http://localhost:8080/api/mock/create
curl -X POST http://localhost:8080/api/mock/pay
然后到 Prometheus 页面里试几个查询:
up
jvm_memory_used_bytes{application="spring-monitor-demo"}
http_server_requests_seconds_count{application="spring-monitor-demo"}
biz_order_created_total
biz_order_pending_count
第八步:配置简单告警规则
监控体系如果只看图,不做告警,价值会打折扣。至少把几个关键规则配起来。
新建 alert-rules.yml:
groups:
- name: spring-boot-alerts
rules:
- alert: SpringBootAppDown
expr: up{job="spring-boot-app"} == 0
for: 1m
labels:
severity: critical
annotations:
summary: "Spring Boot 应用不可用"
description: "实例 {{ $labels.instance }} 已无法抓取超过 1 分钟"
- alert: HighHttp5xxRate
expr: sum(rate(http_server_requests_seconds_count{job="spring-boot-app",status=~"5.."}[1m]))
/
sum(rate(http_server_requests_seconds_count{job="spring-boot-app"}[1m])) > 0.2
for: 2m
labels:
severity: warning
annotations:
summary: "5xx 错误率过高"
description: "最近 2 分钟 5xx 错误率超过 20%"
- alert: HighJvmHeapUsage
expr: sum(jvm_memory_used_bytes{area="heap",job="spring-boot-app"})
/
sum(jvm_memory_max_bytes{area="heap",job="spring-boot-app"}) > 0.8
for: 5m
labels:
severity: warning
annotations:
summary: "JVM 堆内存使用率过高"
description: "最近 5 分钟堆内存使用率持续超过 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']
如果你后续接 Alertmanager,就可以发到企业微信、钉钉、邮件或 Slack。本文先聚焦采集与规则本身。
逐步验证清单
这是我自己做监控接入时常用的一套检查顺序,能少走很多弯路。
1. 应用侧检查
-
/actuator/health可访问 -
/actuator/prometheus可访问 - 指标里存在
application标签 - 访问业务接口后,
http_server_requests_seconds_count有增长 - 自定义指标能查到,如
biz_order_created_total
2. Prometheus 侧检查
-
Targets页面状态是UP -
up{job="spring-boot-app"}返回 1 - 指标查询结果不为空
- 报警规则状态正常加载
- 告警表达式手工验证能触发
常见坑与排查
这部分我建议你认真看,实际项目里很多时间都花在这。
1. /actuator/prometheus 返回 404
常见原因
- 没引入
micrometer-registry-prometheus - 没有暴露
prometheus端点 - Spring Boot 配置写错层级
检查方式
先看依赖是否存在,再看配置:
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
如果还是不行,直接访问:
curl http://localhost:8080/actuator
看端点列表里有没有 prometheus。
2. Prometheus 抓取失败,Target 是 DOWN
常见原因
- 端口写错
- 容器环境里用了
localhost,但 Prometheus 和应用不在同一个容器 - 应用有鉴权或网络策略拦截
排查思路
如果在 Docker 里,localhost 很容易踩坑。Prometheus 容器里的 localhost 指的是它自己,不是你的应用。
正确思路通常是:
- 使用容器服务名
- 或者使用宿主机 IP
- 或者放进同一个 Docker Network
3. 指标有了,但没有 HTTP 耗时分位数
原因
你只采集到了基础 count/sum/max,没有开启 histogram。
解决
management:
metrics:
distribution:
percentiles-histogram:
http.server.requests: true
之后你才能在 Prometheus 里做类似查询:
histogram_quantile(0.95, sum(rate(http_server_requests_seconds_bucket[5m])) by (le))
4. 自定义指标暴涨,Prometheus 内存越来越大
根因
标签维度爆炸。这真的是监控里最常见、也最危险的问题之一。
比如你这样打标签:
- userId
- orderId
- requestId
- 手机号
- 动态 URL
这些都会让时序数量疯狂增长。
错误示例
Counter.builder("biz_order_total")
.tag("userId", userId.toString())
.register(meterRegistry)
.increment();
正确原则
标签必须是低基数的,比如:
- region=cn-hz
- method=GET
- status=success
- module=order
不要把每个请求唯一值放进 tag。
5. 指标名查不到,或者和预期不一致
Micrometer 会对指标名做一定规范化处理,不同导出器也可能有表现差异。
比如你在代码里定义:
Counter.builder("biz.order.created")
导出到 Prometheus 后,常常会变成下划线风格,并且计数器可能带 _total 后缀。
所以建议:
- 统一用清晰、稳定、下划线风格命名
- 在
/actuator/prometheus实际确认最终名字
6. 错误率查询结果怪怪的
这个坑我以前也踩过:总请求量很低时,错误率会被瞬时波动放大。
比如一分钟只有 2 个请求,其中 1 个失败,你看到的错误率就是 50%,这不一定代表真的事故。
建议
- 对低流量服务,适当拉长窗口,比如
[5m] - 或者加最小流量门槛
- 告警里同时看错误率和请求量
示例:
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
安全/性能最佳实践
监控系统很有用,但也不能“为了观测把系统搞慢”。
1. 不要无脑暴露所有 Actuator 端点
线上环境尽量只开放必要端点:
healthinfoprometheus
有些端点信息很多,暴露过宽可能带来安全风险。
推荐配置:
management:
endpoints:
web:
exposure:
include: health,info,prometheus
2. 将管理端口与业务端口分离
如果对安全更敏感,可以单独开放管理端口:
server:
port: 8080
management:
server:
port: 8081
这样 Prometheus 只抓 8081,业务流量走 8080,隔离更清晰。
3. 为 Actuator 端点加访问控制
如果是公网或复杂网络环境,不建议裸奔。至少做到:
- 内网访问
- 网关白名单
- 基于 Spring Security 的鉴权
- 通过 ServiceMesh / Ingress 限制来源
4. 控制指标标签数量
再强调一遍:低基数标签是生命线。
经验建议:
- 标签值尽量枚举化
- 一条指标的标签不宜过多
- 不在标签中放唯一 ID
- 业务指标先少量试点,再逐步扩展
5. 重点监控“会影响用户体验”的指标
不是指标越多越好。中级阶段最值得优先落地的是:
- 实例存活
- HTTP 请求量、错误率、P95/P99
- JVM 堆内存、GC 停顿
- 线程池活跃度
- 数据库连接池使用率
- 核心业务成功率
先抓住这些主干,再考虑更细的业务洞察。
6. 采集频率别一味调太高
scrape_interval 不是越小越好。抓得太频繁:
- 增加 Prometheus 压力
- 增加应用端暴露开销
- 时序数据量变大
一般来说:
- 常规业务:
15s或30s - 高频告警场景:
5s需谨慎评估 - 低优先级指标:甚至可以更长
一个推荐的落地结构
如果你准备在团队里推广,我建议按这个层次推进:
flowchart TD
A[第一阶段: 接入基础监控] --> A1[health/prometheus]
A --> A2[JVM/HTTP/线程池]
B[第二阶段: 接入业务指标] --> B1[下单量]
B --> B2[成功率]
B --> B3[任务积压]
C[第三阶段: 配置告警] --> C1[可用性]
C --> C2[性能]
C --> C3[错误率]
D[第四阶段: 监控治理] --> D1[标签收敛]
D --> D2[阈值调优]
D --> D3[误报收敛]
A --> B --> C --> D
这个顺序的好处是:先有可见性,再谈精细化。不要一上来就设计几十条告警,最后大家被告警轰炸到麻木。
进阶建议:哪些指标值得你自定义
Micrometer 自动给的指标已经很多了,但真正能体现业务价值的,还得你自己补。
我通常会优先选这几类:
-
吞吐类
- 订单创建数
- 支付成功数
- 消息消费数
-
积压类
- 待处理任务数
- 队列堆积量
- 重试中的任务数
-
质量类
- 支付失败数
- 风控拦截率
- 第三方调用失败率
-
耗时类
- 核心业务流程耗时
- 外部接口调用耗时
- 异步任务执行耗时
建议遵守一个简单规则:
如果某个指标异常,会直接影响用户体验、收入、稳定性,那它就值得被监控。
总结
这套方案的核心价值,不是“把指标采出来”,而是让你对应用状态有持续、量化、可告警的认知。
回顾一下本文的主线:
- 用 Actuator 暴露应用运行端点
- 用 Micrometer 统一收集 JVM、HTTP 和自定义业务指标
- 用 Prometheus 定时抓取指标并执行告警规则
- 通过合理的标签、查询和阈值设计,把监控真正用起来
如果你准备在真实项目里落地,我建议按下面这个最小闭环开始:
- 先接入
health和prometheus - 确认 Prometheus 能正常抓取
- 观察 JVM、HTTP 请求、错误率、耗时
- 为 1~2 个关键业务流程补自定义指标
- 先上线 3 条核心告警:
- 实例不可用
- 5xx 错误率过高
- JVM 堆内存持续过高
最后给一个边界条件判断:
- 如果你现在还是单体应用、小流量、单机部署,这套方案已经足够实用
- 如果你进入多集群、多租户、高基数业务场景,就要继续考虑 Grafana 展示、Alertmanager 路由、指标治理和采样策略
监控体系不是一口气建成的,但第一步一定值得尽早做。因为很多线上事故,真的不是不能解决,而是你根本没看见它正在发生。