Micrometer 可观测性

系统讲解 Micrometer 可观测性:MeterRegistry 与 Counter/Gauge/Timer/DistributionSummary 四大计量器、标签与高基数陷阱、Spring Boot Actuator 自动埋点、@Timed/@Counted 注解、Observation API 统一指标与链路追踪、Prometheus 暴露与 exemplar,并附生产治理实践。

日志回答「发生了什么」,指标回答「系统现在怎么样」,链路回答「这一次请求慢在哪」。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」更多文章

  1. Testcontainers 集成测试
  2. Spring Batch 批处理
  3. JPMS 模块系统