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

《Spring Boot 中级实战:基于 Actuator、Micrometer 与 Prometheus 搭建应用监控与告警体系》

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

Spring Boot 中级实战:基于 Actuator、Micrometer 与 Prometheus 搭建应用监控与告警体系

很多团队的服务上线后,日志打得不少,但真正出问题时,大家第一反应还是“先上机器看看”。这说明一件事:日志能解释发生了什么,监控才能告诉我们系统正在变坏

这篇文章我会带你从一个 Spring Boot 应用出发,串起 Actuator、Micrometer、Prometheus 这三件套,搭建一个能落地的监控基础方案。目标不是讲概念大全,而是做出一个中级开发者在项目里能直接复用的最小闭环:

  • 应用暴露健康检查和指标
  • 自定义业务指标
  • Prometheus 抓取指标
  • 基于规则做告警
  • 避开常见坑,控制安全与性能风险

如果你已经会写 Spring Boot 接口,但对监控体系还停留在“开个 /actuator 看看”的阶段,这篇会比较合适。


一、背景与问题

在没有监控体系时,Spring Boot 服务通常有这些典型痛点:

  1. 接口变慢了,但没人第一时间知道
  2. JVM 内存抖动、线程池打满,只能靠报警电话后排查
  3. 数据库连接池耗尽时,日志一堆,但没有趋势图
  4. 线上故障发生后,很难回答“从什么时候开始恶化”
  5. 业务指标缺失,比如下单成功率、消息消费堆积量,没有统一观测入口

很多人会把健康检查、日志、监控混在一起。实际上它们解决的问题不同:

  • Actuator:给你暴露应用管理端点,比如健康状态、环境信息、指标端点
  • Micrometer:给你统一的指标埋点 API,相当于 Spring 生态里的“度量门面”
  • Prometheus:负责定时抓取指标、存储时序数据、执行告警规则

可以把它们理解成这样:

  • Spring Boot 应用负责“产出指标”
  • Prometheus 负责“采集和判断异常”
  • 告警系统负责“通知人”

二、前置知识与环境准备

1. 适合的技术版本

本文示例基于以下组合,实践中比较常见:

  • JDK 8+
  • Spring Boot 2.x
  • Maven 3.6+
  • Prometheus 2.x

如果你使用的是 Spring Boot 3.x,整体思路不变,但个别配置项命名和依赖版本需要注意兼容性。

2. 需要的依赖

最小依赖就三类:

  • spring-boot-starter-web
  • spring-boot-starter-actuator
  • micrometer-registry-prometheus

三、核心原理

先别急着写代码,先把链路想清楚。很多配置问题,本质上是因为没理解数据怎么流动。

1. 整体架构

flowchart LR
    A[Spring Boot 应用] --> B[Actuator 端点]
    A --> C[Micrometer 指标注册]
    C --> D[/actuator/prometheus]
    E[Prometheus] -->|定时抓取| D
    E --> F[告警规则 Alert Rules]
    F --> G[Alertmanager/通知系统]

这张图里最关键的一点是:

Prometheus 不是被应用“推送”的,而是主动“拉取”的。

所以应用要做的是:

  1. 通过 Micrometer 生成指标
  2. 通过 Actuator 暴露 Prometheus 可识别的文本格式指标

2. 指标从哪里来

在 Spring Boot 中,指标通常有三类来源:

(1)框架自动提供的系统指标

例如:

  • JVM 内存、GC、线程
  • CPU 使用
  • HTTP 请求次数、耗时
  • 数据源连接池
  • Tomcat/Undertow 等容器指标

(2)组件集成后的指标

比如你接了:

  • Redis
  • Kafka
  • 连接池 HikariCP
  • Executor 线程池

很多组件已经和 Micrometer 打通,开箱即用。

(3)业务自定义指标

这是最有价值、也最容易被忽略的一类,比如:

  • 订单创建成功次数
  • 支付失败次数
  • 用户注册耗时
  • 消息消费积压量
  • 第三方接口调用失败率

3. Micrometer 的常见指标类型

Micrometer 中最常见的几种指标要分清:

  • Counter:只增不减,适合统计总次数
  • Gauge:某一时刻的瞬时值,适合库存、队列长度、线程数
  • Timer:统计耗时和调用次数
  • DistributionSummary:统计数值分布,如消息大小、订单金额

我自己在项目里最常用的是:Counter + Timer + Gauge。这三种足够覆盖大部分业务监控。

4. 一次请求的指标流转过程

sequenceDiagram
    participant U as 用户请求
    participant S as Spring Boot
    participant M as Micrometer
    participant A as Actuator
    participant P as Prometheus

    U->>S: 调用 /orders/create
    S->>M: 记录请求耗时、成功/失败计数
    M-->>A: 注册并维护指标
    P->>A: 定时 GET /actuator/prometheus
    A-->>P: 返回指标文本
    P->>P: 存储时序数据并评估告警规则

四、实战代码(可运行)

下面我们搭一个最小可运行示例:一个订单接口,暴露系统指标和业务指标,并让 Prometheus 抓取。

4.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 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>
    <name>boot-monitor-demo</name>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.18</version>
        <relativePath/>
    </parent>

    <properties>
        <java.version>8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <dependency>
            <groupId>io.micrometer</groupId>
            <artifactId>micrometer-registry-prometheus</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

4.2 配置 Actuator 与 Prometheus 暴露端点

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: when_authorized
  metrics:
    tags:
      application: ${spring.application.name}

这里有几个重点:

  • include 显式暴露 prometheus 端点
  • 给所有指标打一个统一标签 application
  • health 细节不要默认全开,后面会讲安全问题

4.3 启动类

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);
    }
}

4.4 编写业务指标埋点

我们做一个订单服务,统计:

  • 下单成功总数
  • 下单失败总数
  • 下单耗时
  • 当前处理中订单数(模拟中的 Gauge)

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.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

@Service
public class OrderService {

    private final MeterRegistry meterRegistry;

    private Counter orderSuccessCounter;
    private Counter orderFailedCounter;
    private Timer orderCreateTimer;
    private final AtomicInteger processingOrders = new AtomicInteger(0);

    public OrderService(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
    }

    @PostConstruct
    public void initMetrics() {
        orderSuccessCounter = Counter.builder("biz_order_create_success_total")
                .description("订单创建成功次数")
                .tag("service", "order")
                .register(meterRegistry);

        orderFailedCounter = Counter.builder("biz_order_create_failed_total")
                .description("订单创建失败次数")
                .tag("service", "order")
                .register(meterRegistry);

        orderCreateTimer = Timer.builder("biz_order_create_duration")
                .description("订单创建耗时")
                .tag("service", "order")
                .publishPercentileHistogram()
                .register(meterRegistry);

        Gauge.builder("biz_order_processing_count", processingOrders, AtomicInteger::get)
                .description("当前处理中订单数量")
                .tag("service", "order")
                .register(meterRegistry);
    }

    public String createOrder() {
        long start = System.nanoTime();
        processingOrders.incrementAndGet();

        try {
            mockBusiness();
            orderSuccessCounter.increment();
            return "create order success";
        } catch (RuntimeException ex) {
            orderFailedCounter.increment();
            throw ex;
        } finally {
            long end = System.nanoTime();
            orderCreateTimer.record(end - start, TimeUnit.NANOSECONDS);
            processingOrders.decrementAndGet();
        }
    }

    private void mockBusiness() {
        try {
            Thread.sleep(100 + new Random().nextInt(400));
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        if (new Random().nextInt(10) < 2) {
            throw new RuntimeException("mock order create failed");
        }
    }
}

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("/orders/create")
    public String create() {
        return orderService.createOrder();
    }
}

3. 全局异常处理,避免失败请求返回 500 时看不懂

src/main/java/com/example/demo/controller/GlobalExceptionHandler.java

package com.example.demo.controller;

import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(RuntimeException.class)
    public String handleRuntimeException(RuntimeException ex) {
        return "create order failed: " + ex.getMessage();
    }
}

4.5 运行与验证

启动应用后,先访问:

curl http://localhost:8080/orders/create

多调用几次,再看指标端点:

curl http://localhost:8080/actuator/prometheus

你会看到类似输出:

# HELP biz_order_create_success_total 订单创建成功次数
# TYPE biz_order_create_success_total counter
biz_order_create_success_total{application="boot-monitor-demo",service="order",} 8.0

# HELP biz_order_create_failed_total 订单创建失败次数
# TYPE biz_order_create_failed_total counter
biz_order_create_failed_total{application="boot-monitor-demo",service="order",} 2.0

此外还会看到很多系统指标,例如:

  • jvm_memory_used_bytes
  • system_cpu_usage
  • http_server_requests_seconds_count

这说明应用监控数据已经“产出来了”。


4.6 配置 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

然后在 Prometheus UI 中尝试查询:

biz_order_create_success_total

或者看过去 1 分钟失败增量:

increase(biz_order_create_failed_total[1m])

4.7 增加告警规则

一个能落地的监控方案,不能只停在“能看图”,还要能提醒。

比如我们定义两个最基础的告警:

  1. 最近 2 分钟有订单创建失败
  2. 应用实例不可用

alert-rules.yml

groups:
  - name: spring-boot-alerts
    rules:
      - alert: OrderCreateFailuresDetected
        expr: increase(biz_order_create_failed_total[2m]) > 0
        for: 1m
        labels:
          severity: warning
        annotations:
          summary: "订单创建出现失败"
          description: "最近 2 分钟内检测到订单创建失败"

      - alert: SpringBootAppDown
        expr: up{job="spring-boot-app"} == 0
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "Spring Boot 应用不可达"
          description: "Prometheus 无法抓取 spring-boot-app 指标"

把规则文件加入 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']

完整告警链路如下:

flowchart TD
    A[业务请求异常] --> B[Micrometer 指标变化]
    B --> C[Prometheus 抓取到异常指标]
    C --> D[Alert Rule 命中]
    D --> E[Alertmanager]
    E --> F[邮件/钉钉/企业微信/短信]

本文重点放在应用侧与 Prometheus 侧,Alertmanager 的通知模板与路由配置可以在后续单独展开。


五、逐步验证清单

这一部分非常实用,我建议你在本地按顺序验证,避免“一把梭全配完,不知道哪一步错了”。

第 1 步:确认 Actuator 已启用

访问:

curl http://localhost:8080/actuator

若返回可用端点列表,说明管理端点正常。

第 2 步:确认 Prometheus 端点暴露成功

访问:

curl http://localhost:8080/actuator/prometheus

如果返回文本格式指标,说明 Micrometer + Prometheus Registry 工作正常。

第 3 步:触发业务指标变化

连续调用:

for i in {1..10}; do curl http://localhost:8080/orders/create; echo; done

再搜索指标:

curl http://localhost:8080/actuator/prometheus | grep biz_order

第 4 步:确认 Prometheus 抓取状态

打开 Prometheus 页面:

http://localhost:9090/targets

看到 target 为 UP 才算采集成功。

第 5 步:验证 PromQL

尝试这些表达式:

up
biz_order_create_success_total
rate(http_server_requests_seconds_count[1m])
increase(biz_order_create_failed_total[5m])

六、常见坑与排查

这一段我尽量写得接地气一点,因为这些问题真的是大家最常踩的。

6.1 /actuator/prometheus 访问 404

常见原因

  1. 没引入 micrometer-registry-prometheus
  2. 没暴露 prometheus 端点
  3. Spring Boot 版本和依赖版本不兼容

排查方式

先看依赖是否存在,再看配置:

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus

如果还是不行,建议查看启动日志中与 actuator、metrics 相关的自动配置输出。


6.2 Prometheus target 一直是 DOWN

常见原因

  1. metrics_path 配错了
  2. 目标机器端口不通
  3. 应用绑定在 127.0.0.1,Prometheus 不在同一机器
  4. 容器环境下把 localhost 当成宿主机地址了

排查思路

先在 Prometheus 所在机器执行:

curl http://目标地址:8080/actuator/prometheus

如果 curl 不通,就不是 Prometheus 配置问题,而是网络或服务暴露问题。


6.3 自定义指标有了名字,但值一直不变

常见原因

  1. 指标对象每次请求都重新创建
  2. Gauge 引用对象被 GC 回收
  3. 实际代码路径没执行到埋点逻辑

我踩过的一个坑

很多人会在方法里直接这样写:

Counter.builder("demo_counter").register(registry).increment();

这不是绝对错,但如果你在高频路径里每次都注册一次,既浪费性能,也容易让代码变得混乱。更稳妥的做法是:

  • 在初始化阶段注册指标
  • 在业务执行时只做 increment() / record()

6.4 标签设计不当导致指标爆炸

这是生产环境里特别危险的问题。

错误示例

Counter.builder("order_create_total")
    .tag("userId", userId)
    .register(registry);

如果 userId 是高基数字段,Prometheus 的时序数量会迅速膨胀,内存和查询性能都会变差。

原则

不要把用户 ID、订单号、请求 ID 这类高基数值作为标签。

更适合做标签的是:

  • service=order
  • result=success|failed
  • region=cn-east
  • method=POST

6.5 HTTP 指标很多,但看不出业务慢在哪

这是系统指标和业务指标脱节的问题。

例如你能看到:

  • http_server_requests_seconds

但看不到:

  • 下单接口中,数据库耗时多少
  • 调用第三方支付耗时多少
  • 库存检查耗时多少

解决方式是:按关键业务步骤补充 Timer 指标

例如:

Timer.builder("biz_payment_invoke_duration")
    .tag("channel", "wechat")
    .register(registry);

七、安全/性能最佳实践

监控系统本身也有成本,设计不好,可能先把自己监控挂了。

7.1 不要把所有 Actuator 端点都暴露到公网

很多示例为了方便,喜欢配置:

management:
  endpoints:
    web:
      exposure:
        include: "*"

开发环境可以临时这么干,生产环境不建议。

推荐做法

只暴露必要端点:

management:
  endpoints:
    web:
      exposure:
        include: health,info,prometheus

并结合以下措施:

  • 通过网关或防火墙限制访问来源
  • 管理端口和业务端口隔离
  • 给敏感端点加认证授权

例如独立管理端口:

management:
  server:
    port: 8081
  endpoints:
    web:
      exposure:
        include: health,info,prometheus

7.2 控制指标标签维度

记住一句话:标签不是越多越好,而是越稳越好。

建议:

  • 标签值枚举范围要小
  • 标签语义要稳定
  • 能不用动态值就不用动态值

好的标签设计

biz_order_create_total{service="order",result="success"}

不好的标签设计

biz_order_create_total{userId="10000001",orderId="OD20240821001"}

7.3 Timer 和百分位统计不要乱开

像这段配置:

Timer.builder("biz_order_create_duration")
    .publishPercentileHistogram()
    .register(meterRegistry);

很有用,但也会增加统计成本。

建议

只对关键路径开启:

  • 核心交易接口
  • 第三方依赖调用
  • 容易成为瓶颈的慢操作

不要什么方法都开 histogram,否则指标量和内存占用会明显上升。


7.4 告警要“可执行”,不要“可吓人”

很多团队告警规则的问题是:

  • 阈值太敏感,半夜疯狂误报
  • 只有“异常了”,没有定位信息
  • 没有分级,所有告警都像事故

更好的思路

  1. 先监控“可用性”

    • up == 0
    • 健康检查失败
  2. 再监控“错误率”

    • 失败次数增长
    • 5xx 比例升高
  3. 最后监控“性能劣化”

    • P95/P99 响应时间升高
    • 线程池、连接池接近饱和

告警内容建议包含

  • 哪个服务
  • 哪个环境
  • 从什么时候开始
  • 指标值是多少
  • 建议先看哪块

7.5 管理端点与业务流量解耦

如果你的服务流量大,建议把管理端点单独考虑:

  • 单独端口
  • 单独 ingress 路由
  • 单独访问控制
  • 避免被业务网关限流策略误伤

八、监控指标设计建议:从“能看”到“有用”

很多团队接上 Prometheus 后,图很多,但对排障帮助不大。根源往往是没有指标分层设计。

我通常建议按三层来建:

1. 基础层:系统与运行时指标

关注服务是否“活着”:

  • JVM 内存
  • GC 次数/耗时
  • CPU 使用率
  • 线程数
  • HTTP 请求量、响应时间、状态码
  • 连接池活跃数

2. 组件层:依赖资源指标

关注服务是否“卡在外部”:

  • 数据库连接池
  • Redis 连接/命中率
  • MQ 消费积压
  • 线程池队列长度
  • 外部接口调用耗时/失败率

3. 业务层:真正支持决策的指标

关注业务是否“受影响”:

  • 下单成功率
  • 支付失败率
  • 库存扣减冲突次数
  • 用户注册转化率
  • 退款处理耗时

下面这张图可以帮助你建立指标分层意识:

classDiagram
    class 监控体系 {
      基础层指标
      组件层指标
      业务层指标
    }

    class 基础层指标 {
      JVM
      CPU
      HTTP请求
      线程
    }

    class 组件层指标 {
      数据库连接池
      Redis
      MQ
      线程池
    }

    class 业务层指标 {
      下单成功率
      支付失败率
      核心接口耗时
    }

    监控体系 --> 基础层指标
    监控体系 --> 组件层指标
    监控体系 --> 业务层指标

九、一个更贴近生产的配置建议

如果你准备把这套东西放到测试环境或生产环境,建议从下面这个思路起步:

应用侧

  • 开启 healthinfoprometheus
  • 给所有指标补 application 标签
  • 为关键业务埋 CounterTimer
  • 不暴露高风险端点

Prometheus 侧

  • 抓取周期 15s 或 30s
  • 对核心服务配置基础告警
  • 先上少量高价值规则,不要一开始就几十条

告警侧

  • critical:服务不可用、核心交易失败率过高
  • warning:性能下降、少量失败、资源接近阈值
  • 通知渠道按环境隔离,避免测试环境吵到生产值班

十、总结

到这里,你已经搭建了一个完整的基础监控闭环:

  • Actuator 暴露管理与指标端点
  • Micrometer 统一采集系统和业务指标
  • Prometheus 抓取、查询、评估告警规则

如果你只记住三件事,我建议是:

  1. 先把基础监控跑起来health + prometheus + JVM/HTTP
  2. 再补关键业务指标:成功率、失败率、耗时
  3. 最后做告警收敛:先保可用性,再看错误率,再看性能

还有一个很实际的建议:
不要一上来追求“监控平台很大很全”,先从一个核心服务做透。把接口耗时、失败率、实例存活这些最关键的信号稳定下来,你的线上可观测性就已经比很多项目强不少了。

如果你正在做中型 Spring Boot 项目,这套方案完全可以作为团队监控基线。边跑边补业务指标,通常两三轮迭代后,排障效率会有非常明显的提升。


分享到:

上一篇
《区块链中智能合约安全审计实战:从常见漏洞识别到自动化检测流程搭建-330》
下一篇
《Spring Boot + MyBatis 实战:在 Java Web 项目中设计高可用的用户权限与接口鉴权体系》