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
你会看到 count、sum、max 以及 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 后端、架构治理和线上问题排查相关实战笔记。