日志回答「发生了什么」,指标回答「系统现在怎么样」,链路回答「这一次请求慢在哪」。Micrometer 是 Java 界的「SLF4J for metrics」——它提供统一的计量 API,把指标后端(Prometheus、Datadog、OTLP 等)抽象成可插拔的实现,让业务代码只写一次埋点就能对接任意监控系统。配合 Spring Boot Actuator,它几乎是零成本接入。本文从四大计量器讲到标签基数陷阱、Observation API 与生产治理。
一、Micrometer 的定位
类比理解:
SLF4J : 日志门面,后端 Logback/Log4j2 可换
Micrometer : 指标门面,后端 Prometheus/Datadog/OTLP 可换
| 能力 | 说明 |
|---|---|
| 门面抽象 | 一套 API,多后端适配 |
| 维度模型 | 指标名 + 标签(tag)构成时间序列 |
| 自动埋点 | JVM、HTTP、数据源、缓存开箱即用 |
| 与 Spring 集成 | Actuator 自动配置,注解驱动 |
| 统一观测 | Observation API 同时产出指标与追踪 |
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
一句话总结: Micrometer 的价值是解耦「埋点」与「后端」——用统一 API 写一次,换监控系统只改依赖,业务代码零改动。
二、四大核心计量器
| 计量器 | 语义 | 典型用途 | 后端形态 |
|---|---|---|---|
| Counter | 单调递增计数 | 请求数、错误数、处理条数 | 累加值(rate 由后端算) |
| Gauge | 瞬时值(可升可降) | 队列长度、内存占用、连接数 | 当前值快照 |
| Timer | 耗时分布 + 计数 | 请求 RT、方法耗时 | 计数 + 分位/直方图 |
| DistributionSummary | 数值分布 | 响应体大小、批量条数 | 计数 + 分位/直方图 |
@Service
public class OrderService {
private final Counter created;
private final Timer createTimer;
private final AtomicInteger queueSize = new AtomicInteger();
public OrderService(MeterRegistry registry) {
this.created = Counter.builder("order.created.total")
.description("创建的订单总数")
.tag("type", "normal")
.register(registry);
this.createTimer = Timer.builder("order.create.duration")
.description("订单创建耗时")
.publishPercentiles(0.5, 0.95, 0.99)
.register(registry);
// Gauge 必须持有被观测对象的强引用,否则会被 GC 回收
Gauge.builder("order.queue.size", queueSize, AtomicInteger::get)
.description("待处理订单队列长度")
.register(registry);
}
public Order create(CreateCmd cmd) {
return createTimer.record(() -> { // 自动计时 + 异常也记录
Order o = doCreate(cmd);
created.increment();
return o;
});
}
}
Counter vs Gauge 的常见误用:
错误:用 Counter 记录「当前在线人数」(会降,Counter 只能增)
正确:用 Gauge 或直接读业务指标
错误:手动给 Timer 加标签 "result=success/failure" 让基数翻倍
正确:失败单独一个 Counter,避免同一指标标签爆炸
一句话总结: 选计量器的口诀是「只增用 Counter、瞬态用 Gauge、耗时用 Timer、分布用 Summary」;Gauge 记得持有被观测对象的强引用,否则读数会变 NaN。
三、标签与高基数陷阱
指标 = 名称 + 一组标签(key=value),标签组合数 = 时间序列数。标签基数失控是 Micrometer 生产事故的头号来源。
// 危险:用用户 ID 作标签 → 每条时间序列 × 用户数
registry.counter("api.request", "userId", userId).increment();
// 危险:用 URL 全路径作标签 → 每个 ID 一个序列
registry.counter("http.request", "uri", "/order/12345").increment();
| 标签 | 基数风险 | 建议 |
|---|---|---|
method、status、uri(模板化) | 低 | 推荐 |
userId、orderId、traceId | 极高 | 禁止 |
instance、region | 低 | 推荐 |
error(错误类型,有限枚举) | 中 | 可接受 |
# Spring Boot 3 默认已做 URI 模板化,避免路径变量撑爆基数
management:
metrics:
web:
server:
request:
autotime:
enabled: true
基数治理三原则:
1. 标签值必须来自「有限枚举」,不能是 ID/时间戳/自由文本
2. 路径用模板(/order/{id})而非实际值
3. 高基数需求改用「追踪(trace)」或「日志」承载,别塞进指标
4. 上线前估算:指标数 × 标签组合数 × 实例数,超过 10 万就要警惕
一句话总结: 指标系统的成本几乎全部来自基数,「标签值必须有限枚举」是唯一红线;需要按 ID 下钻的场景,用追踪或日志,不要用指标。
四、Spring Boot Actuator 自动埋点
引入 spring-boot-starter-actuator 后,JVM、HTTP、数据源、缓存等自动接入:
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
metrics:
tags:
application: ${spring.application.name}
distribution:
percentiles-histogram:
http.server.requests: true # 输出直方图,供后端算分位
slo:
http.server.requests: 100ms,500ms,1s
# 查看所有指标名
curl -s localhost:8080/actuator/metrics | jq '.names | length'
# 查看某指标及其标签
curl -s 'localhost:8080/actuator/metrics/http.server.requests?tag=uri:/order/{id}'
# Prometheus 拉取端点
curl -s localhost:8080/actuator/prometheus | grep http_server_requests
开箱即用的指标族:
jvm.memory.used / jvm.gc.pause / jvm.threads.live
http.server.requests(含 uri/method/status/outcome 标签)
hikaricp.connections.*(连接池)
spring.data.repository.invocations
cache.gets / cache.puts
logback.events
一句话总结: Actuator 让「80% 的通用指标零代码接入」,只要配好
management.metrics.tags.application与直方图开关,剩下的自定义埋点才需要写代码。
五、注解式埋点:@Timed 与 @Counted
@Configuration
public class MetricsConfig {
// 注册切面,让注解生效
@Bean
public TimedAspect timedAspect(MeterRegistry registry) {
return new TimedAspect(registry);
}
}
@Service
public class PaymentService {
@Timed(value = "payment.process", percentiles = {0.95, 0.99},
extraTags = {"channel", "alipay"})
public Result process(PaymentCmd cmd) { /* ... */ }
@Counted(value = "payment.retry.total", recordFailuresOnly = false)
public void retry(String id) { /* ... */ }
}
注解埋点的注意点:
1. @Timed 依赖 AOP 代理 → 自调用(this.method())不生效
2. 类必须是 Spring Bean
3. extraTags 是静态值,不能放动态 ID
4. 生产更推荐 Observation API(见下节),注解适合快速接入
一句话总结:
@Timed/@Counted是最省事的接入方式,但受 AOP 自调用限制且标签静态;需要动态标签与统一追踪时,切到 Observation API。
六、Observation API:统一指标与追踪
Micrometer Observation 用一个 Observation 同时产出指标、追踪与日志,避免埋点重复:
@Service
public class InventoryService {
private final ObservationRegistry registry;
public InventoryService(ObservationRegistry registry) {
this.registry = registry;
}
public Stock check(String sku) {
Observation obs = Observation.createNotStarted("inventory.check", registry)
.lowCardinalityKeyValue("warehouse", "cn-east")
.highCardinalityKeyValue("sku", sku); // 只进 trace,不进 metric
return obs.observe(() -> doCheck(sku));
}
}
低基数 vs 高基数标签的自动分流:
lowCardinalityKeyValue → 进入指标(metric),要求有限枚举
highCardinalityKeyValue → 进入追踪(span),可放 ID
这一设计从根上避免「把 traceId 塞进指标」的灾难
// 用 ObservationConvention 统一定义标准标签
public class InventoryConvention implements ObservationConvention<InventoryContext> {
@Override
public KeyValues getLowCardinalityKeyValues(InventoryContext ctx) {
return KeyValues.of("warehouse", ctx.warehouse());
}
@Override
public String getName() { return "inventory.check"; }
}
# Spring Boot 3 已自动为 HTTP/RestTemplate/WebClient 埋 Observation
management:
observations:
key-values:
region: cn-east
一句话总结: Observation API 用「低基数标签进指标、高基数标签进追踪」的机制,从设计上杜绝基数事故;新项目应优先用它而非直接操作 MeterRegistry。
七、对接 Prometheus 与 Grafana
# prometheus.yml
scrape_configs:
- job_name: 'spring-app'
metrics_path: '/actuator/prometheus'
scrape_interval: 15s
static_configs:
- targets: ['app:8080']
指标暴露格式:
# HELP http_server_requests_seconds
# TYPE http_server_requests_seconds histogram
http_server_requests_seconds_bucket{uri="/order/{id}",le="0.1"} 1024
http_server_requests_seconds_bucket{uri="/order/{id}",le="+Inf"} 1200
http_server_requests_seconds_count{uri="/order/{id}"} 1200
http_server_requests_seconds_sum{uri="/order/{id}"} 42.5
# 常见查询
# QPS
rate(http_server_requests_seconds_count{application="order"}[1m])
# P99 延迟(基于直方图)
histogram_quantile(0.99,
sum(rate(http_server_requests_seconds_bucket{application="order"}[5m])) by (le, uri))
# 错误率
sum(rate(http_server_requests_seconds_count{status=~"5.."}[5m]))
/ sum(rate(http_server_requests_seconds_count[5m]))
Exemplar(样本):
在指标里携带 traceId 样本,Grafana 可从延迟毛刺一键跳转到对应 trace
需要 Micrometer Tracing + OTel/Brave 与 Prometheus 3.x 支持
配置:management.tracing.enabled=true,配合 observation
一句话总结: Prometheus 负责「存与算」,Grafana 负责「看与告警」,直方图 +
histogram_quantile是算分位的标准姿势,Exemplar 把指标与追踪打通。
八、生产治理与常见坑
| 坑 | 现象 | 对策 |
|---|---|---|
| 标签用 ID | 时间序列爆炸,Prometheus OOM | 标签值限定枚举 |
| Gauge 读数为 NaN | 被观测对象被 GC | 持有强引用 |
@Timed 不生效 | 自调用绕过代理 | 走 Observation 或注入代理 |
| 分位在客户端算 | 多实例无法聚合 | 用直方图让后端算 |
| 忘记 application 标签 | 多服务指标混淆 | 统一 management.metrics.tags |
| 指标名含点号 | Prometheus 命名冲突 | 用 _ 或让 Micrometer 转换 |
| 无 SLO 分桶 | 无法算达标率 | 配置 distribution.slo |
| 指标无人消费 | 埋了不看 | 埋点与仪表盘/告警同步上线 |
上线检查清单:
1. 每个自定义指标都问:谁消费它?告警还是看板?
2. 估算基数:指标数 × 标签组合 × 实例数 < 10 万
3. 统一标签:application / instance / region 由配置注入,不在代码里写
4. 关键路径埋 Timer,失败路径埋 Counter,别用同一个指标的标签区分
5. 直方图 + SLO 分桶按需开启,避免全量开启导致存储暴涨
6. 指标与追踪的 traceId 用 Exemplar 打通
一句话总结: 指标治理的核心是「基数可控、标签统一、有人消费」;把这三条写进上线检查清单,能避免 90% 的可观测性事故。
小结
| 维度 | 要点 |
|---|---|
| 定位 | Java 的指标门面,一套 API 多后端 |
| 计量器 | Counter / Gauge / Timer / DistributionSummary |
| 标签 | 值必须有限枚举,高基数交给追踪 |
| 接入 | Actuator 自动埋点 + 注解 + Observation API |
| 后端 | Prometheus 存储计算,Grafana 展示告警 |
| 治理 | 基数可控、标签统一、有人消费 |
Micrometer 让 Java 应用的可观测性从「事后补埋点」变成「框架自带」。真正决定成败的不是 API 用法,而是标签设计——把基数管住、把高基数需求交给追踪、把指标与告警绑定上线,你得到的才是一套能用的可观测体系,而不是一堆没人看的时间序列。
延伸阅读
- Java 日志体系与工程实践完整指南 — MDC 链路追踪与日志侧的可观测性
- JFR 与 JMC 性能分析实战 — JVM 内部事件的深度观测
- Spring Boot 3 深度解析:自动装配、Starter 开发与生产就绪 — Actuator 与自动配置机制
- Prometheus 深度实践 — 拉取模型、PromQL 与存储原理
- Grafana 可视化与告警 — 指标看板与告警规则设计
- Micrometer 与 OTLP 可观测性集成 — 对接 OpenTelemetry 协议栈
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。