一、可观测性不是“多打日志”

可观测性的三根支柱是:

  • Logs:某个时间点发生了什么;
  • Metrics:系统整体是否异常,例如错误率、延迟分位数、线程池队列长度;
  • Traces:一次请求经过了哪些组件,时间消耗在哪里。

三者各自有价值,但关联后才真正高效:

告警:orders.create 的 P95 延迟超过 800ms
  ↓ 按 service、uri、status 缩小范围
Trace:找到一个 2.4s 的请求
  ↓ 查看 Span 瀑布图
发现 payment-service HTTP 调用耗时 2.1s
  ↓ 使用 traceId 搜索日志
定位到下游连接池等待和超时重试

Spring Boot 3 使用 Micrometer Observation 统一表达一次“被观测的操作”。一个 Observation 可以同时产生指标和 Trace Span;Micrometer Tracing 再把追踪数据桥接到 OpenTelemetry。


二、组件之间是什么关系

Spring MVC / RestClient / DataSource / 自定义业务代码
                       ↓
             Micrometer Observation
                 ↙             ↘
          Micrometer Metrics   Micrometer Tracing
                                      ↓
                         OpenTelemetry Bridge
                                      ↓ OTLP
                           OpenTelemetry Collector
                                      ↓
                         Tempo / Jaeger / 其他后端

应用代码优先使用 Micrometer 的 ObservationRegistryTracer,不要同时手工维护另一套 OpenTelemetry SDK。Spring Boot 会负责自动配置和生命周期管理,OTLP 只是导出协议。


三、添加依赖

<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-tracing-bridge-otel</artifactId>
    </dependency>
    <dependency>
        <groupId>io.opentelemetry</groupId>
        <artifactId>opentelemetry-exporter-otlp</artifactId>
    </dependency>

    <dependency>
        <groupId>io.micrometer</groupId>
        <artifactId>micrometer-registry-prometheus</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>

不要在使用 Spring Boot BOM 时手工拼凑这些依赖的版本。版本不一致很容易引发自动配置缺失、方法找不到或上下文传播失败。

如果只需要 Trace 导出,可以去掉 Prometheus Registry;它在本文中用于演示指标端点。


四、配置 OTLP、采样和 Actuator

spring:
  application:
    name: order-service

management:
  endpoints:
    web:
      exposure:
        include: health,info,prometheus
  endpoint:
    health:
      probes:
        enabled: true

  tracing:
    sampling:
      probability: 0.1
    propagation:
      type: w3c
    baggage:
      remote-fields: tenant-id
      correlation:
        fields: tenant-id

  otlp:
    tracing:
      endpoint: http://otel-collector:4318/v1/traces
      timeout: 10s

  observations:
    key-values:
      region: cn-east-1
      stack: prod

关键点:

  • spring.application.name 会成为服务识别的重要维度;
  • probability: 0.1 表示约 10% 的请求创建可导出的 Trace;
  • w3c 使用 traceparenttracestatebaggage 标准 Header;
  • management.otlp.tracing.endpoint 指向 Collector 的 HTTP Trace 接收端点;
  • regionstack 这类低基数标签适合作为公共维度。

开发环境可以暂时把采样率设为 1.0,生产环境不要默认全量采样。采样率应根据吞吐、故障定位要求、存储成本和后端限额决定。

Actuator 端点也不应全部公开。envconfigpropsheapdump 等端点可能泄露配置或占用大量资源,必须通过独立管理端口、网络策略和鉴权保护。


五、让日志自动带上 TraceId 和 SpanId

Spring Boot 在有效追踪上下文中会把 traceIdspanId 放入 MDC。可以自定义控制台日志格式:

logging:
  pattern:
    console: >-
      %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level
      [${spring.application.name:unknown},trace=%X{traceId:-},span=%X{spanId:-}]
      [%thread] %logger{36} - %msg%n

示例输出:

2026-07-05 20:16:32.117 INFO
[order-service,trace=7a9f31d8a2f0d9138e1eae74d9c74421,span=0a4fd37b9953a12c]
[http-nio-8080-exec-4] c.e.order.OrderController - create order, skuId=10001

TraceId 是跨系统关联键,SpanId 是当前操作的局部标识。不要自己为每个服务生成新的 TraceId,否则链路会在服务边界断开。

结构化日志系统中,应把 traceIdspanId 保存为独立字段,而不是只嵌入 message,后续检索和聚合会更可靠。


六、跨服务调用必须使用自动配置的 Builder

Spring Boot 会给 RestClient.BuilderWebClient.BuilderRestTemplateBuilder 安装观测拦截器。必须注入这些 Builder 创建客户端:

package com.example.order.client;

import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;

@Component
public class InventoryClient {

    private final RestClient restClient;

    public InventoryClient(RestClient.Builder builder) {
        this.restClient = builder
                .baseUrl("http://inventory-service:8080")
                .build();
    }

    public InventoryView findBySku(long skuId) {
        return restClient.get()
                .uri("/api/inventory/{skuId}", skuId)
                .retrieve()
                .body(InventoryView.class);
    }
}

不要这样做:

RestClient restClient = RestClient.create();

直接创建的客户端没有 Spring Boot 定制器,通常不会自动注入 traceparent,于是上游和下游会显示成两条不相关的 Trace。

还要保证代理、网关和服务网格允许追踪 Header 通过。Header 在应用内正确生成,并不代表一定能穿过所有中间层。


七、为关键业务阶段创建自定义 Observation

框架能自动观测 HTTP 请求,但它不知道“库存预占”和“价格计算”哪个阶段更关键。可以围绕业务操作创建 Observation:

package com.example.order.application;

import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
import org.springframework.stereotype.Service;

@Service
public class OrderApplicationService {

    private final ObservationRegistry observationRegistry;
    private final InventoryClient inventoryClient;

    public OrderApplicationService(
            ObservationRegistry observationRegistry,
            InventoryClient inventoryClient) {
        this.observationRegistry = observationRegistry;
        this.inventoryClient = inventoryClient;
    }

    public OrderView create(CreateOrderCommand command) {
        return Observation.createNotStarted(
                        "order.create", observationRegistry)
                .contextualName("create order")
                .lowCardinalityKeyValue("channel", command.channel())
                .highCardinalityKeyValue(
                        "customer.id", command.customerId())
                .observe(() -> doCreate(command));
    }

    private OrderView doCreate(CreateOrderCommand command) {
        inventoryClient.findBySku(command.skuId());
        // 校验价格、保存订单、发送领域事件
        return new OrderView(1001L, "CREATED");
    }
}

低基数与高基数必须分清:

  • 低基数:取值集合有限,如 channel=APP|WEBresult=success|failure
  • 高基数:取值几乎无限,如用户 ID、订单号、请求 ID。

低基数键会进入指标和 Trace,高基数键只应进入 Trace。把订单号放进 Metrics 标签会制造海量时间序列,显著增加 Prometheus 和后端存储压力。

同一个控制器或 Repository 已经被框架自动观测时,不要再无差别加一层同名 Observation,否则会产生重复 Span。自定义 Observation 应表达框架不知道的业务阶段。


八、什么时候使用注解

如果团队偏好注解,可以启用 Micrometer Observation 注解扫描:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
management:
  observations:
    annotations:
      enabled: true

然后使用 @Observed@NewSpan

import io.micrometer.observation.annotation.Observed;

@Observed(name = "price.calculate", contextualName = "calculate price")
public Money calculatePrice(PricingCommand command) {
    return pricingEngine.calculate(command);
}

注解适合稳定的方法边界;编程式 Observation 更适合动态标签、只观测某个代码片段或精确控制错误记录。两种方式不要在同一边界重复使用。


九、Baggage:少量传播业务上下文

Baggage 可以把租户号等少量字段随 Trace 传播:

management:
  tracing:
    baggage:
      remote-fields: tenant-id
      correlation:
        fields: tenant-id

remote-fields 允许字段跨网络传播,correlation.fields 把字段放入 MDC,方便日志检索。

但 Baggage 会进入请求 Header 并跨多个服务复制,应遵循三个限制:

  1. 不放密码、Token、手机号等敏感信息;
  2. 不放大对象或数量不受控的键值;
  3. 只传播确实用于诊断或路由的字段。

Baggage 不是业务参数传输协议。下游业务逻辑需要的关键数据仍应在 API 契约中明确声明。


十、异步线程为什么容易丢 Trace

追踪上下文通常绑定在当前执行上下文。把任务提交给自建线程池后,如果没有上下文传播,日志里的 TraceId 会消失。

Spring Framework 提供了上下文传播 TaskDecorator:

package com.example.order.config;

import java.util.concurrent.Executor;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.support.ContextPropagatingTaskDecorator;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

@Configuration
@EnableAsync
public class AsyncConfig {

    @Bean(name = "applicationTaskExecutor")
    Executor applicationTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(8);
        executor.setMaxPoolSize(32);
        executor.setQueueCapacity(500);
        executor.setThreadNamePrefix("order-async-");
        executor.setTaskDecorator(new ContextPropagatingTaskDecorator());
        executor.initialize();
        return executor;
    }
}

使用 CompletableFuture.supplyAsync(task) 时会进入公共线程池,也可能丢失上下文。应显式传入由应用管理、已配置上下文传播的 Executor。

异步消息还需要在生产端注入上下文、消费端提取上下文。若使用的消息组件没有自动埋点,需要在消息 Header 与 Observation 之间建立清晰的传播约定。


十一、采样不是简单地“保留 10% 日志”

management.tracing.sampling.probability 是头部采样:请求进入系统时决定是否采样。它实现简单、开销可控,但随机 10% 可能刚好丢掉稀有错误。

实践中可分三层:

  • 开发环境:短期 100%,方便验证链路;
  • 普通生产流量:按成本设置 1%~20% 的头部采样;
  • 关键错误或高延迟:在 Collector 侧评估尾部采样,等待 Trace 完整后再决定保留。

尾部采样需要 Collector 暂存更多数据,资源成本和配置复杂度更高。不要在没有容量评估时直接开启全量 Trace。

另外,采样只影响 Trace 是否导出,不应影响业务日志和核心指标的正确性。


十二、验证链路是否真的打通

按以下顺序验证,比直接打开 Trace UI 更快:

  1. 请求服务后,日志是否出现非空 traceIdspanId
  2. 下游服务日志是否出现相同 TraceId;
  3. 抓取或临时记录请求 Header,确认存在 traceparent
  4. Collector 是否监听正确协议和端口;
  5. 应用到 Collector 的网络是否畅通;
  6. Collector 是否成功向后端导出;
  7. Trace 后端的时间范围、服务名筛选是否正确。

测试请求:

curl -i http://localhost:8080/api/orders/1001

指标端点:

curl http://localhost:8080/actuator/prometheus

如果应用日志有 TraceId,但后端没有 Trace,问题通常在采样、Exporter、Collector 或网络;如果第一个服务有 TraceId、下游换了新的 TraceId,问题通常在客户端创建方式或 Header 传播。


十三、常见问题排查表

现象 原因与处理
日志中 TraceId 始终为空 检查 Actuator、Tracing Bridge 是否存在,请求是否经过被观测的入口
下游生成新的 TraceId 使用自动配置的 HTTP Client Builder,检查网关是否保留 traceparent
Collector 收不到数据 检查 OTLP HTTP/gRPC 协议、端口、路径和容器网络
Trace 数量远低于请求量 检查采样率;这不一定是故障
指标时间序列暴涨 高基数字段被错误放入低基数标签
@Async 中 TraceId 消失 为线程池配置上下文传播 TaskDecorator
一个请求出现重复 Span 同一框架边界同时存在自动埋点和自定义注解/Observation
Trace 后端能看到链路但日志搜不到 日志格式未输出 MDC 字段,或日志采集器没有解析独立字段

十四、生产治理清单

  • 为每个服务设置稳定且唯一的 spring.application.name
  • 使用 W3C Trace Context 作为跨语言默认传播格式;
  • HTTP 客户端统一由 Spring Bean 管理,禁止业务代码随意 create()
  • 只对关键业务阶段添加自定义 Observation;
  • Metrics 标签严格限制为低基数,用户 ID、订单号只进入 Trace 或日志;
  • 生产采样率由吞吐、存储成本和排障目标共同决定;
  • Collector 部署高可用,并监控自身队列、丢弃量和导出失败;
  • TraceId、SpanId 进入结构化日志字段;
  • Baggage 不携带凭证和个人敏感信息;
  • Actuator 端点实施最小暴露、鉴权和网络隔离;
  • 对跨服务、异步线程和消息消费分别做链路连续性测试。

总结

Spring Boot 3 的可观测性主线并不复杂:框架和业务代码通过 Micrometer Observation 产生观测数据,Micrometer Tracing 建立追踪语义,OpenTelemetry Bridge 与 OTLP 负责把 Trace 发送给 Collector。

真正决定方案质量的是工程约束:统一创建 HTTP 客户端、控制标签基数、正确传播异步上下文、合理采样,并把 TraceId 与结构化日志打通。完成这些工作后,排障过程才能从“逐台机器猜问题”变成“从指标定位异常,再沿 Trace 和日志找到根因”。

参考资料

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐