跳转到内容
123xiao | 无名键客

《Spring Boot 中基于 Actuator + Micrometer + Prometheus 的应用监控实战与性能告警落地》

字数: 0 阅读时长: 1 分钟

Spring Boot 中基于 Actuator + Micrometer + Prometheus 的应用监控实战与性能告警落地

很多团队的应用“能跑”不难,难的是“跑得稳、出问题时能快速定位”。
我见过不少 Spring Boot 项目,上线前接口压测做了、日志也打了,但一到线上,CPU 飙高、接口变慢、线程池堆积,大家第一反应还是登录机器 topjstack、翻日志。这种方式不是不能用,而是太慢,且往往等你开始查时,现场已经变了。

这篇文章我想带你完整走一遍:如何在 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 个角色:

  1. Actuator:暴露应用管理端点,比如健康检查、指标端点。
  2. Micrometer:统一指标采集门面,负责计数器、计时器、仪表盘指标抽象。
  3. Prometheus Registry:Micrometer 的一个指标导出实现,把指标转成 Prometheus 能识别的格式。
  4. 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:统计耗时和次数,适合接口 RT
  • Gauge:瞬时值,适合队列长度、缓存大小、线程池活跃线程数
  • 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
  • 给所有指标统一加上 application tag,后续查询和聚合很方便
  • 生产环境里 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/POST
  • status=200/500
  • bizType=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 深挖原因

一套比较实用的落地建议

如果你准备在团队里正式推广这套方案,我建议按这个顺序推进:

第一阶段:先让应用“可见”

至少接入:

  • health
  • prometheus
  • JVM 指标
  • HTTP 指标

第二阶段:补业务关键指标

针对核心场景补:

  • 成功次数
  • 失败次数
  • 处理耗时
  • 队列积压
  • 下游依赖调用情况

第三阶段:落告警

从少而精开始,先做:

  • 实例存活
  • 错误率
  • 延迟
  • 堆内存
  • 线程池堆积

第四阶段:持续调阈值

上线后一周到两周,根据真实流量修正:

  • 告警阈值
  • 统计窗口
  • 标签维度
  • 是否需要按应用、机房、实例分层告警

总结

在 Spring Boot 里做应用监控,Actuator + Micrometer + Prometheus 是一条非常成熟且工程化的方案:

  • Actuator 负责暴露管理与监控端点
  • Micrometer 负责统一采集和组织指标
  • Prometheus 负责拉取、存储、查询与告警

真正能产生价值的关键,不只是“接了监控”,而是:

  1. 默认系统指标要有
  2. 关键业务指标要补
  3. 告警要围绕可执行动作设计
  4. 控制高基数 tag,避免监控系统反噬业务系统

如果你现在的 Spring Boot 应用还没有完整监控,我建议先把本文的最小示例跑起来,再逐步补业务指标和告警。别一上来就想做“大而全”的监控平台,先把可见、可查、可告警三件事做好,收益通常就已经很明显了。

如果只给一个最落地的建议,那就是:
先盯住错误率、延迟、堆内存、线程池队列这四类指标,它们最容易帮助你在问题扩大前发现异常。


分享到:

上一篇
《从浏览器指纹到请求签名:一次 Web 逆向中级实战拆解与自动化复现》
下一篇
《Java 中基于 CompletableFuture 的异步编排实战:并行调用、超时控制与异常兜底-363》