Micrometer 可观测性体系:指标、日志、链路追踪的统一方案

从三套埋点到一套 API:可观测性统一的真正动机

做过微服务监控的人大概都经历过这种困境:指标用 Micrometer 写一套,链路追踪用 Spring Cloud Sleuth 又写一套,日志里想关联 TraceId 还得自己加 MDC。三套东西各管各的,埋点代码侵入业务逻辑,维护起来像在三个系统之间来回打补丁。

Micrometer 可观测性体系:指标、日志、链路追踪的统一方案

这个问题的根源不在于工具不够好,而在于可观测性的三大支柱——指标、日志、链路追踪——在 API 层面一直是割裂的。你为一个 HTTP 请求记录耗时,这个数据在 Metrics 里是一个 Timer,在 Tracing 里是一个 Span,在日志里是一行带 TraceId 的文本。同一个事件,要用三套 API 描述三遍,而且它们之间没有共享的上下文。

Micrometer 从 1.10 开始引入的 Observation API,就是冲着这个问题来的。它的核心思路很简单:你只埋一次点,用一套 API 描述一个观测事件,Micrometer 负责把这个事件同时分发到指标、追踪和日志三个通道。Spring Boot 3 把这套机制作为默认的可观测性基础设施,Spring Cloud Sleuth 也正式被 Micrometer Tracing 替代。

这不是一个简单的库升级,而是 Java 生态在可观测性层面的一次架构收敛。下面我会从实际工程角度拆解这套体系怎么运作,以及在真实项目中落地时需要关注哪些问题。

Observation API 的工作机制:一次埋点,三路输出

要理解 Micrometer 可观测性体系的统一性,得先看 Observation API 的内部结构。一个 Observation 的生命周期分为几个阶段:start → event → stop,每个阶段都会被注册的 ObservationHandler 消费。

关键在于,ObservationRegistry 里可以注册多个 Handler,每个 Handler 负责把观测数据投递到不同的后端。比如 MetricsObservationHandler 会把观测事件转成 Micrometer 指标,TracingObservationHandler 会创建对应的 Span,而日志关联则通过自动注入 MDC 的 TraceId/SpanId 来实现。

@Component
public class OrderService {

    private final ObservationRegistry registry;

    public OrderService(ObservationRegistry registry) {
        this.registry = registry;
    }

    public Order createOrder(OrderRequest request) {
        return Observation.createNotStarted("order.create", registry)
            .lowCardinalityKeyValue("type", request.getType())
            .highCardinalityKeyValue("customerId", request.getCustomerId())
            .observe(() -> {
                // 业务逻辑
                return doCreateOrder(request);
            });
    }
}

这段代码只写了一次埋点,但它的效果是:Micrometer 会记录 order.create 的耗时分布(作为 Timer),Tracing 会生成一个名为 order.create 的 Span,而日志框架会自动在日志行中附带这个操作的 TraceId 和 SpanId。lowCardinalityKeyValue 适合放可枚举的维度(会作为指标标签),highCardinalityKeyValue 适合放不可枚举的值(只会出现在 Span 属性中,不会作为指标标签)。

这个区分非常重要。很多团队第一次用的时候会把 customerId 直接放到 low cardinality 里,结果 Prometheus 的指标标签基数爆炸,直接把内存撑爆。

Spring Boot 3 的自动配置:开箱即用到什么程度

Spring Boot 3 对 Micrometer 的集成已经非常深了。引入 Actuator 和对应的 registry 依赖后,HTTP 请求、数据库调用、Redis 操作、Kafka 消费等常见组件的埋点都是自动完成的,不需要手写任何 Observation 代码。

典型的依赖配置长这样:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-tracing-bridge-brave</artifactId>
</dependency>
<dependency>
    <groupId>io.zipkin.reporter2</groupId>
    <artifactId>zipkin-reporter-brave</artifactId>
</dependency>

对应的 YAML 配置:

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
  tracing:
    sampling:
      probability: 0.1
  metrics:
    distribution:
      percentiles-histogram:
        http.server.requests: true
      percentiles:
        http.server.requests: 0.5, 0.95, 0.99
    tags:
      application: ${spring.application.name}

配置好之后,你的应用就同时具备了 Prometheus 指标导出、Brave 链路追踪、以及日志 TraceId 自动注入能力。日志关联不需要手动改 logback 配置,Spring Boot 会自动在日志模式中加入 %X{traceId}%X{spanId}

但”开箱即用”不等于”生产可用”。自动埋点覆盖的是标准组件,你的业务代码中那些自定义的、跨方法的复杂操作链路,还是需要手动加 Observation。而且在高流量场景下,默认配置的采样率和直方图精度可能都不合适,需要根据实际情况调整。

Brave 还是 OpenTelemetry:桥接方案怎么选

这是落地 Micrometer Tracing 时最常见的决策点。Micrometer 本身不实现追踪,它通过桥接器对接不同的追踪后端,目前主流的两个选择是 Brave(对接 Zipkin)和 OpenTelemetry(对接 OTLP 兼容的后端,如 Jaeger、Tempo 等)。

维度 Brave Bridge OpenTelemetry Bridge
追踪后端 Zipkin(原生支持) Jaeger、Tempo、Datadog 等 OTLP 兼容后端
依赖复杂度 较低,依赖链短 较高,引入 OTel SDK 全家桶
生态扩展性 主要绑定 Zipkin 生态 CNCF 标准,跨语言一致性好
自动传播 支持 W3C TraceContext 支持 W3C TraceContext + Baggage
日志关联 自动注入 MDC 自动注入 MDC
适合场景 已有 Zipkin 基础设施的中小团队 多语言微服务、需要统一可观测性标准的团队

我的判断是:如果你的服务全是 Java,而且已经在用 Zipkin,Brave 桥接是最省事的选择,依赖少、配置简单、行为可预期。但如果你有 Go、Python、Node.js 等多语言服务,或者你的后端基础设施正在向 Grafana 全家桶(Tempo + Loki + Prometheus)迁移,那 OpenTelemetry 桥接是更面向未来的选择。

切换桥接器对业务代码是零侵入的——因为业务代码只依赖 Observation API,不直接依赖 Brave 或 OTel。换桥接器只需要改依赖和少量配置,这也是 Micrometer 抽象层的价值所在。

生产环境中的真实坑点

采样率与追踪丢失

默认 10% 的采样率在开发环境够用,但到了生产环境会有问题。不是采样率太低导致关键请求丢失,而是采样率太低时你根本无法建立统计意义上的性能基线。一个每天调用 100 万次的接口,10% 采样意味着每天有 10 万条 Span,看起来不少,但如果你想分析 P99 延迟的尾部异常,采样会系统性地丢失慢请求——因为慢请求往往是少数,被采样命中的概率更低。

一个比较务实的做法是:对关键业务路径设置更高的采样率,甚至全量采样。Micrometer 支持通过 Sampler 自定义采样策略:

@Bean
public Sampler customSampler() {
    return new Sampler() {
        @Override
        public SamplingResult decide(Request request) {
            String name = request.getName();
            // 支付类接口全量采样
            if (name != null && name.startsWith("payment")) {
                return SamplingResult.record();
            }
            // 其他接口 5% 采样
            return Math.random() < 0.05
                ? SamplingResult.record()
                : SamplingResult.notRecord();
        }
    };
}

指标标签基数失控

这是我在生产中见过最多的事故类型。有个团队把 HTTP 请求的完整 URI 作为指标标签上报到 Prometheus,某个接口的 URI 里带了请求参数(因为路由配置问题),结果一天之内生成了 20 万个不同的标签值,Prometheus 的内存从 2GB 涨到 16GB 后 OOM。

Micrometer 默认对 http.server.requests 指标的 URI 标签做了归一化处理,会去掉路径变量。但如果你的自定义 Observation 用了 lowCardinalityKeyValue 传入了高基数维度,这个问题就会复现。

原则是:指标标签的候选值数量必须可控。状态码、HTTP 方法、服务名这类适合做标签,用户 ID、请求参数、完整 URL 这类绝对不适合。

日志与链路脱节

自动配置的日志 TraceId 注入依赖 MDC,而 MDC 是线程本地的。一旦你的代码里用了 @Async、线程池或者响应式编程(WebFlux),MDC 的上下文传播就会断掉,日志里的 TraceId 就没了。

对于 @Async 和线程池场景,Spring Boot 3 已经通过 MicrometerObservationListener 和 TaskDecorator 机制做了自动传播。但如果你手动创建线程池或用 CompletableFuture 链式调用,就需要自己处理上下文传播:

// 手动传播观测上下文
Observation.Scope scope = observation.openScope();
try {
    CompletableFuture.supplyAsync(() -> {
        // scope 中的上下文会自动传播到新线程
        return doWork();
    }, threadPool).thenAccept(result -> {
        log.info("处理完成"); // 这行日志会带 TraceId
    });
} finally {
    scope.close();
}

WebFlux 场景更复杂一些,需要确保 Reactor 的上下文传播配置正确。Spring Boot 3.2+ 对此做了大量改进,但如果你在用更早的版本,可能还会遇到上下文丢失的问题。

自定义 Observation 的实践建议

自动埋点覆盖了大部分基础设施层面,但真正有业务价值的观测数据往往需要自定义。在设计自定义 Observation 时,有几个经验值得参考。

  • 命名要稳定且可枚举:Observation 名称会直接作为指标名称和 Span 名称,一旦上线就很难改。建议采用 domain.action 的格式,如 order.createinventory.deduct,避免把动态内容放到名称里。
  • 区分高低基数标签:low cardinality 的 key 会同时出现在指标和 Span 中,high cardinality 的 key 只出现在 Span 中。利用这个特性,把适合聚合的维度放低基数,把只用于排查的上下文放高基数。
  • 避免嵌套过深:Observation 支持父子关系,但过深的嵌套会导致 Span 树过于复杂,而且每一层都会增加指标维度。一般控制在 2-3 层以内。
  • 合理使用事件:Observation 支持 .event(Observation.Event.of("something-happened")) 记录事件点,这些事件会出现在 Span 的时间轴上,适合标记业务关键时刻,比如”库存检查通过””风控拦截触发”。

从 Spring Cloud Sleuth 迁移:不是简单换依赖

如果你的项目还在用 Spring Cloud Sleuth,迁移到 Micrometer Tracing 不只是改个依赖那么简单。Sleuth 的 API 和 Micrometer Observation API 在设计理念上有差异,Sleuth 更偏向”自动给你的所有操作加上追踪”,而 Observation API 更强调”显式声明你想观测什么”。

迁移过程中常见的几个问题:

第一,Sleuth 的 @NewSpan 注解在 Micrometer Tracing 中没有直接等价物。你需要把它们改成手动 Observation 调用,或者使用 Spring Boot 3 提供的 @Observed 注解(功能类似但语义不完全一致)。

第二,Sleuth 的 Baggage 传播方式和 Micrometer Tracing 的 Baggage API 不同。如果你在 Sleuth 中大量使用了 Baggage 传递业务上下文(比如用户 ID、租户 ID),迁移时需要改用 Micrometer 的 BaggageManager

第三,日志格式可能有细微差异。Sleuth 默认的日志格式是 [app,traceId,spanId],而 Micrometer Tracing 默认是 [traceId,spanId]。如果你的日志解析系统对格式有硬编码依赖,这会是一个需要同步调整的点。

可观测性不是工具堆叠,而是上下文打通

回到最初的问题:为什么要把指标、日志、链路追踪统一到一套 API 下?

真正有价值的不是三个支柱各自的采集能力,而是它们之间的关联能力。当你收到一个 P99 延迟告警时,你需要的是:从指标看到哪个接口慢了 → 从追踪看到这个请求经过了哪些服务、每一步耗时多少 → 从日志看到每一步内部的具体行为。这个”从指标到追踪到日志”的关联跳转,只有在三者共享上下文的前提下才能顺畅完成。

Micrometer 通过 Observation API 统一了埋点入口,通过 TraceId/SpanId 自动注入打通了日志与追踪,通过指标标签上的维度对齐了指标与追踪。这不是三个工具的简单组合,而是一套上下文贯穿的可观测性体系。

对于正在做技术选型的团队,如果你的技术栈以 Spring Boot 为主,Micrometer 已经是默认且最优的选择,不需要犹豫。如果你是多语言环境,Micrometer 负责 Java 侧,配合 OpenTelemetry 做跨语言统一,是目前最务实的路径。核心不在于你用了哪个工具,而在于你是否建立了一套从指标到追踪到日志的完整关联链路——这才是可观测性体系真正的价值所在。

原创文章,作者:,如若转载,请注明出处:https://fudengji.cn/article/310/

(0)
上一篇 1天前
下一篇 1天前

相关推荐