Span 上下文传递:HTTP headers 传播、gRPC metadata、消息队列 header 注入

做过分布式链路追踪的朋友都知道,光在单进程内把 Span 串起来不难。难的是跨进程、跨语言的 Context 传递。说白了,我得想办法让下游服务知道「我是谁、我从哪来」。有三种主流方式:HTTP Header、gRPC Metadata、消息队列 Header。每种我都踩过坑,今天一个一个讲透。

1. HTTP Headers 传播——W3C 标准 vs B3 格式

HTTP 是最常见的 RPC 协议。我个人习惯用 W3C 的 traceparent 头,它把版本、TraceID、SpanID 和 trace flags 拼成一个字符串,简洁明了。

// 示例:W3C traceparent 格式
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
// 00 = 版本号
// 4bf9... = TraceID (16字节 hex)
// 00f0... = SpanID (8字节 hex)
// 01 = trace flags (01 表示采样)

我之前接过一个项目,上游用 B3 格式(Zipkin 的 X-B3-TraceId),下游要收 W3C。两边没对齐,结果链路全断。我的建议是:团队统一一个标准,推荐 W3C,生态支持最好。

2. gRPC Metadata——用拦截器隐式传递

gRPC 的 metadata 类似 HTTP header,但更轻量。我把 Span Context 塞进 outgoing 的 metadata key 里,比如 traceparent。

// Python 客户端拦截器示例
class ClientInterceptor(grpc.UnaryUnaryClientInterceptor):
    def intercept_unary_unary(self, continuation, client_call_details, request):
        metadata = []
        if client_call_details.metadata:
            metadata = list(client_call_details.metadata)
        # 注入当前 span context
        carrier = {}
        propagator.inject(carrier)
        metadata.append(('traceparent', carrier['traceparent']))
        new_details = client_call_details._replace(metadata=metadata)
        return continuation(new_details, request)

一个小技巧:
别忘了在服务端也通过拦截器提取 metadata,否则下游收不到。我曾经一个团队没加服务端提取,链路全部中断,查了半天才发现。

3. 消息队列 Header 注入——Kafka / RabbitMQ / RocketMQ 的差异

消息队列是异步场景,Context 传递最容易出问题。很多中间件支持在消息的 Header / Properties 里携带自定义属性。我以 Kafka 为例:

// Java 生产者示例
ProducerRecord<String, String> record = new ProducerRecord<>(topic, value);
Headers headers = record.headers();
headers.add("traceparent", spanContext.encode().getBytes(StandardCharsets.UTF_8));
producer.send(record);

注意:
Kafka 的 Header 是二进制的(Byte 数组),你传输字符串时要显式编码。RabbitMQ 的 Header 是 Map,传递更方便。RocketMQ 的 Properties 也是字符串。务必统一编码,否则下游解析乱码,链路照样断。

传递方式 载体类型 常用 Header 名称 编码要求
HTTP Headers 字符串键值对 traceparent / X-B3-TraceId 纯文本
gRPC Metadata 元数据键值对 traceparent 字符串(支持 ASCII)
Kafka Header 字节数组 traceparent 必须转换为 UTF-8 字节
RabbitMQ Header Map (String, Object) traceparent 字符串直接放入 Map
RocketMQ Properties Map (String, String) traceparent 字符串

核心观点:

  • 不管用什么协议,传递的内容就是 TraceID + ParentSpanID + Trace flags。
  • 传递方式要跟团队客户端/服务端框架的拦截能力匹配。
  • 所有中间件都需要在 Producer 端注入、Consumer 端提取,缺一不可。
Span 上下文跨进程传递示意图 应用 A 应用 B HTTP Headers gRPC Metadata 消息队列 Header 关键要点 • 应用A在发出请求/投递消息前,将 Span 上下文序列化注入到对应载体中 • 应用B收到后,从载体中反序列化,创建新的子 Span 并关联父 Span • 所有通路都必须同时实现注入(Inject)和提取(Extract),否则链路中断

我的个人经验:
我在项目里见过最坑的情况是——Kafka Consumer 端忘记加拦截器提取 Header,结果所有消费链路都显示“孤儿 Span”。排查时发现 Producer 明明注入了 TraceID,Consumer 却没拿到。最后加了统一拦截器,问题才解决。所以我的原则:传递这件事必须在框架层统一封装,业务代码永远别手动操作 Header。

你想想看,Span 上下文传递其实就四个字:注入和提取。但每种载体都有细节。记住一个要点:先统一标准(比如 W3C),再用框架拦截器自动化注入提取,最后在压测和灰度时验证链路完整性。这样你就能避开 90% 的坑了。