52. 特性

  • 将 trace 和 span ID 添加到 Slf4J MDC,这样你就可以在日志聚合器中从给定的 trace 或 span 提取所有日志,如下示例日志所示:

    2016-02-02 15:30:57.902  INFO [bar,6bfd228dc00d216b,6bfd228dc00d216b,false] 23030 --- [nio-8081-exec-3] ...
    2016-02-02 15:30:58.372 ERROR [bar,6bfd228dc00d216b,6bfd228dc00d216b,false] 23030 --- [nio-8081-exec-3] ...
    2016-02-02 15:31:01.936  INFO [bar,46ab0d418373cbc9,46ab0d418373cbc9,false] 23030 --- [nio-8081-exec-4] ...

    注意 MDC 中的 [appname,traceId,spanId,exportable] 条目:

    • spanId: 发生的特定操作的 ID。
    • appname: 记录该 Span 的应用程序的名称。
    • traceId: 包含 Span 的延迟图的 ID。
    • exportable: 日志是否应导出到 Zipkin。你希望什么时候不能导出 Span ?当希望将某个操作包装在一个 Span 内并只将其写入日志时。
  • 提供对常见分布式跟踪数据模型的抽象:跟踪、 Span (形成 DAG)、注解和 key-value 注解。Spring CloudS leuth 基于 HTrace,但与 Zipkin(Dapper)兼容。
  • Sleuth 记录时间信息以帮助进行延迟分析。通过使用 sleuth,可以查明应用程序中延迟的原因。
  • 写 Sleuth 的目的是为了避免日志过多,并且不会导致生产应用程序崩溃。为此,Sleuth:

    • 在带内和带外传播关于调用图的结构数据。
    • 包括对 HTTP 等层的独立检测。
    • 包括用于管理卷的采样策略。
    • 可以向 Zipkin 系统报告以进行查询和可视化。
  • 工具从 Spring 应用程序(servlet 过滤器、异步端点、rest 模板、定时操作、消息通道、Zuul 过滤器和 Feign 客户端)的公共入口和出口点。
  • Sleuth 包含默认逻辑,用于连接跨越 HTTP 或消息传递边界的跟踪。例如,HTTP 传播在 Zipkin 兼容的请求头上工作。
  • Sleuth 可以在进程之间传播上下文(也称为 baggage)。因此,如果你在一个 Span 上设置了一个 baggage 元素,它将通过 HTTP 或消息传递被发送到下游的其他进程。
  • 提供一种创建或继续 Span 的方法,并通过注解添加标记和日志。
  • 如果 spring-cloud-sleuth-zipkin 在类路径上,应用程序将生成并收集与 Zipkin 兼容的跟踪。默认情况下,它通过 HTTP 将它们发送到本地主机(端口 9411)上的 Zipkin 服务器。你可以通过设置 spring.zipkin.baseUrl 来配置服务的位置。

    • 如果你依赖于 spring-rabbit,那么应用程序将跟踪发送到 RabbitMQ 代理,而不是 HTTP。
    • 如果你依赖于 spring-kafka,并设置 spring.zipkin.sender.type: kafka,那么应用程序将跟踪发送到 Kafka 代理而不是 HTTP。
[Caution] 警告

spring-cloud-sleuth-stream 已弃用,不应再使用。

  • Spring Cloud Sleuth 与 OpenTracing 兼容。
[Important] 重点

如果使用 Zipkin,请通过设置 spring.sleuth.sampler.probability 来配置导出 Span 的概率(默认值:0.1,即 10%)。否则,可能会认为 Sleuth 不起作用,因为它省略了一些 Span 。

[Note] Note

总是设置 SLF4J MDC,并且根据前面的示例,logback 用户会立即在日志中看到跟踪和 Span ID。其他日志记录系统必须配置自己的格式化程序以获得相同的结果。默认值如下:logging.pattern.level 设置为 %5p [${spring.zipkin.service.name:${spring.application.name:-}},%X{X-B3-TraceId:-},%X{X-B3-SpanId:-},%X{X-Span-Export:-}](这是 logback 用户的 Spring Boot 特性)。如果不使用 SLF4J,则不会自动应用此模式。

52.1 Brave 简介

[Important] 重点

从 2.0.0 版开始,Spring Cloud Sleuth 使用了 Brave 作为跟踪库。为了方便,我们在这里嵌入了 Brave 的部分文档。

[Important] 重点

在大多数情况下,只需要使用 Sleuth 提供的对于 Brave 的 Tracer 或 SpanCustomizer bean。下面的文档对什么是 Brave 以及它是如何工作的有一个很重要的概述。

Brave 是一个库,用于捕获和向 Zipkin 报告分布式操作的延迟信息。大多数用户不直接使用 Brave。他们使用库或框架,而不是以 Brave 的名义。

这个模块包括一个跟踪程序,它创建并连接 Span 范围,对潜在分布式工作的延迟进行建模。它还包括在网络边界上传播跟踪上下文的库(例如,使用 HTTP 头)。

52.1.1 Tracing

最重要的是,你需要一个 brave.Tracer,配置成向 Zipkin 报告。

以下示例安装程序通过 HTTP(而不是 Kafka)将跟踪数据( Span )发送到 Zipkin:

class MyClass {

    private final Tracer tracer;

    // Tracer will be autowired
    MyClass(Tracer tracer) {
        this.tracer = tracer;
    }

    void doSth() {
        Span span = tracer.newTrace().name("encode").start();
        // ...
    }
}
[Important] 重点

如果你的范围包含的名称超过 50 个字符,那么该名称将被截断为 50 个字符。你的名字必须明确具体。过大的名称会导致延迟问题,有时甚至会抛出异常。

跟踪程序创建和连接跨范围的模型潜在分布式工作的延迟。它可以使用采样来减少过程中的开销,减少发送到 Zipkin 的数据量,或者两者兼而有之。

跟踪报告数据在完成时返回到 Zipkin,如果未完成,则不执行任何操作。开始一个范围后,你可以注解感兴趣的事件,或者添加包含详细信息或查找键的标记。

Span 具有一个上下文,其中包含将 Span 放置在树中表示分布式操作的正确位置的跟踪标识符。

52.1.2 局部跟踪

跟踪从不离开进程的代码时,请在 Span 内运行它。

@Autowired Tracer tracer;

// Start a new trace or a span within an existing trace representing an operation
ScopedSpan span = tracer.startScopedSpan("encode");
try {
  // The span is in "scope" meaning downstream code such as loggers can see trace IDs
  return encoder.encode();
} catch (RuntimeException | Error e) {
  span.error(e); // Unless you handle exceptions, you might not know the operation failed!
  throw e;
} finally {
  span.finish(); // always finish the span
}

当需要更多功能或更精细的控制时,请使用 Span 类型:

@Autowired Tracer tracer;

// Start a new trace or a span within an existing trace representing an operation
Span span = tracer.nextSpan().name("encode").start();
// Put the span in "scope" so that downstream code such as loggers can see trace IDs
try (SpanInScope ws = tracer.withSpanInScope(span)) {
  return encoder.encode();
} catch (RuntimeException | Error e) {
  span.error(e); // Unless you handle exceptions, you might not know the operation failed!
  throw e;
} finally {
  span.finish(); // note the scope is independent of the span. Always finish a span.
}

以上两个例子在完成时报告的 Span 完全相同!

在过去的例子中, Span 将是一个新的根 Span ,或是一个现有跟踪中的下一个子级。

52.1.3 自定义 Span

一旦有了 Span ,就可以向其添加标记。标记可以用作查找键或详细信息。例如,可以使用运行时版本添加标记,如下例所示:

span.tag("clnt/finagle.version", "6.36.0");

当向第三方公开定制 Span 的能力时,首选 brave.SpanCustomizer,而不是 brave.Span。前者更容易理解和测试,并且不会用 Span 生命周期钩子吸引用户。

interface MyTraceCallback {
  void request(Request request, SpanCustomizer customizer);
}

由于 brave.Span 实现了 brave.SpanCustomizer,因此可以将其传递给用户,如下例所示:

for (MyTraceCallback callback : userCallbacks) {
  callback.request(request, span);
}

52.1.4 隐式查找当前 Span

有时,你不知道跟踪是否正在进行,也不希望用户执行空检查。brave.CurrentSpanCustomizer 通过将数据添加到正在进行或删除的任何 Span 来处理此问题,如下面的示例所示:

// The user code can then inject this without a chance of it being null.
@Autowired SpanCustomizer span;

void userCode() {
  span.annotate("tx.started");
  ...
}

52.1.5 RPC 跟踪

[Tip] Tip

在使用你自己的 RPC 工具之前,请检查此处编写的工具和 Zipkin 的列表。

RPC 跟踪通常由拦截器自动完成。在后台,他们添加与他们在 RPC 操作中的角色相关的标记和事件。

以下示例显示如何添加客户端 Span :

@Autowired Tracing tracing;
@Autowired Tracer tracer;

// before you send a request, add metadata that describes the operation
span = tracer.nextSpan().name(service + "/" + method).kind(CLIENT);
span.tag("myrpc.version", "1.0.0");
span.remoteServiceName("backend");
span.remoteIpAndPort("172.3.4.1", 8108);

// Add the trace context to the request, so it can be propagated in-band
tracing.propagation().injector(Request::addHeader)
                     .inject(span.context(), request);

// when the request is scheduled, start the span
span.start();

// if there is an error, tag the span
span.tag("error", error.getCode());
// or if there is an exception
span.error(exception);

// when the response is complete, finish the span
span.finish();

单向跟踪

有时,你需要为异步操作建模,其中存在请求但没有响应。在正常的 RPC 跟踪中,使用 span.finish() 指示已收到响应。在单向跟踪中,将使用 span.flush(),因为不需要响应。

下面的示例显示客户端如何建模单向操作:

@Autowired Tracing tracing;
@Autowired Tracer tracer;

// start a new span representing a client request
oneWaySend = tracer.nextSpan().name(service + "/" + method).kind(CLIENT);

// Add the trace context to the request, so it can be propagated in-band
tracing.propagation().injector(Request::addHeader)
                     .inject(oneWaySend.context(), request);

// fire off the request asynchronously, totally dropping any response
request.execute();

// start the client side and flush instead of finish
oneWaySend.start().flush();

以下示例显示服务器如何处理单向操作:

@Autowired Tracing tracing;
@Autowired Tracer tracer;

// pull the context out of the incoming request
extractor = tracing.propagation().extractor(Request::getHeader);

// convert that context to a span which you can name and add tags to
oneWayReceive = nextSpan(tracer, extractor.extract(request))
    .name("process-request")
    .kind(SERVER)
    ... add tags etc.

// start the server side and flush instead of finish
oneWayReceive.start().flush();

// you should not modify this span anymore as it is complete. However,
// you can create children to represent follow-up work.
next = tracer.newSpan(oneWayReceive.context()).name("step2").start();