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。推荐使用observescoped或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对RestTemplateBuilderRestClient.BuilderWebClient.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-TraceIdX-B3-SpanIdX-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:把traceIdspanId放入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参数的varString.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. ObservationRegistryPostProcessorObservationRegistryConfigurer: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. ContextRegistryContextSnapshotFactoryContextExecutorService:线程切换传播。
  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。