Skip to content

Spring Boot 3可观测性内部原理与生产治理 ​

Spring Boot 3中的可观测性不是“过滤器生成一个UUID,再把它打印进日志”。一次完整观测要经历Observation创建、Context与Convention补充语义、ObservationHandler生成Metric和Span、Receiver提取远程上下文、Sender向下游注入、OpenTelemetry SDK采样与批处理、OTLP Exporter发送、Collector加工路由以及后端存储查询。任何一层失败都可能出现“业务发生了,但Trace平台没有数据”。

本章以可复核依赖为样本:

技术线Java与Spring追踪实现样本用途
极老存量线JDK 7、Boot 1.x或非Boot应用依项目锁定的旧追踪库或手工关联只做隔离维护与升级取证,不能复制Boot 2/3 API
存量线JDK 8、Boot 2.7.18、Cloud 2021.0.8Sleuth 3.1.9、Brave 5.13.9、Zipkin Reporter 2.16.3维护和迁移老系统
现代线Java 17、Boot 3.2.4Observation 1.12.4、Tracing 1.2.4、OTel SDK 1.31.0本章自动配置源码与Demo基线

JDK 7不能运行Boot 2.7或Boot 3。若业务仍停留在JDK 7,应先按真实依赖锁定可用API,通过HTTP Header和日志关联理解传播原理,同时制定运行时升级与服务隔离计划;不能为了让示例“看起来统一”而把Sleuth 3或Micrometer Tracing代码放进JDK 7项目。

后续Boot 3.x会升级Micrometer和OpenTelemetry版本,属性、默认值和语义约定可能演进。实际项目必须以依赖树、配置元数据和目标版本Release Notes为准;不能把Boot 3.2.4样本当成所有未来3.x版本的固定实现。

一、学习目标 ​

学完后应能独立回答:

  1. Observation、Metric、Span、Trace、Log和OpenTelemetry各自是什么。
  2. Boot如何创建ObservationRegistry并把Handler、Predicate、Convention和Filter注册进去。
  3. 一次Observation的start、openScope、error、closeScope和stop怎样通知Handler。
  4. 为什么一个Observation可以同时产生Metric和Span。
  5. Receiver Handler怎样提取traceparent,Sender Handler怎样注入下游Carrier。
  6. Micrometer Tracing怎样桥接OpenTelemetry Tracer、Propagator和MDC。
  7. Web入口、HTTP客户端、OpenFeign、Gateway、线程池和MQ在哪一步最容易断链。
  8. Head Sampling、Parent-based Sampling与Collector Tail Sampling为什么不是一回事。
  9. BatchSpanProcessor队列满、Exporter失败和Collector背压时数据怎样丢失。
  10. Sleuth存量系统怎样迁移到Boot 3,并兼容B3与W3C传播。
  11. 如何设计低基数Metric、可检索Trace、脱敏日志和Exemplar关联。
  12. 如何排查无Trace、断链、MDC错乱、关键错误未采样和观测系统拖慢业务。

二、先建立完整数据链 ​

mermaid
flowchart TD
    A["业务框架创建Observation"] --> B["Handler生成Metric与Span"]
    B --> C["OTel SDK采样并批处理"]
    C --> D["OTLP Exporter发送"]
    D --> E["Collector接收与加工"]
    E --> F["Trace、Metric、Log后端"]

这条链中有三个不同问题:

问题负责回答的层
业务代码是否真的执行和提交业务日志、数据库事实、审计
是否创建并传播了正确上下文Observation、Tracing、Propagator
遥测是否最终存储可查询SDK、Exporter、Collector、Backend

Trace平台没有数据只证明“查询不到遥测”,不能反推业务请求没有发生。观测链本身是异步、有损、受采样和容量限制的数据系统。

三、核心对象职责地图 ​

对象作用常见误解
ObservationRegistry保存当前Observation和全局配置不是Trace数据库
Observation表达一个被观测操作的生命周期不一定总会生成可导出的Span
Observation.Context保存名称、错误、KeyValues和传输Carrier不是线程池自动传播器
ObservationConvention统一名称和标签语义不应为每个订单生成Metric名称
ObservationHandler在生命周期事件上生成Metric、Span等副作用Handler顺序和supportsContext会影响结果
TracerMicrometer Tracing门面不等于具体OTel SDK或Brave实现
Propagator从Carrier提取或向Carrier注入上下文不负责用户认证和Header可信度
OtelTracerMicrometer Tracer到OTel API的桥不是Exporter
SdkTracerProviderOTel Span创建、采样和Processor入口不负责最终存储
BatchSpanProcessor有界排队、批量交给Exporter队列满会丢Span,不是持久队列
SpanExporter把SpanData写向OTLP等出口Export成功不等于后端已永久可查
Collector接收、加工、采样、缓冲和路由不在核心业务请求路径中更安全

四、Boot 3启动时自动配置了什么 ​

现代样本中的主链:

mermaid
flowchart TD
    A["Actuator与Tracing依赖满足条件"] --> B["创建ObservationRegistry"]
    B --> C["配置Predicate、Convention、Filter与Handler"]
    C --> D["创建OTel SDK与Micrometer桥"]
    D --> E["注册Tracing与Meter Handler"]
    E --> F["Web与客户端组件接入Registry"]

4.1 ObservationAutoConfiguration ​

Boot 3.2.4样本在类路径存在ObservationRegistry时创建默认Registry。ObservationRegistryPostProcessor再收集并应用:

  • ObservationRegistryCustomizer。
  • ObservationPredicate。
  • GlobalObservationConvention。
  • ObservationHandler。
  • ObservationFilter。

这说明只声明一个自定义Handler Bean还不够理解全局结果:Predicate可能先禁止某类Observation,Convention可能修改名称和标签,Filter可能在stop前重写Context,Handler分组还会决定哪个Handler处理哪类Context。

4.2 Metrics与Tracing怎样同时接入 ​

当MeterRegistry和Tracer都存在时,Boot把Tracing Handler和Meter Handler分组注册,并创建TracingAwareMeterObservationHandler。因此同一个HTTP Observation可以:

  • 生成请求计数和耗时Metric。
  • 创建Server或Client Span。
  • 将Trace上下文关联到日志。
  • 在支持的Meter实现中产生Exemplar关联。

这就是Observation比“手写Span”更适合Spring框架统一埋点的原因:框架只描述一次操作,多个Handler分别输出不同信号。

4.3 OpenTelemetryAutoConfiguration ​

当OTel桥、SdkTracerProvider和OpenTelemetry API都存在时,Boot样本会组装:

  1. Sampler。
  2. SdkTracerProvider。
  3. SpanProcessors。
  4. BatchSpanProcessor。
  5. SpanExporters。
  6. OTel API Tracer。
  7. Micrometer OtelTracer。
  8. OtelPropagator。
  9. OtelCurrentTraceContext。
  10. Slf4JEventListener。

默认采样器是parentBased(traceIdRatioBased(probability))。Boot 3.2.4的management.tracing.sampling.probability默认值是0.10,即根Trace约10%头部采样;子Span优先遵循父采样决定。

4.4 MicrometerTracingAutoConfiguration ​

在Micrometer Tracer Bean存在时创建三类核心Handler:

Handler处理什么
DefaultTracingObservationHandler普通进程内Observation,创建子Span或根Span
PropagatingReceiverTracingObservationHandlerHTTP/MQ接收端,从Carrier提取父上下文并创建Receiver Span
PropagatingSenderTracingObservationHandlerHTTP/MQ发送端,创建Sender Span并注入Carrier

Handler有顺序。启动按注册顺序通知,停止和Scope关闭通常按逆序回调,以正确恢复嵌套资源和当前上下文。

五、一次Observation生命周期 ​

mermaid
flowchart TD
    A["createNotStarted"] --> B["start通知onStart"]
    B --> C["openScope设为当前Observation"]
    C --> D["业务执行并添加Event或Error"]
    D --> E["Scope关闭并恢复上一个上下文"]
    E --> F["stop通知onStop并导出语义"]

SimpleObservation样本中的关键顺序:

  1. start()先应用Convention提供的低/高基数KeyValues和名称,再按顺序调用Handler onStart。
  2. openScope()把当前Observation放入Registry,并通知Scope打开。
  3. error(Throwable)把错误保存到Context并通知onError。
  4. stop()再次应用Convention,执行ObservationFilter,再逆序通知Handler onStop。
  5. Scope关闭时也逆序通知Handler并恢复上一个Observation。

5.1 Scope关闭和Observation停止不是一回事 ​

  • Scope决定当前线程里“谁是当前Observation/Span”。
  • stop决定这个操作完成并可以计算耗时、记录最终标签和结束Span。

忘记关闭Scope会污染线程复用后的上下文;忘记stop会产生永不完成或无法导出的Span。推荐使用observe、scoped或try-with-resources模式,减少异常路径遗漏。

5.2 Error和Status为什么必须显式记录 ​

捕获异常后返回降级结果,如果不调用observation.error(error)或给当前Span标记错误,Trace可能显示“成功”。但也不能把所有业务拒绝都标成系统错误:库存不足是业务结果,数据库连接失败才是基础设施错误。错误语义要与SLO一致。

六、低基数和高基数KeyValue ​

Micrometer Observation明确区分:

类型适合内容主要去向
低基数operation、规范化route、status、region、versionMetric标签,也可进入Trace
高基数orderId、requestId、具体URL参数、错误实例Trace属性或受控日志,不应成为Metric标签

错误示例:

java
observation.lowCardinalityKeyValue("order.id", orderId);

一百万订单会制造大量时间序列组合。正确做法:

java
observation.lowCardinalityKeyValue("operation", "reserve");
observation.highCardinalityKeyValue("order.id", orderId);

高基数也不是无限免费:Trace存储、索引和隐私成本同样存在。订单号应按查询需求、保留期和访问权限治理,身份证、Token、密码和完整医疗数据不应直接写入属性。

七、HTTP入口怎样建立Server Span ​

Spring MVC样本通过ServerHttpObservationFilter接入ObservationRegistry。概念链:

mermaid
flowchart TD
    A["HTTP请求进入Servlet容器"] --> B["创建ReceiverContext"]
    B --> C["Propagator提取traceparent或B3"]
    C --> D["创建SERVER Span并打开Scope"]
    D --> E["Controller与Service执行"]
    E --> F["记录route、status、error并结束"]

PropagatingReceiverTracingObservationHandler.onStart从Carrier提取远程上下文,设置Span Kind和远端信息后启动Span。没有合法父上下文时创建新的根Trace;有父上下文时创建子Server Span。

7.1 为什么不能只读取X-Trace-Id ​

自定义Header只提供一个字符串,通常缺少:

  • Parent Span ID。
  • 采样标志。
  • 传播格式版本。
  • tracestate。
  • 与SDK当前上下文的桥接。
  • Sender与Receiver Span关系。

它可以作为老系统兼容字段,但不应冒充完整W3C Trace Context。现代系统优先使用标准Propagator,并在迁移期受控同时消费B3和W3C。

7.2 外部Trace ID不是认证凭据 ​

调用方可以伪造traceparent。系统可以用它关联遥测,但不能据此信任用户、租户或权限。认证Header、mTLS身份和业务授权必须独立校验;日志系统也要防止攻击者用恶意Trace ID制造查询污染。

八、HTTP客户端怎样创建Client Span并注入 ​

mermaid
flowchart TD
    A["业务调用HTTP客户端"] --> B["创建SenderContext与CLIENT Span"]
    B --> C["从当前Span建立父子关系"]
    C --> D["Propagator注入traceparent"]
    D --> E["底层连接池发送请求"]
    E --> F["响应或异常结束Client Span"]

PropagatingSenderTracingObservationHandler创建Sender Span后,通过Context提供的Setter把传播字段写入HTTP Header或消息Carrier。

Boot对RestTemplateBuilder、RestClient.Builder和WebClient.Builder提供Observation定制。关键陷阱是:

  • 使用Spring注入的Builder创建客户端,定制器才能参与。
  • 直接new RestTemplate()可能绕过Boot注册的Observation拦截器。
  • 自建底层HTTP Client还要确认连接池、重试和观测拦截顺序。
  • Feign是否生成Observation取决于相应Micrometer Capability和依赖条件。

Trace只观察调用,不负责HTTP重试语义。一次逻辑业务调用若有三次物理HTTP Attempt,必须明确是一个Span带事件、三个子Span,还是由底层库另行记录;否则平台耗时和实际下游负载会对不上。

九、W3C、B3与传播迁移 ​

标准W3C traceparent格式:

text
00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01

四部分依次是:version、32位十六进制trace-id、16位parent-id、trace-flags。trace-id和parent-id不能全0。

9.1 B3存量线 ​

Sleuth/Zipkin老系统常见:

  • 单Header b3。
  • 多Header X-B3-TraceId、X-B3-SpanId、X-B3-Sampled。

迁移不能一天内把所有服务从B3切成W3C,否则新旧服务会各自创建根Trace。更稳妥的过程:

  1. 清点入口、HTTP、MQ和第三方传播格式。
  2. 现代服务先同时消费B3与W3C。
  3. 选择唯一主生产格式。
  4. 灰度观察断链率和Trace树形。
  5. 所有消费者兼容后再停止旧格式生产。

9.2 多格式为什么也有代价 ​

同时收到W3C和B3且两者Trace ID不同,必须定义优先级,不能随机选择。Header增大、代理白名单、跨域策略和安全审计也要同步更新。

十、Trace怎样进入日志MDC ​

Boot样本的LogCorrelationEnvironmentPostProcessor在类路径存在Micrometer Tracer时,让日志系统期待关联ID。OTel桥中的Slf4JEventListener监听Context Scope事件:

  • Scope attached:把traceId、spanId放入MDC。
  • Scope restored:恢复上一个Span的ID。
  • Scope closed:移除MDC字段。
mermaid
flowchart TD
    A["Span Scope进入"] --> B["MDC写traceId与spanId"]
    B --> C["业务日志自动带关联字段"]
    C --> D["Scope退出"]
    D --> E["MDC恢复或清理"]

10.1 为什么MDC会错乱 ​

  • 手写MDC.put后异常路径没remove。
  • 线程池任务没有捕获和恢复Context。
  • 异步回调在另一个线程执行。
  • 同时运行手写Filter与框架Tracing,双方覆盖字段。
  • 日志在Scope打开前或关闭后打印。

不要再叠加一套全局自定义TraceFilter去无条件覆盖traceId。迁移期若必须保留X-Trace-Id,应作为兼容属性,而不是替换当前Span上下文。

十一、手工Observation与@Observed ​

框架已自动覆盖HTTP入口和客户端时,不要为每层Controller、Service、Repository重复创建Span。手工Observation适合有独立业务意义和耗时边界的操作,例如“库存预留计算”或“生成对账批次”。

java
return Observation.createNotStarted("inventory.reserve", observationRegistry)
        .contextualName("reserve inventory")
        .lowCardinalityKeyValue("operation", "reserve")
        .highCardinalityKeyValue("sku.id", skuId)
        .observe(() -> inventoryService.reserve(skuId, quantity));

name应稳定、低基数,便于Metric聚合;contextualName可更贴近Trace阅读。不要把订单号拼进Observation名称。

@Observed依赖AOP和ObservedAspect。Boot 3.2.4源码还显示Tracing注解支持受management.observations.annotations.enabled等条件控制。使用注解前要验证:

  • AOP依赖是否存在。
  • 方法是否通过Spring代理调用。
  • 是否为self-invocation。
  • 属性是否在当前Boot版本启用。
  • 异常是否被代理正确捕获。

显式Observation API更容易看清作用域和异常路径;注解适合稳定、重复的边界。

十二、线程池和CompletableFuture为什么断链 ​

当前Observation、OTel Context和MDC通常依赖线程上下文。任务提交到另一个线程后,ThreadLocal不会天然复制。

正确模型:

mermaid
flowchart TD
    A["提交线程捕获ContextSnapshot"] --> B["任务进入有界线程池"]
    B --> C["执行线程恢复上下文"]
    C --> D["执行任务并产生子Observation"]
    D --> E["finally关闭Scope并恢复旧上下文"]

Micrometer Context Propagation提供:

  • ContextRegistry登记ThreadLocalAccessor。
  • ContextSnapshotFactory.captureAll()捕获已登记上下文。
  • ContextSnapshot.wrap(Runnable/Callable)包装任务。
  • ContextExecutorService包装ExecutorService。
  • Slf4jThreadLocalAccessor传播选定MDC字段。

示意:

java
ContextSnapshot snapshot = ContextSnapshotFactory.builder().build().captureAll();
executor.execute(snapshot.wrap(() -> {
    Observation.createNotStarted("inventory.async-check", registry)
            .observe(this::checkInventory);
}));

不要在任务执行时才捕获Context,那时已经切到线程池旧线程,可能捕获到空值或上一个任务残留值。必须“提交时捕获、执行时恢复、finally清理”。

12.1 CompletableFuture常见坑 ​

CompletableFuture.supplyAsync不传Executor时使用公共池,既缺少业务隔离,也不保证观测上下文传播。生产应使用明确有界Executor并进行Context包装。只传播traceId字符串仍无法恢复当前Span、采样决定和Baggage。

12.2 Reactor与WebFlux ​

Reactor跨线程依赖Reactor Context而不是普通ThreadLocal。Micrometer Context Propagation可在两者间桥接,但自动传播模式、操作符边界和版本配置必须按当前Spring/Reactor版本验证。不要在WebFlux链中用subscribe()脱离原链后假设上下文仍然存在。

十三、MQ消息怎样传播Trace ​

生产端Sender Observation将Trace Context注入消息Header,消费端Receiver Observation提取。是否建立父子关系还是Span Link取决于消费语义:

场景推荐关系
单消息立即异步处理可建立Producer到Consumer的因果父子
批量消费多个消息Consumer Span使用多个Link更准确
消息长期延迟或重试Link常比拉长单一父子时间轴更清晰
聚合多个来源后输出用Link表达多来源因果

重试消息要保留原业务event_id,但Trace如何处理需统一:可以保留原Trace并新增Attempt Span,也可以新Trace加Link。不能每次重试既创建新Trace又完全丢失原关联。

消息Header大小有限,Baggage传播必须白名单;不要把订单对象、Token和敏感数据塞进Baggage。

十四、Boot头部采样与Collector尾部采样 ​

14.1 Boot 3.2.4样本默认头采样 ​

yaml
management:
  tracing:
    sampling:
      probability: 0.10

根Trace按trace-id比率决定,子Span遵循父采样决定。优点是应用开销可预测;缺点是入口时不知道最终是否500或超时,关键错误可能正好未采样。

14.2 为什么“错误强制采样”不能靠请求结束时改Head Sampling ​

Head Sampling在Trace开始时已决定。请求结束看到异常时,前面的Span可能没有记录完整数据。要保留错误和慢Trace通常使用:

  • Collector Tail Sampling。
  • 全量或较高采样进入边缘Collector后再筛选。
  • 对关键入口采用更高概率或规则采样。
  • Metrics和业务错误日志保持全量聚合。

14.3 Tail Sampling代价 ​

Collector必须在decision_wait窗口内缓存同一Trace的Span,并尽量让同Trace路由到同一个Tail Sampling实例。代价包括内存、等待延迟、负载路由和不完整Trace风险。不能在每个Collector副本独立随机看到半条Trace后期待正确尾采样。

采样不等于指标采样。HTTP请求计数、错误率和延迟Histogram应保持聚合完整,Trace负责抽样解释具体请求。

十五、BatchSpanProcessor为什么会丢数据 ​

OTel SDK 1.31.0样本默认值:

参数样本默认值
schedule delay5000ms
max queue size2048 spans
max export batch size512 spans
exporter timeout30000ms

流程:

mermaid
flowchart TD
    A["结束且采样的Span"] --> B["进入有界队列"]
    B --> C["达到批量或时间条件"]
    C --> D["Exporter异步发送"]
    D --> E["成功移交或记录失败"]

队列满时Span会被丢弃,而不是阻塞核心业务无限等待。这是正确的故障隔离方向,但必须监控Dropped、队列使用率和Export失败,否则团队会误以为“应用没有请求”。

15.1 应用关闭为什么可能丢最后一批Span ​

Batch Processor需要forceFlush/shutdown。容器强杀、极短terminationGracePeriod或进程崩溃会丢内存队列。优雅停机应给SDK和Exporter有限刷新时间,但不能为了遥测无限阻塞业务下线。

15.2 队列是不是越大越好 ​

不是。队列过大增加堆内存、GC和事故期间的数据陈旧;Collector长期不可用时,再大最终也会满。合理做法是有界容量、异步失败、告警、容量计算和Collector高可用。

十六、Boot 3.2.4的OTLP导出链 ​

现代样本配置:

yaml
management:
  tracing:
    enabled: true
    sampling:
      probability: 0.10
  otlp:
    tracing:
      endpoint: http://otel-collector:4318/v1/traces
      timeout: 10s
      compression: gzip

Boot 3.2.4的OtlpAutoConfiguration样本自动配置的是OTLP HTTP/protobuf Exporter;只有配置endpoint或提供ConnectionDetails时创建。若应用自定义OtlpGrpcSpanExporter,自动配置会后退。不要把HTTP端口4318和gRPC端口4317混用。

链路:

mermaid
flowchart TD
    A["BatchSpanProcessor形成批次"] --> B["OtlpHttpSpanExporter"]
    B --> C["HTTP protobuf请求到4318"]
    C --> D["Collector OTLP Receiver"]
    D --> E["Processor与Backend Exporter"]

Exporter的10秒timeout覆盖DNS、连接、写入、Collector处理和读取响应。SDK Exporter重试与Collector队列重试要分层观察,避免观测故障产生大量后台重试和日志风暴。

十七、Collector生产Pipeline ​

示例需要按目标Collector版本校验:

yaml
receivers:
  otlp:
    protocols:
      grpc:
      http:

processors:
  memory_limiter:
    check_interval: 1s
    limit_mib: 512
  batch:
    timeout: 2s
    send_batch_size: 512
  tail_sampling:
    decision_wait: 10s
    policies:
      - name: keep-errors
        type: status_code
        status_code:
          status_codes: [ERROR]
      - name: keep-slow
        type: latency
        latency:
          threshold_ms: 1000

exporters:
  otlp:
    endpoint: tempo:4317
    tls:
      insecure: true

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, tail_sampling, batch]
      exporters: [otlp]

生产不能照搬insecure: true跨不可信网络,应配置TLS、认证和最小网络访问。Processor顺序会改变行为:内存限制通常应尽早保护Collector;Tail Sampling需要同Trace聚合;Batch在合适位置减少导出请求。

17.1 Agent与Gateway模式 ​

模式优点风险
每节点/Pod Agent本地接收、降低应用网络依赖节点资源和配置数量增加
集中Gateway统一采样、路由、认证容量和高可用要求高
Agent + Gateway本地削峰加集中治理两层队列、重试和观测更复杂

Collector不应成为核心交易的同步强依赖。应用Exporter应异步有界,Collector故障时宁可丢部分遥测并告警,也不能把支付线程永久阻塞。

十八、Metrics、Trace、Logs怎样闭环 ​

商业排查顺序:

  1. Metric通过规范化route、status、version发现影响范围。
  2. Histogram或Exemplar跳到代表性Trace。
  3. Trace找到慢Span和具体实例。
  4. 用traceId查询结构化日志。
  5. 用request_id或订单号查询业务事实。

Metric标签不能放traceId;Trace属性也不能替代数据库审计;日志没有记录不能证明操作没发生。三个信号分别解决聚合、因果和细节问题。

建议日志字段:

json
{
  "timestamp": "2026-07-19T10:00:00.123+08:00",
  "level": "ERROR",
  "service": "order-service",
  "traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
  "spanId": "00f067aa0ba902b7",
  "operation": "reserve-inventory",
  "errorCode": "INVENTORY_TIMEOUT"
}

订单号和用户标识按安全策略脱敏或哈希,禁止输出Token、密码、私钥、身份证和完整医疗数据。

十九、Sleuth存量线怎样迁移 ​

JDK 8、Boot 2.7.18、Cloud 2021.0.8样本解析到:

  • Spring Cloud Sleuth 3.1.9。
  • Brave 5.13.9。
  • Zipkin Reporter 2.16.3。

现代Boot 3主线不再继续使用Sleuth Starter,而是Micrometer Observation/Tracing加Brave或OTel Bridge。

存量Sleuth概念Boot 3方向
spring.sleuth.sampler.probabilitymanagement.tracing.sampling.probability
Sleuth自动埋点Framework/Boot Observation
Brave Tracer APIMicrometer Tracer门面,可桥接Brave或OTel
Zipkin ReporterZipkin或OTLP SpanExporter
B3默认传播明确配置W3C/B3生产与消费策略
Sleuth MDCMicrometer桥的Scope/MDC关联

迁移步骤:

  1. 记录旧服务名、Span名、采样率、B3格式、Baggage、MDC和Zipkin后端。
  2. 清点自定义Sleuth Bean、Sampler、Filter和Feign拦截器。
  3. 在契约测试中验证入口、Feign、线程池、MQ和Gateway传播。
  4. 新服务先兼容消费旧B3。
  5. 逐步切换主生产格式和Exporter。
  6. 对比Trace完整率、Span数量、Metric标签和日志关联。
  7. 最后删除手写X-Trace-Id覆盖逻辑和旧Sleuth依赖。

不能只把依赖坐标替换掉:Span命名、采样默认值、Header、MDC、Baggage和Exporter都会影响平台查询与告警。

二十、Java 17与Boot 3可运行Demo ​

20.1 Maven依赖 ​

xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.2.4</version>
    <relativePath/>
</parent>

<groupId>com.example</groupId>
<artifactId>inventory-observability-demo</artifactId>
<version>1.0.0</version>

<properties>
    <java.version>17</java.version>
</properties>

<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>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>
</project>

版本由Boot BOM管理,本样本解析为Observation 1.12.4、Tracing 1.2.4和OTel SDK 1.31.0。不要给每个依赖随意写另一个版本。

20.2 启动类 ​

java
package com.example.inventory;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class InventoryApplication {
    public static void main(String[] args) {
        SpringApplication.run(InventoryApplication.class, args);
    }
}

20.3 商业库存Observation ​

java
package com.example.inventory;

import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/inventory")
public class InventoryObservationController {
    private static final Logger log =
            LoggerFactory.getLogger(InventoryObservationController.class);

    private final ObservationRegistry registry;

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

    @PostMapping("/{skuId}/reserve")
    public Map<String, Object> reserve(@PathVariable String skuId,
                                       @RequestParam int quantity) {
        return Observation.createNotStarted("inventory.reserve", registry)
                .contextualName("reserve inventory")
                .lowCardinalityKeyValue("operation", "reserve")
                .lowCardinalityKeyValue("result", "accepted")
                .highCardinalityKeyValue("sku.id", skuId)
                .observe(() -> {
                    log.info("Reserve inventory: skuId={}, quantity={}", skuId, quantity);
                    return Map.of(
                            "skuId", skuId,
                            "quantity", quantity,
                            "status", "ACCEPTED");
                });
    }
}

sku.id故意放高基数,避免进入请求Metric标签;生产是否写入Trace还要评估商品数量、隐私和索引成本。

20.4 配置 ​

yaml
spring:
  application:
    name: inventory-service

management:
  tracing:
    enabled: true
    sampling:
      probability: 1.0
    propagation:
      type: w3c
  otlp:
    tracing:
      endpoint: http://localhost:4318/v1/traces
      timeout: 5s
  endpoints:
    web:
      exposure:
        include: health,prometheus,metrics

Demo用1.0便于学习时每次看到Trace;商业高流量系统不能不做容量评估就全量采样。生产应通过Collector Tail Sampling、分入口概率和SLO策略控制成本。

20.5 启动与调用验证 ​

bash
mvn spring-boot:run
bash
curl -i -X POST "http://localhost:8080/inventory/SKU-1001/reserve?quantity=2"

应验证四类证据:

  1. HTTP响应成功。
  2. 应用日志包含traceId和spanId。
  3. Prometheus端点包含规范化的HTTP和业务Observation Metric,不出现SKU标签爆炸。
  4. Collector收到Server Span和reserve inventory子Span,并导出到后端。

二十一、JDK 8与Boot 2.7存量Demo ​

这一节不是让新项目回退到旧技术,而是让维护者能识别真实边界。JDK 8不能使用Record、Map.of、文本块、Lambda参数的var和String.isBlank;Boot 2.7也不能直接使用Boot 3的Observation自动配置属性。

21.1 POM版本骨架 ​

xml
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.18</version>
    <relativePath/>
</parent>

<properties>
    <java.version>8</java.version>
    <spring-cloud.version>2021.0.8</spring-cloud.version>
</properties>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring-cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-sleuth</artifactId>
    </dependency>
</dependencies>

该组合解析到Sleuth 3.1.9与Brave 5.13.9。BOM负责兼容组合,不要只把Sleuth Starter单独升级到另一个大版本。

21.2 读取Sleuth当前Span ​

java
package com.example.legacy;

import java.util.LinkedHashMap;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.sleuth.Span;
import org.springframework.cloud.sleuth.Tracer;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class LegacyTraceController {
    private static final Logger log = LoggerFactory.getLogger(LegacyTraceController.class);
    private final Tracer tracer;

    public LegacyTraceController(Tracer tracer) {
        this.tracer = tracer;
    }

    @GetMapping("/legacy/trace")
    public Map<String, String> trace() {
        Span currentSpan = tracer.currentSpan();
        String traceId = currentSpan == null
                ? "NO_CURRENT_SPAN"
                : currentSpan.context().traceId();
        log.info("Legacy request traceId={}", traceId);

        Map<String, String> response = new LinkedHashMap<String, String>();
        response.put("traceId", traceId);
        response.put("line", "JDK8_BOOT2_SLEUTH");
        return response;
    }
}

这里使用LinkedHashMap而不是Map.of,编译目标应为Java 8字节码major version 52。若currentSpan()为空,不应随便伪造一个Span ID冒充框架链路,应检查请求是否经过Sleuth接入点、Sampler和Scope。

21.3 JDK 8异步传播边界 ​

Sleuth会为一部分受Spring管理的异步组件提供装饰和传播,但不能推导出“任意new的线程池都会自动传播”。原理仍是:提交时捕获当前Trace Context,执行时恢复,finally关闭并恢复旧Context。排查时先检查项目实际使用的是Sleuth的Tracer、Brave API还是自定义包装器,再按该版本提供的Executor装饰方式实现;不要把Boot 3的ContextSnapshotFactory直接复制进尚未迁移依赖的Boot 2项目。

二十二、观测链失败窗口 ​

窗口业务是否发生平台现象处理方式
Observation被Predicate禁用发生无Metric/Span查Registry配置和条件
Head Sampling未采样发生无完整Trace用Metric/日志证明,调整采样策略
HTTP Header未注入发生下游新Trace查客户端是否使用受管Builder
线程池未传播Context发生异步任务新Trace或无MDC提交时捕获并包装Executor
SDK队列满发生Span dropped监控队列和Exporter失败
应用强杀前未flush发生最后一批Trace缺失优雅停机,接受崩溃窗口
Collector拒绝或OOM发生accepted少、dropped高限内存、扩容、削减高成本属性
Tail Sampling丢弃发生后端查不到查采样策略与decision_wait
Backend写入失败发生Collector export失败检查队列、重试、认证和存储
查询时间/服务名错误发生且已存储UI显示无数据校验时区、resource和查询条件

二十三、生产Runbook ​

23.1 完全没有Trace ​

  1. 检查Actuator、Tracing Bridge和Exporter依赖树。
  2. 查看Tracer、ObservationRegistry、SpanExporter Bean是否存在。
  3. 检查management.tracing.enabled和采样概率。
  4. 用本地手工Observation确认SDK是否产生Span。
  5. 查看BatchSpanProcessor队列和Exporter日志。
  6. 查看Collector Receiver accepted、refused、dropped。
  7. 查看Backend Exporter和存储写入。
  8. 最后检查UI时间范围、服务名和租户。

23.2 Trace在某个服务断开 ​

  1. 对比上游Client Span与下游Server Span的Trace ID。
  2. 抓取脱敏后的traceparent/B3 Header。
  3. 检查是否手动new了未受管HTTP Client。
  4. 检查网关、代理是否删除或改写Header。
  5. 检查传播格式是否一端只懂W3C、另一端只懂B3。
  6. MQ场景检查消息Header、重试和批量Link策略。

23.3 异步任务日志没有traceId ​

  1. 记录提交线程和执行线程。
  2. 检查是否使用公共ForkJoinPool。
  3. 检查ContextSnapshot是否在提交时捕获。
  4. 检查ThreadLocalAccessor是否注册。
  5. 确认Scope在finally关闭。
  6. 删除与框架冲突的手写MDC覆盖逻辑。

23.4 只有错误请求查不到Trace ​

  1. 查看Head Sampling概率与父采样决定。
  2. 用全量错误Metric证明错误真实存在。
  3. 查看Collector是否启用Tail Sampling。
  4. 检查同Trace是否被路由到不同Collector实例。
  5. 检查decision_wait、内存和策略顺序。
  6. 不要临时全局100%采样后忘记恢复。

23.5 Collector不可用后业务变慢 ​

  1. 检查Exporter是否同步阻塞业务线程。
  2. 检查后台重试、DNS和连接超时。
  3. 检查SDK队列内存和GC。
  4. 限制Exporter日志频率。
  5. 让遥测失败快速、有界并产生自身告警。
  6. 恢复后限速排队数据,防止冲垮后端。

23.6 Prometheus时间序列暴涨 ​

  1. 找新增Metric和标签组合。
  2. 检查是否把URL原始路径、orderId、userId、异常文本作为标签。
  3. 改为规范化route、有限errorCode和version。
  4. 高基数转到受控Trace/日志。
  5. 清理无用Dashboard与录制规则不能消除源头基数,必须修埋点。

二十四、安全与数据治理 ​

  • Trace Header来自不可信网络,不作为身份凭据。
  • Baggage只允许白名单键、长度限制和敏感检查。
  • Span属性和日志执行脱敏、访问控制与保留期管理。
  • OTLP链路启用TLS、认证和最小网络权限。
  • Collector配置和Exporter凭据进入Secret管理,不写文档或日志。
  • 多租户Backend必须防止跨租户Trace查询。
  • 调试时不要把请求体、响应体和Authorization全量记录。

医疗、支付和身份数据应把“能排查”与“最小必要披露”同时设计。traceId用于关联,不代表可以围绕它无限收集业务内容。

二十五、常见反模式 ​

反模式后果
手写UUID Header当完整Tracing没有Span父子、采样和SDK上下文
框架Tracing上再覆盖MDCtraceId错乱和线程污染
直接new HTTP Client绕过Boot Observation定制
orderId作为Metric标签时间序列爆炸
Baggage传完整用户信息Header膨胀和敏感泄露
Head Sampling 10%却要求每个错误可查错误Trace随机缺失
Collector挂了阻塞支付线程观测故障升级成业务事故
队列无限增大防止丢Span堆内存和GC事故
只看Trace判断交易成功采样和遥测丢失误判事实
Sleuth依赖直接换成OTelHeader、MDC、采样和Span命名不兼容

二十六、源码阅读路线 ​

Boot 3.2.4与对应依赖可按以下顺序阅读:

  1. ObservationAutoConfiguration:Registry和Meter/Tracing Handler分组。
  2. ObservationRegistryPostProcessor与ObservationRegistryConfigurer:Bean怎样注册到Registry。
  3. SimpleObservation:start、Scope、error、stop与逆序关闭。
  4. MicrometerTracingAutoConfiguration:默认、Sender、Receiver Handler。
  5. DefaultTracingObservationHandler:普通Observation怎样创建子Span。
  6. PropagatingReceiverTracingObservationHandler:从Carrier提取。
  7. PropagatingSenderTracingObservationHandler:向Carrier注入。
  8. OpenTelemetryAutoConfiguration:Sampler、SDK、Processor、Bridge和MDC。
  9. OtlpAutoConfiguration:HTTP/protobuf Exporter条件。
  10. BatchSpanProcessor:有界队列、批量、flush和shutdown。
  11. ContextRegistry、ContextSnapshotFactory、ContextExecutorService:线程切换传播。
  12. WebMvcObservationAutoConfiguration和HTTP Client配置:框架接入点。

二十七、关联知识点 ​

本章小结 ​

Boot 3可观测性主链是“框架创建Observation → Registry选择Convention与Handler → Receiver提取或Sender注入远程Context → Tracing Handler创建Span、Meter Handler聚合Metric → OTel Bridge进入SdkTracerProvider → Parent-based Sampler决定记录 → BatchSpanProcessor有界排队 → OTLP Exporter发送Collector → Processor采样、批处理和路由 → 后端查询并与日志、业务事实关联”。真正掌握它必须知道每层为什么存在、失败时丢什么、怎样验证,而不是只会在日志格式中加一个traceId。