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.8 | Sleuth 3.1.9、Brave 5.13.9、Zipkin Reporter 2.16.3 | 维护和迁移老系统 |
| 现代线 | Java 17、Boot 3.2.4 | Observation 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版本的固定实现。
一、学习目标
学完后应能独立回答:
- Observation、Metric、Span、Trace、Log和OpenTelemetry各自是什么。
- Boot如何创建ObservationRegistry并把Handler、Predicate、Convention和Filter注册进去。
- 一次Observation的start、openScope、error、closeScope和stop怎样通知Handler。
- 为什么一个Observation可以同时产生Metric和Span。
- Receiver Handler怎样提取
traceparent,Sender Handler怎样注入下游Carrier。 - Micrometer Tracing怎样桥接OpenTelemetry Tracer、Propagator和MDC。
- Web入口、HTTP客户端、OpenFeign、Gateway、线程池和MQ在哪一步最容易断链。
- Head Sampling、Parent-based Sampling与Collector Tail Sampling为什么不是一回事。
- BatchSpanProcessor队列满、Exporter失败和Collector背压时数据怎样丢失。
- Sleuth存量系统怎样迁移到Boot 3,并兼容B3与W3C传播。
- 如何设计低基数Metric、可检索Trace、脱敏日志和Exemplar关联。
- 如何排查无Trace、断链、MDC错乱、关键错误未采样和观测系统拖慢业务。
二、先建立完整数据链
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会影响结果 |
Tracer | Micrometer Tracing门面 | 不等于具体OTel SDK或Brave实现 |
Propagator | 从Carrier提取或向Carrier注入上下文 | 不负责用户认证和Header可信度 |
OtelTracer | Micrometer Tracer到OTel API的桥 | 不是Exporter |
SdkTracerProvider | OTel Span创建、采样和Processor入口 | 不负责最终存储 |
BatchSpanProcessor | 有界排队、批量交给Exporter | 队列满会丢Span,不是持久队列 |
SpanExporter | 把SpanData写向OTLP等出口 | Export成功不等于后端已永久可查 |
| Collector | 接收、加工、采样、缓冲和路由 | 不在核心业务请求路径中更安全 |
四、Boot 3启动时自动配置了什么
现代样本中的主链:
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样本会组装:
Sampler。SdkTracerProvider。SpanProcessors。BatchSpanProcessor。SpanExporters。- OTel API
Tracer。 - Micrometer
OtelTracer。 OtelPropagator。OtelCurrentTraceContext。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 |
PropagatingReceiverTracingObservationHandler | HTTP/MQ接收端,从Carrier提取父上下文并创建Receiver Span |
PropagatingSenderTracingObservationHandler | HTTP/MQ发送端,创建Sender Span并注入Carrier |
Handler有顺序。启动按注册顺序通知,停止和Scope关闭通常按逆序回调,以正确恢复嵌套资源和当前上下文。
五、一次Observation生命周期
flowchart TD
A["createNotStarted"] --> B["start通知onStart"]
B --> C["openScope设为当前Observation"]
C --> D["业务执行并添加Event或Error"]
D --> E["Scope关闭并恢复上一个上下文"]
E --> F["stop通知onStop并导出语义"]SimpleObservation样本中的关键顺序:
start()先应用Convention提供的低/高基数KeyValues和名称,再按顺序调用HandleronStart。openScope()把当前Observation放入Registry,并通知Scope打开。error(Throwable)把错误保存到Context并通知onError。stop()再次应用Convention,执行ObservationFilter,再逆序通知HandleronStop。- 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、version | Metric标签,也可进入Trace |
| 高基数 | orderId、requestId、具体URL参数、错误实例 | Trace属性或受控日志,不应成为Metric标签 |
错误示例:
observation.lowCardinalityKeyValue("order.id", orderId);一百万订单会制造大量时间序列组合。正确做法:
observation.lowCardinalityKeyValue("operation", "reserve");
observation.highCardinalityKeyValue("order.id", orderId);高基数也不是无限免费:Trace存储、索引和隐私成本同样存在。订单号应按查询需求、保留期和访问权限治理,身份证、Token、密码和完整医疗数据不应直接写入属性。
七、HTTP入口怎样建立Server Span
Spring MVC样本通过ServerHttpObservationFilter接入ObservationRegistry。概念链:
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并注入
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格式:
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。更稳妥的过程:
- 清点入口、HTTP、MQ和第三方传播格式。
- 现代服务先同时消费B3与W3C。
- 选择唯一主生产格式。
- 灰度观察断链率和Trace树形。
- 所有消费者兼容后再停止旧格式生产。
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字段。
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适合有独立业务意义和耗时边界的操作,例如“库存预留计算”或“生成对账批次”。
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不会天然复制。
正确模型:
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字段。
示意:
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样本默认头采样
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 delay | 5000ms |
| max queue size | 2048 spans |
| max export batch size | 512 spans |
| exporter timeout | 30000ms |
流程:
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导出链
现代样本配置:
management:
tracing:
enabled: true
sampling:
probability: 0.10
otlp:
tracing:
endpoint: http://otel-collector:4318/v1/traces
timeout: 10s
compression: gzipBoot 3.2.4的OtlpAutoConfiguration样本自动配置的是OTLP HTTP/protobuf Exporter;只有配置endpoint或提供ConnectionDetails时创建。若应用自定义OtlpGrpcSpanExporter,自动配置会后退。不要把HTTP端口4318和gRPC端口4317混用。
链路:
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版本校验:
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怎样闭环
商业排查顺序:
- Metric通过规范化route、status、version发现影响范围。
- Histogram或Exemplar跳到代表性Trace。
- Trace找到慢Span和具体实例。
- 用traceId查询结构化日志。
- 用request_id或订单号查询业务事实。
Metric标签不能放traceId;Trace属性也不能替代数据库审计;日志没有记录不能证明操作没发生。三个信号分别解决聚合、因果和细节问题。
建议日志字段:
{
"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.probability | management.tracing.sampling.probability |
| Sleuth自动埋点 | Framework/Boot Observation |
| Brave Tracer API | Micrometer Tracer门面,可桥接Brave或OTel |
| Zipkin Reporter | Zipkin或OTLP SpanExporter |
| B3默认传播 | 明确配置W3C/B3生产与消费策略 |
| Sleuth MDC | Micrometer桥的Scope/MDC关联 |
迁移步骤:
- 记录旧服务名、Span名、采样率、B3格式、Baggage、MDC和Zipkin后端。
- 清点自定义Sleuth Bean、Sampler、Filter和Feign拦截器。
- 在契约测试中验证入口、Feign、线程池、MQ和Gateway传播。
- 新服务先兼容消费旧B3。
- 逐步切换主生产格式和Exporter。
- 对比Trace完整率、Span数量、Metric标签和日志关联。
- 最后删除手写
X-Trace-Id覆盖逻辑和旧Sleuth依赖。
不能只把依赖坐标替换掉:Span命名、采样默认值、Header、MDC、Baggage和Exporter都会影响平台查询与告警。
二十、Java 17与Boot 3可运行Demo
20.1 Maven依赖
<?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 启动类
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
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 配置
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,metricsDemo用1.0便于学习时每次看到Trace;商业高流量系统不能不做容量评估就全量采样。生产应通过Collector Tail Sampling、分入口概率和SLO策略控制成本。
20.5 启动与调用验证
mvn spring-boot:runcurl -i -X POST "http://localhost:8080/inventory/SKU-1001/reserve?quantity=2"应验证四类证据:
- HTTP响应成功。
- 应用日志包含traceId和spanId。
- Prometheus端点包含规范化的HTTP和业务Observation Metric,不出现SKU标签爆炸。
- 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版本骨架
<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
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
- 检查Actuator、Tracing Bridge和Exporter依赖树。
- 查看Tracer、ObservationRegistry、SpanExporter Bean是否存在。
- 检查
management.tracing.enabled和采样概率。 - 用本地手工Observation确认SDK是否产生Span。
- 查看BatchSpanProcessor队列和Exporter日志。
- 查看Collector Receiver accepted、refused、dropped。
- 查看Backend Exporter和存储写入。
- 最后检查UI时间范围、服务名和租户。
23.2 Trace在某个服务断开
- 对比上游Client Span与下游Server Span的Trace ID。
- 抓取脱敏后的
traceparent/B3 Header。 - 检查是否手动new了未受管HTTP Client。
- 检查网关、代理是否删除或改写Header。
- 检查传播格式是否一端只懂W3C、另一端只懂B3。
- MQ场景检查消息Header、重试和批量Link策略。
23.3 异步任务日志没有traceId
- 记录提交线程和执行线程。
- 检查是否使用公共ForkJoinPool。
- 检查ContextSnapshot是否在提交时捕获。
- 检查ThreadLocalAccessor是否注册。
- 确认Scope在finally关闭。
- 删除与框架冲突的手写MDC覆盖逻辑。
23.4 只有错误请求查不到Trace
- 查看Head Sampling概率与父采样决定。
- 用全量错误Metric证明错误真实存在。
- 查看Collector是否启用Tail Sampling。
- 检查同Trace是否被路由到不同Collector实例。
- 检查decision_wait、内存和策略顺序。
- 不要临时全局100%采样后忘记恢复。
23.5 Collector不可用后业务变慢
- 检查Exporter是否同步阻塞业务线程。
- 检查后台重试、DNS和连接超时。
- 检查SDK队列内存和GC。
- 限制Exporter日志频率。
- 让遥测失败快速、有界并产生自身告警。
- 恢复后限速排队数据,防止冲垮后端。
23.6 Prometheus时间序列暴涨
- 找新增Metric和标签组合。
- 检查是否把URL原始路径、orderId、userId、异常文本作为标签。
- 改为规范化route、有限errorCode和version。
- 高基数转到受控Trace/日志。
- 清理无用Dashboard与录制规则不能消除源头基数,必须修埋点。
二十四、安全与数据治理
- Trace Header来自不可信网络,不作为身份凭据。
- Baggage只允许白名单键、长度限制和敏感检查。
- Span属性和日志执行脱敏、访问控制与保留期管理。
- OTLP链路启用TLS、认证和最小网络权限。
- Collector配置和Exporter凭据进入Secret管理,不写文档或日志。
- 多租户Backend必须防止跨租户Trace查询。
- 调试时不要把请求体、响应体和Authorization全量记录。
医疗、支付和身份数据应把“能排查”与“最小必要披露”同时设计。traceId用于关联,不代表可以围绕它无限收集业务内容。
二十五、常见反模式
| 反模式 | 后果 |
|---|---|
| 手写UUID Header当完整Tracing | 没有Span父子、采样和SDK上下文 |
| 框架Tracing上再覆盖MDC | traceId错乱和线程污染 |
| 直接new HTTP Client | 绕过Boot Observation定制 |
| orderId作为Metric标签 | 时间序列爆炸 |
| Baggage传完整用户信息 | Header膨胀和敏感泄露 |
| Head Sampling 10%却要求每个错误可查 | 错误Trace随机缺失 |
| Collector挂了阻塞支付线程 | 观测故障升级成业务事故 |
| 队列无限增大防止丢Span | 堆内存和GC事故 |
| 只看Trace判断交易成功 | 采样和遥测丢失误判事实 |
| Sleuth依赖直接换成OTel | Header、MDC、采样和Span命名不兼容 |
二十六、源码阅读路线
Boot 3.2.4与对应依赖可按以下顺序阅读:
ObservationAutoConfiguration:Registry和Meter/Tracing Handler分组。ObservationRegistryPostProcessor与ObservationRegistryConfigurer:Bean怎样注册到Registry。SimpleObservation:start、Scope、error、stop与逆序关闭。MicrometerTracingAutoConfiguration:默认、Sender、Receiver Handler。DefaultTracingObservationHandler:普通Observation怎样创建子Span。PropagatingReceiverTracingObservationHandler:从Carrier提取。PropagatingSenderTracingObservationHandler:向Carrier注入。OpenTelemetryAutoConfiguration:Sampler、SDK、Processor、Bridge和MDC。OtlpAutoConfiguration:HTTP/protobuf Exporter条件。BatchSpanProcessor:有界队列、批量、flush和shutdown。ContextRegistry、ContextSnapshotFactory、ContextExecutorService:线程切换传播。WebMvcObservationAutoConfiguration和HTTP Client配置:框架接入点。
二十七、关联知识点
- Spring Cloud链路追踪入门
- Spring可观测性独立面试题
- 微服务可观测性、Collector、采样与SLO
- 服务调用端到端全过程
- OpenFeign内部原理与观测
- Gateway调用链
- 消息队列与异步追踪
- 生产故障证据链
本章小结
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。
