Spring Boot Actuator + Micrometer 自定义业务指标:不只是健康检查

Spring Boot Actuator + Micrometer 自定义业务指标:不只是健康检查

摘要:Spring Boot Actuator 不只是 /actuator/health,Micrometer 也不只是 JVM、HTTP 默认指标。对 Java 后端来说,真正能支撑故障排查和容量治理的,往往是"业务指标":订单创建量、支付成功率、接口分段耗时、队列积压、库存扣减失败次数。本文基于 Spring Boot 3.x + Micrometer,演示如何注册 Counter、Gauge、Timer 和 DistributionSummary,导出 Prometheus 格式指标,并给出一套业务指标设计和排查落地流程。

1. 为什么只看 health 不够

很多项目接入 Actuator 后,只做了两件事:

复制代码
/actuator/health 看服务是否存活
/actuator/metrics 看 JVM、HTTP、线程池等默认指标

这些指标有用,但它们回答的是"系统资源是否异常",不一定能回答"业务到底哪里慢了、哪里失败了"。

比如线上支付接口报警,你可能看到:

  • JVM 内存正常;
  • CPU 没有明显打满;
  • HTTP 请求耗时升高;
  • 但不知道是创建订单慢、调用支付渠道慢,还是回写订单状态慢。

这时就需要业务指标。

一个更完整的监控视角应该是:

复制代码
基础指标:JVM / CPU / 内存 / 线程 / HTTP
业务指标:订单量 / 支付成功率 / 渠道耗时 / 库存失败 / 队列积压
排查指标:接口分段耗时 / 异常计数 / 重试次数 / 限流次数

本文用一个订单支付场景演示如何把这些指标接入 Spring Boot。

验证状态:本文示例基于 Spring Boot 3.x、Micrometer 1.12+、Actuator Prometheus endpoint 的常见用法整理。代码可作为本地 demo 验证,生产接入时需要按自身业务维度控制标签数量。

2. 环境和依赖

示例环境:

项目 版本 / 说明
JDK 17+
Spring Boot 3.x
Micrometer Spring Boot Actuator 默认集成
指标导出 Prometheus 格式
场景 订单创建、支付、库存扣减

Maven 依赖:

复制代码
<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>

application.yml 开启 Prometheus endpoint:

复制代码
server:
  port: 8080

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
  endpoint:
    health:
      show-details: always
  metrics:
    tags:
      application: order-service

启动后可以先验证:

复制代码
curl http://localhost:8080/actuator/health
curl http://localhost:8080/actuator/metrics
curl http://localhost:8080/actuator/prometheus | head

如果 /actuator/prometheus 能看到文本格式指标,说明 Prometheus registry 已经生效。

3. Micrometer 四类常用指标怎么选

不要一上来就写代码,先选对指标类型。

指标类型 适合场景 示例
Counter 只增不减的次数 订单创建次数、支付失败次数、重试次数
Gauge 当前瞬时值 队列积压数、库存剩余数、线程池队列长度
Timer 耗时和次数 支付渠道调用耗时、创建订单接口耗时
DistributionSummary 分布统计,不一定是时间 订单金额分布、批处理条数、消息大小

一个简单判断规则:

复制代码
是次数:Counter
是当前值:Gauge
是耗时:Timer
是大小/金额/数量分布:DistributionSummary

常见误用是把当前值做成 Counter,或者给 Gauge 绑定一个临时对象导致数值被 GC 后不可用。后面会专门讲坑。

4. 用 Counter 统计业务事件次数

先定义一个订单服务,注入 MeterRegistry

java 复制代码
`package com.example.metricsdemo.service;

import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.stereotype.Service;

@Service
public class OrderMetricsService {

    private final Counter orderCreatedCounter;
    private final Counter paymentFailedCounter;

    public OrderMetricsService(MeterRegistry meterRegistry) {
        this.orderCreatedCounter = Counter.builder("biz_order_created_total")
                .description("Total number of created orders")
                .tag("channel", "web")
                .register(meterRegistry);

        this.paymentFailedCounter = Counter.builder("biz_payment_failed_total")
                .description("Total number of failed payments")
                .tag("channel", "web")
                .register(meterRegistry);
    }

    public void recordOrderCreated() {
        orderCreatedCounter.increment();
    }

    public void recordPaymentFailed() {
        paymentFailedCounter.increment();
    }
}
`

测试接口:

java 复制代码
`package com.example.metricsdemo.controller;

import com.example.metricsdemo.service.OrderMetricsService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class OrderController {

    private final OrderMetricsService metricsService;

    public OrderController(OrderMetricsService metricsService) {
        this.metricsService = metricsService;
    }

    @GetMapping("/demo/order/create")
    public String createOrder() {
        metricsService.recordOrderCreated();
        return "order created";
    }

    @GetMapping("/demo/payment/fail")
    public String paymentFail() {
        metricsService.recordPaymentFailed();
        return "payment failed";
    }
}
`

调用几次:

复制代码
curl http://localhost:8080/demo/order/create
curl http://localhost:8080/demo/order/create
curl http://localhost:8080/demo/payment/fail
curl http://localhost:8080/actuator/prometheus | grep biz_

可以看到类似输出:

复制代码
# HELP biz_order_created_total Total number of created orders
# TYPE biz_order_created_total counter
biz_order_created_total{application="order-service",channel="web",} 2.0

# HELP biz_payment_failed_total Total number of failed payments
# TYPE biz_payment_failed_total counter
biz_payment_failed_total{application="order-service",channel="web",} 1.0

Counter 适合做趋势和速率,例如 PromQL:

复制代码
rate(biz_order_created_total[5m])
rate(biz_payment_failed_total[5m])

如果再计算失败率,可以用:

复制代码
rate(biz_payment_failed_total[5m]) / rate(biz_order_created_total[5m])

实际生产中,建议把"成功次数"和"失败次数"都作为业务事件记录下来,而不是只记异常日志。

5. 用 Timer 统计业务分段耗时

很多接口慢,不是整个方法慢,而是某一段慢。

例如支付链路可以拆成:

复制代码
创建订单 -> 扣减库存 -> 调用支付渠道 -> 回写支付状态

如果只看 HTTP 总耗时,排查时还要猜;如果每段都有 Timer,定位会快很多。

示例代码:

java 复制代码
`package com.example.metricsdemo.service;

import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.springframework.stereotype.Service;

import java.time.Duration;
import java.util.concurrent.ThreadLocalRandom;

@Service
public class PaymentService {

    private final MeterRegistry meterRegistry;

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

    public String pay() {
        Timer.Sample sample = Timer.start(meterRegistry);
        String result = "success";
        try {
            mockRemotePayment();
            return result;
        } catch (RuntimeException ex) {
            result = "fail";
            throw ex;
        } finally {
            sample.stop(Timer.builder("biz_payment_channel_latency")
                    .description("Payment channel latency")
                    .tag("channel", "mockpay")
                    .tag("result", result)
                    .publishPercentileHistogram()
                    .serviceLevelObjectives(
                            Duration.ofMillis(100),
                            Duration.ofMillis(300),
                            Duration.ofSeconds(1)
                    )
                    .register(meterRegistry));
        }
    }

    private void mockRemotePayment() {
        try {
            Thread.sleep(ThreadLocalRandom.current().nextInt(50, 500));
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}
`

Controller:

java 复制代码
`@GetMapping("/demo/payment/pay")
public String pay() {
    return paymentService.pay();
}
`

调用后查看:

复制代码
for i in {1..10}; do curl -s http://localhost:8080/demo/payment/pay; echo; done
curl http://localhost:8080/actuator/prometheus | grep biz_payment_channel_latency

你会看到 countsummax 以及 histogram bucket 相关指标。

Timer 的价值在于:

  • 可以看某段业务调用的平均耗时;
  • 可以看 P95 / P99 分位;
  • 可以把成功和失败分开统计;
  • 可以直接对应告警规则。

例如 PromQL:

复制代码
histogram_quantile(
  0.95,
  sum(rate(biz_payment_channel_latency_seconds_bucket[5m])) by (le)
)

如果你的接口 RT 升高,这个指标能帮助判断是否是支付渠道调用拖慢,而不是数据库或应用本身。

6. 用 Gauge 记录当前积压量

Gauge 表示"当前值"。比如队列里还有多少订单待处理,库存服务当前失败队列长度是多少。

示例:

java 复制代码
`package com.example.metricsdemo.service;

import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.stereotype.Service;

import java.util.concurrent.atomic.AtomicInteger;

@Service
public class BacklogMetricsService {

    private final AtomicInteger pendingOrderCount = new AtomicInteger(0);

    public BacklogMetricsService(MeterRegistry meterRegistry) {
        Gauge.builder("biz_order_pending_current", pendingOrderCount, AtomicInteger::get)
                .description("Current pending order count")
                .tag("queue", "order-create")
                .register(meterRegistry);
    }

    public void increase() {
        pendingOrderCount.incrementAndGet();
    }

    public void decrease() {
        pendingOrderCount.updateAndGet(value -> Math.max(0, value - 1));
    }
}
`

Gauge 的一个大坑是:不要这样写临时对象。

java 复制代码
`// 不推荐:对象没有被业务代码持有,可能被 GC,Gauge 后续读不到稳定值
Gauge.builder("bad_gauge", new AtomicInteger(0), AtomicInteger::get)
        .register(meterRegistry);
`

更稳妥的做法是把被观测对象作为字段保存,或者从可靠的数据源读取当前值。

查看指标:

复制代码
curl http://localhost:8080/actuator/prometheus | grep biz_order_pending_current

Gauge 适合告警:

复制代码
biz_order_pending_current > 1000

但不要用 Gauge 表示历史增长次数。历史增长趋势应该用 Counter + rate。

7. 用 DistributionSummary 观察金额或批量大小

如果你想观察订单金额分布、一次批处理消息条数、一次导入文件大小,DistributionSummary 比 Timer 更合适。

示例:

java 复制代码
`package com.example.metricsdemo.service;

import io.micrometer.core.instrument.DistributionSummary;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;

@Service
public class AmountMetricsService {

    private final DistributionSummary orderAmountSummary;

    public AmountMetricsService(MeterRegistry meterRegistry) {
        this.orderAmountSummary = DistributionSummary.builder("biz_order_amount")
                .description("Order amount distribution")
                .baseUnit("yuan")
                .publishPercentileHistogram()
                .register(meterRegistry);
    }

    public void recordAmount(BigDecimal amount) {
        orderAmountSummary.record(amount.doubleValue());
    }
}
`

它能帮助判断:

  • 最近订单金额是否异常偏大;
  • 批处理大小是否突然变大;
  • 某个渠道是否出现异常分布。

不过生产里要谨慎设计金额类指标,不要把用户、订单号、手机号等高基数或敏感信息放进 tag。

8. 标签 tag 怎么设计,最容易踩坑

Micrometer 指标真正难的不是 Counter.builder(),而是标签设计。

一个指标通常可以带 tag:

java 复制代码
`.tag("channel", "web")
.tag("result", "success")
`

标签会形成维度,方便分组查询。但标签值如果无限增长,会让 Prometheus 压力暴涨。

标签设计 是否推荐 原因
result=success/fail 推荐 低基数,适合聚合
channel=web/app/openapi 推荐 维度有限
region=cn/eu/us 可用 数量可控
userId=10001 不推荐 高基数,用户越多指标序列越多
orderNo=NO_xxx 禁止 高基数且可能泄露业务信息
exceptionMessage=完整异常信息 禁止 高基数且不可控

建议规则:

复制代码
tag 只能放有限枚举值,不放用户级、订单级、请求级信息。

如果你确实需要排查某个用户或订单,用日志、Trace、审计表;不要用 Prometheus 指标承载明细。

9. 一套可落地的业务指标设计流程

我建议后端项目按下面的顺序设计指标,而不是看到哪里就埋哪里。

以支付链路为例:

链路环节 指标 类型 典型用途
创建订单 biz_order_created_total Counter 订单创建速率
支付失败 biz_payment_failed_total Counter 失败率告警
支付渠道调用 biz_payment_channel_latency Timer P95/P99 延迟分析
待处理订单 biz_order_pending_current Gauge 积压告警
订单金额 biz_order_amount DistributionSummary 金额分布观察

排查时可以这样看:

复制代码
支付成功率下降
  -> 看 payment_failed_total 是否升高
  -> 看 channel_latency P95 是否升高
  -> 看 pending_order_current 是否积压
  -> 再结合日志 / Trace 定位具体异常

指标不是为了替代日志,而是为了更快告诉你"该往哪里查"。

10. Grafana 面板可以怎么设计

如果接入 Prometheus + Grafana,一个业务面板不要只放 JVM 图。建议至少包含 4 组:

面板 指标 目的
订单创建速率 rate(biz_order_created_total[5m]) 判断流量是否异常
支付失败率 rate(fail[5m]) / rate(total[5m]) 判断业务成功率
支付渠道 P95 histogram_quantile(0.95, ...) 判断外部依赖耗时
当前积压量 biz_order_pending_current 判断处理能力是否不足

一个实用面板顺序是:

复制代码
业务量 -> 成功率 -> 延迟 -> 积压 -> JVM/线程池 -> 日志链接

这样值班同学第一眼看到的是业务是否受影响,而不是先在几十张 JVM 图里找线索。

11. 生产接入注意事项

最后总结几个生产里更容易踩的坑。

11.1 指标名要稳定

不要今天叫 order_create_total,明天改成 biz_order_created_total。指标名一变,PromQL、告警和历史图都会断。

建议命名:

复制代码
业务域_对象_动作_单位
biz_order_created_total
biz_payment_channel_latency_seconds
biz_order_pending_current

11.2 不要滥用 tag

最重要的规则再说一遍:

复制代码
指标 tag 只能放低基数枚举,不能放用户 ID、订单号、请求 ID。

高基数指标会让监控系统变慢,也会让问题更难排查。

11.3 不要用指标保存明细

指标适合趋势、聚合和告警;明细应该交给日志、Trace、数据库或审计系统。

例如:

  • 指标告诉你支付失败率升高;
  • 日志告诉你失败原因;
  • Trace 告诉你慢在哪个调用;
  • 业务表告诉你具体订单状态。

11.4 Timer 不等于日志耗时

Timer 适合聚合统计,不适合还原单次请求。单次请求链路仍然需要 Trace 或日志 requestId。

11.5 先埋核心指标,不要一次铺太多

业务指标不是越多越好。第一版建议只做:

复制代码
核心业务量
关键失败次数
核心外部依赖耗时
核心积压量

等告警和面板真的用起来,再逐步补充。

12. 小结

Spring Boot Actuator 提供入口,Micrometer 提供指标抽象,Prometheus 和 Grafana 负责采集与展示。但真正让监控有价值的,是你是否把业务链路拆成可观测的指标。

本文的核心结论:

复制代码
health 只能说明服务还活着;
业务指标才能说明服务是否真的把业务跑好了。

对 Java 后端项目来说,建议从这四类指标开始:

  • Counter:记录核心业务事件次数;
  • Timer:记录关键调用和外部依赖耗时;
  • Gauge:记录当前积压和资源状态;
  • DistributionSummary:记录金额、大小、批量等分布。

如果你正在做后端服务治理、接口稳定性或可观测性建设,可以先从一条最核心业务链路开始,把"业务量、失败率、耗时、积压"四类指标接起来。后面排查问题时,它会比只看 health 和 JVM 指标有用得多。

如果这篇文章对你有帮助,可以关注我的 CSDN,后续会继续整理 Java 后端、架构治理和线上问题排查相关实战笔记。

相关推荐
Eason_LYC1 小时前
【GetShell 实战】CVE-2026-34486 Tomcat 加密拦截器绕过:从漏洞验证到反弹 Shell 全流程
java·渗透测试·tomcat·java反序列化·rce·远程代码执行漏洞·cve-2026-34486
xiangw@GZ1 小时前
ARM TCM 紧耦合内存与 Cache 架构区别
arm开发·架构
ting94520001 小时前
InsForge Backend Branching 后端全链路 Git 式分支技术原理、架构实现与底层源码剖析
人工智能·git·elasticsearch·架构
qq_2518364571 小时前
基于java 税务管理系统设计与实现
java·开发语言
超梦dasgg1 小时前
Java 生产环境分布式定时任务全解(实战落地版)
java·开发语言·分布式
破土士V1 小时前
Java基础知识集合
java·开发语言
工控发烧友1 小时前
边缘计算 vs 云端处理:工业场景如何选择数据处理架构
人工智能·架构·边缘计算
一只齐刘海的猫1 小时前
【Leetcode】 接雨水
java·算法·leetcode
ZC跨境爬虫1 小时前
跟着 MDN 学JavaScript day_5:技能测试——变量实战
java·开发语言·前端·javascript