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

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

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

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

做 Spring Boot 项目时,很多团队一开始只关心“服务能跑起来”,等线上真的出问题了,才发现自己几乎是“盲飞”:

  • CPU 飙高了不知道是哪个实例
  • 接口变慢了,但没有可观测数据支撑
  • 错误率上升时,只能靠用户反馈
  • JVM 堆内存快满了,却没人提前知道

这篇文章我想换一个更偏“落地”的角度,不是单独介绍某个组件,而是直接带你搭一条完整链路:

Spring Boot 应用 → Actuator 暴露信息 → Micrometer 统一指标采集 → Prometheus 抓取并告警

如果你已经写过 Spring Boot 服务,但对监控体系还是“知道名词,不太敢上手”,这篇会比较适合你。


背景与问题

在没有监控体系之前,很多问题其实都属于“事后分析”:

  1. 服务挂了才知道
  2. 接口慢了说不清慢在哪
  3. 数据库连接池耗尽没有预警
  4. 垃圾回收频繁但没人关注
  5. 应用是健康还是亚健康,无法量化

而一个靠谱的监控体系,至少应该回答这些问题:

  • 应用活着吗?
  • 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:

  1. Web 层接收到请求
  2. Micrometer 自动记录请求次数、耗时、状态码
  3. 指标被注册到 MeterRegistry
  4. Prometheus 下次抓取时拿到这些指标
  5. 规则引擎判断是否需要告警
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 端点

线上环境尽量只开放必要端点:

  • health
  • info
  • prometheus

有些端点信息很多,暴露过宽可能带来安全风险。

推荐配置:

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 压力
  • 增加应用端暴露开销
  • 时序数据量变大

一般来说:

  • 常规业务:15s30s
  • 高频告警场景: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 自动给的指标已经很多了,但真正能体现业务价值的,还得你自己补。

我通常会优先选这几类:

  1. 吞吐类

    • 订单创建数
    • 支付成功数
    • 消息消费数
  2. 积压类

    • 待处理任务数
    • 队列堆积量
    • 重试中的任务数
  3. 质量类

    • 支付失败数
    • 风控拦截率
    • 第三方调用失败率
  4. 耗时类

    • 核心业务流程耗时
    • 外部接口调用耗时
    • 异步任务执行耗时

建议遵守一个简单规则:

如果某个指标异常,会直接影响用户体验、收入、稳定性,那它就值得被监控。


总结

这套方案的核心价值,不是“把指标采出来”,而是让你对应用状态有持续、量化、可告警的认知。

回顾一下本文的主线:

  • Actuator 暴露应用运行端点
  • Micrometer 统一收集 JVM、HTTP 和自定义业务指标
  • Prometheus 定时抓取指标并执行告警规则
  • 通过合理的标签、查询和阈值设计,把监控真正用起来

如果你准备在真实项目里落地,我建议按下面这个最小闭环开始:

  1. 先接入 healthprometheus
  2. 确认 Prometheus 能正常抓取
  3. 观察 JVM、HTTP 请求、错误率、耗时
  4. 为 1~2 个关键业务流程补自定义指标
  5. 先上线 3 条核心告警:
    • 实例不可用
    • 5xx 错误率过高
    • JVM 堆内存持续过高

最后给一个边界条件判断:

  • 如果你现在还是单体应用、小流量、单机部署,这套方案已经足够实用
  • 如果你进入多集群、多租户、高基数业务场景,就要继续考虑 Grafana 展示、Alertmanager 路由、指标治理和采样策略

监控体系不是一口气建成的,但第一步一定值得尽早做。因为很多线上事故,真的不是不能解决,而是你根本没看见它正在发生


分享到:

上一篇
《Java Web开发中基于Spring Boot与MyBatis的接口性能优化实战:从慢SQL排查到线程池与缓存调优》
下一篇
《从源码到部署:基于开源项目 MinIO 搭建高可用对象存储服务的实战指南》