54. 传播

传播需要确保来自同一根的活动被收集到同一个跟踪中。最常见的传播方法是通过向接收跟踪上下文的服务器发送 RPC 请求,从客户端复制跟踪上下文。

例如,当进行下游 HTTP 调用时,其跟踪上下文编码为请求头并与之一起发送,如下图所示:

   Client Span                                                Server Span
┌──────────────────┐                                       ┌──────────────────┐
│                  │                                       │                  │
│   TraceContext   │           Http Request Headers        │   TraceContext   │
│ ┌──────────────┐ │          ┌───────────────────┐        │ ┌──────────────┐ │
│ │ TraceId      │ │          │ X─B3─TraceId      │        │ │ TraceId      │ │
│ │              │ │          │                   │        │ │              │ │
│ │ ParentSpanId │ │ Extract  │ X─B3─ParentSpanId │ Inject │ │ ParentSpanId │ │
│ │              ├─┼─────────>│                   ├────────┼>│              │ │
│ │ SpanId       │ │          │ X─B3─SpanId       │        │ │ SpanId       │ │
│ │              │ │          │                   │        │ │              │ │
│ │ Sampled      │ │          │ X─B3─Sampled      │        │ │ Sampled      │ │
│ └──────────────┘ │          └───────────────────┘        │ └──────────────┘ │
│                  │                                       │                  │
└──────────────────┘                                       └──────────────────┘

上面的名称来自 B3传播,它内置于 Brave 中,并且在许多语言和框架中都有实现。

大多数用户使用框架拦截器来自动传播。接下来的两个例子展示了这对于客户端和服务器是如何工作的。

下面的示例展示客户端传播可能如何工作:

@Autowired Tracing tracing;

// configure a function that injects a trace context into a request
injector = tracing.propagation().injector(Request.Builder::addHeader);

// before a request is sent, add the current span's context to it
injector.inject(span.context(), request);

以下示例展示了服务器端传播的工作方式:

@Autowired Tracing tracing;
@Autowired Tracer tracer;

// configure a function that extracts the trace context from a request
extractor = tracing.propagation().extractor(Request::getHeader);

// when a server receives a request, it joins or starts a new trace
span = tracer.nextSpan(extractor.extract(request));

54.1 传播额外字段

有时需要传播额外的字段,例如请求 ID 或备用跟踪上下文。例如,如果你在 Cloud Foundry 环境中,可能希望传递请求 ID,如下例所示:

// when you initialize the builder, define the extra field you want to propagate
Tracing.newBuilder().propagationFactory(
  ExtraFieldPropagation.newFactory(B3Propagation.FACTORY, "x-vcap-request-id")
);

// later, you can tag that request ID or use it in log correlation
requestId = ExtraFieldPropagation.get("x-vcap-request-id");

你可能还需要传播一个不使用的跟踪上下文。例如,可能在 Amazon Web 服务环境中,但没有向 X-Ray 报告数据。为了确保 X-Ray 能够正确共存,请通过其跟踪头,如下例所示:

tracingBuilder.propagationFactory(
  ExtraFieldPropagation.newFactory(B3Propagation.FACTORY, "x-amzn-trace-id")
);
[Tip] Tip

在 Spring Cloud Sleuth 中,tracing builder Tracing.newBuilder() 的所有元素都定义为 bean。因此,如果想传递一个自定义的传播工厂,那么你就可以创建一个这种类型的 bean,我们将在跟踪 bean 中设置它。

54.1.1 前缀字段

如果它们遵循一个共同的模式,你也可以为字段加前缀。下面的示例展示如何按原样传播 x-vcap-request-id 字段,但分别以 x-baggage-country-code 和 x-baggage-user-id 的形式在线路上发送 country-code 和 user-id 字段:

Tracing.newBuilder().propagationFactory(
  ExtraFieldPropagation.newFactoryBuilder(B3Propagation.FACTORY)
                       .addField("x-vcap-request-id")
                       .addPrefixedFields("x-baggage-", Arrays.asList("country-code", "user-id"))
                       .build()
);

稍后,你可以调用以下代码以影响当前跟踪上下文的 country code:

ExtraFieldPropagation.set("x-country-code", "FO");
String countryCode = ExtraFieldPropagation.get("x-country-code");

或者,如果你有对跟踪上下文的引用,则可以显式使用它,如下例所示:

ExtraFieldPropagation.set(span.context(), "x-country-code", "FO");
String countryCode = ExtraFieldPropagation.get(span.context(), "x-country-code");
[Important] 重点

与之前版本的 Sleuth 不同的是,有了 Brave,你必须通过 baggage key。实现这一点有两个属性。使用 spring.sleuth.baggage-keys,可以设置带有 baggage- 前缀的 key,用于 HTTP 调用,以及用于消息传递的 baggage_。还可以使用 spring.sleuth.propagation-keys 属性来传递没有任何前缀的白名单前缀键列表。注意在 header key 前面没有 x-。

为了自动将 baggage 值设置为 Slf4j 的 MDC,必须使用白名单 baggage 和 传播 key 设置 spring.sleuth.log.slf4j.whitelisted-mdc-keys 属性。例如 spring.sleuth.log.slf4j.whitelisted-mdc-keys=foo 将 foo baggage 的值设置为 MDC。

[Important] 重点

记住,向 MDC 中添加条目会显著降低应用程序的性能!

如果要将 baggage 条目添加为标签,为了能够通过 baggage 条目搜索 Span ,可以使用白名单 baggage key 设置 spring.sleuth.propagation.tag.whitelisted-keys 的值。要禁用此功能,必须传递 spring.sleuth.propagation.tag.enabled=false 属性。

54.1.2 提取传播的上下文

TraceContext.Extractor<C> 从传入的请求或消息中读取跟踪标识符和抽样状态。载体通常是请求对象或报头。

此实用程序用于标准检测(如 HttpServerHandler),但也可用于自定义 RPC 或消息代码。

TraceContextOrSamplingFlags 通常只与 Tracer.nextSpan(extracted) 一起使用,除非在客户端和服务器之间共享 Span ID。

54.1.3 在客户端和服务器之间共享 Span ID

正常的检测模式是创建一个表示 RPC 服务器端的 Span 。Extractor.extract 在应用于传入的客户端请求时可能返回完整的跟踪上下文。Tracer.joinSpan 尝试使用相同的 Span ID(如果支持)继续此跟踪,或者如果不支持,则创建子 Span 。共享 Span ID 时,报告的数据包含一个这样的标志。

下图展示了 B3 传播的示例:

                              ┌───────────────────┐      ┌───────────────────┐
 Incoming Headers             │   TraceContext    │      │   TraceContext    │
┌───────────────────┐(extract)│ ┌───────────────┐ │(join)│ ┌───────────────┐ │
│ X─B3-TraceId      │─────────┼─┼> TraceId      │ │──────┼─┼> TraceId      │ │
│                   │         │ │               │ │      │ │               │ │
│ X─B3-ParentSpanId │─────────┼─┼> ParentSpanId │ │──────┼─┼> ParentSpanId │ │
│                   │         │ │               │ │      │ │               │ │
│ X─B3-SpanId       │─────────┼─┼> SpanId       │ │──────┼─┼> SpanId       │ │
└───────────────────┘         │ │               │ │      │ │               │ │
                              │ │               │ │      │ │  Shared: true │ │
                              │ └───────────────┘ │      │ └───────────────┘ │
                              └───────────────────┘      └───────────────────┘

有些传播系统仅转发父 Span ID,在 Propagation.Factory.supportsJoin() == false 时检测到。在这种情况下,总是提供一个新的 Span ID,并且传入的上下文决定父 ID。

下图展示了 AWS 传播的示例:

                              ┌───────────────────┐      ┌───────────────────┐
 x-amzn-trace-id              │   TraceContext    │      │   TraceContext    │
┌───────────────────┐(extract)│ ┌───────────────┐ │(join)│ ┌───────────────┐ │
│ Root              │─────────┼─┼> TraceId      │ │──────┼─┼> TraceId      │ │
│                   │         │ │               │ │      │ │               │ │
│ Parent            │─────────┼─┼> SpanId       │ │──────┼─┼> ParentSpanId │ │
└───────────────────┘         │ └───────────────┘ │      │ │               │ │
                              └───────────────────┘      │ │  SpanId: New  │ │
                                                         │ └───────────────┘ │
                                                         └───────────────────┘

注意:一些 Span 报告程序不支持共享 Span ID。例如,如果设置 Tracing.Builder.spanReporter(amazonXrayOrGoogleStackdrive),则应通过设置 Tracing.Builder.supportsJoin(false) 禁用 join。这样做会强制 Tracer.joinSpan() 上的新子 Span 。

54.1.4 实现传播

TraceContext.Extractor<C> 由 Propagation.Factory 插件实现。在内部,此代码创建联合类型 TraceContextOrSamplingFlags,其中包含以下内容之一:
* TraceContext(如果存在跟踪和范围ID)。
* TraceIdContext (如果存在跟踪 ID,但不存在 Span ID)。
* SamplingFlags (如果没有标识符)。

一些传播实现将额外的数据从提取点(例如,读取传入头)传输到注入点(例如,写入传出头)。例如,它可能携带一个请求 ID。当实现有额外的数据时,它们会按如下方式处理它:
* 如果提取了一个 TraceContext,则将额外的数据添加为 TraceContext.extra()。
* 否则,将其添加为 TraceContextOrSamplingFlags.extra(),Tracer.nextSpan 将处理它。