Spring Sleuth Hoxton.SR9 RabbitMQ b3单头缺失ParentSpan咨询
应用从Spring Cloud Greenwich.SR2 迁移至 Hoxton.SR9 版本,目标是实现从REST请求入口触发、到最终投递RabbitMQ的全链路调用追踪。
两个版本的核心传播机制差异:
- 旧Greenwich版本会传递3个独立消息头,分别存储trace、span、parentSpan信息
- 新Hoxton版本默认仅传递单个
b3头,格式为{traceId}-{spanId}-{sample}-{parentSpan},后两个字段为可选参数
遇到的异常表现
RabbitMQ队列中收到的消息携带的b3头始终不包含parentSpan字段,链路表现为:
- 应用REST控制器内部链路:traceId=T, spanId=S1, parentSpanId=S0
- 应用内部触发消息投递前的链路:traceId=T, spanId=S2, parentSpanId=S1
- RabbitMQ队列中的消息头:
b3=T-S2-1
trace值传递正确,但消息头直接复用了应用内发消息上下文的span值,完全缺失parentSpan字段。按照旧版本的预期,消息投递时应当生成新的spanId,且消息头里的parentSpan值等于应用内发消息上下文对应的span值。
当前显式配置的Sleuth参数为:
spring.sleuth.enabled=true spring.messaging.enabled=true spring.supportsJoin=false
注:将
supportsJoin恢复为默认值测试,同样无法解决parentSpanId缺失的问题。
问题根因定位
通过断点调试逐层追踪的逻辑链路:
- 向
AbstractMessageChannel.send()传入GenericMessage实例 - 消息最终传递至
TracingChannelInterceptor.preSend()方法 - 该拦截器配置的
DeferredInjector实例中,消息头写入逻辑的实现类为MessageHeaderPropagation,生产端的注入函数被默认设置为SINGLE_NO_PARENT - 拦截器执行注入逻辑时,
DeferredInjector.inject(span.context(), headers)会按照SINGLE_NO_PARENT模式向外发消息添加格式为{traceId}-{spanId}-1的b3头
根因明确:parentSpanId缺失是Hoxton版本默认的生产端注入逻辑导致,默认采用SINGLE_NO_PARENT的B3格式,不会写入父span信息。
自定义配置的效果
注册如下Bean覆盖默认传播设置后,链路表现符合预期:
- 应用REST控制器内部链路:traceId=T, spanId=S1, parentSpanId=S0
- 应用内部触发消息投递前的链路:traceId=T, spanId=S2, parentSpanId=S1
- RabbitMQ队列中的消息头:
b3=T-S2-1-S1
对应的自定义配置代码:
@Configuration public class B3Configuration { @Bean Propagation.Factory customPropagationFactory() { return B3Propagation.newFactoryBuilder() .injectFormat(B3Propagation.Format.SINGLE) .injectFormat(Span.Kind.CLIENT, B3Propagation.Format.SINGLE) .injectFormat(Span.Kind.CONSUMER, B3Propagation.Format.SINGLE) .injectFormat(Span.Kind.PRODUCER, B3Propagation.Format.SINGLE) .build(); } }
默认行为的设计逻辑
Hoxton版本的Sleuth基于Brave 5.x的新传播模型,选择SINGLE_NO_PARENT作为生产端的默认格式,是遵循了B3传播规范的通用设计:
- 异步消息场景下,生产端投递消息时,本质是把当前链路上下文作为下游消费端的根上下文,不需要额外传递父span——因为下游消费端启动新的链路span时,上游生产端的span本身就是链路的边界
- 旧Greenwich版本的传播逻辑没有遵循这个设计,会把跨进程的父span一并写入消息头,实际会造成下游消费端的链路嵌套深度无意义增加
- 需要手动修改配置拿到parentSpan的场景,本质是有跨系统全链路串联需求:希望把应用内发消息的上下文span值作为消息头里的父span,方便追踪从发消息逻辑到队列的完整关联。这时候只需要把生产端的B3注入格式从
SINGLE_NO_PARENT改成SINGLE格式即可。
更优的实现方式
现有自定义配置存在冗余,不需要给所有Span.Kind都指定SINGLE格式,只需要精准覆盖生产端的注入格式即可,简化后的配置为:
@Configuration public class SleuthPropagationConfig { @Bean Propagation.Factory b3PropagationFactory() { return B3Propagation.newFactoryBuilder() // 仅覆盖生产端的注入格式为带父span的SINGLE格式 .injectFormat(Span.Kind.PRODUCER, B3Propagation.Format.SINGLE) .build(); } }
注意:CLIENT、CONSUMER、全局默认格式保留
SINGLE_NO_PARENT的默认配置即可兼容跨服务的正常链路追踪,仅修改PRODUCER类型的注入格式,就能拿到消息头带parentSpan的预期效果,不会影响其他场景的传播性能。
内容的提问来源于stack exchange,提问作者wabrit

