50. 简介

Spring Cloud Sleuth 为 Spring Cloud 实现了一个分布式跟踪解决方案。

50.1 术语

Spring Cloud Sleuth 借用了 Dapper 的术语。

Span: 基本工作单位。例如,发送一个 RPC 是一个新的范围,发送一个对 RPC 的响应也是如此。 Span 由 Span 的唯一 64 位 ID 和 Span 所属跟踪的另一个 64 位 ID 标识。 Span 还具有其他数据,例如描述、时间戳事件、键值注释(标记)、导致这些数据的 Span 的 ID 以及进程 ID(通常为 IP 地址)。

Span 可以开始和停止,并且它们可以跟踪它们的时间信息。一旦你创建了一个 Span ,你必须在将来的某个时候停止它。

[Tip] Tip

开始跟踪的初始 Span 称为 root span。该 Span 的 ID 值等于跟踪 ID。

Trace: 形成树形结构的一组 Span 。例如,如果你运行一个分布式大数据存储,那么可能会通过一个 PUT 请求形成跟踪。

Annotation: 用于及时记录事件的存在。使用 Brave 工具,我们不再需要为 Zipkin 设置特殊事件来了解客户端和服务器是谁、请求的开始位置和结束位置。然而,为了学习的目的,我们标记这些事件来突出发生了什么样的行为。

  • cs: Client Sent。客户端发出了请求。此注解指示 Span 的起点。
  • sr: Server Received: 服务器端收到请求并开始处理它。从这个时间戳中减去 cs 时间戳会显示网络延迟。
  • ss: Server Sent。在请求处理完成时加上注释(当响应被发送回客户端时)。从这个时间戳中减去 sr 时间戳可以显示服务器端处理请求所需的时间。
  • cr: Client Received。表示 Span 的终点。客户端已成功接收服务器端的响应。从这个时间戳中减去 cs 时间戳可以显示客户端从服务器接收响应所需的全部时间。

下图展示了系统中的 Span 和 Trace,以及 Zipkin 注解:

Trace Info propagation

便条签的每种颜色都表示一个 Span (有七个 Span - 从 A 到 G)。请考虑以下注意事项:

Trace Id = X
Span Id = D
Client Sent

此说明表明当前 Span 的 Trace Id 设置为 X,而 Span Id 设置为 D。此外,Client Sent 的事件也发生了。

下图显示了 Span 的父子关系:

Parent child relationship

50.2 目的

以下各节参考上图中所示的示例。

50.2.1 Zipkin 分布式跟踪

这个例子有七个 Span 。如果你转到 Zipkin 中的跟踪,可以在第二个跟踪中看到这个数字,如下图所示:

Traces

但是,如果选择特定的跟踪,则可以看到四个 Span ,如下图所示:

Traces Info propagation
[Note] Note

当你选择一个特定的跟踪时,会看到合并的 Span 。这意味着,如果有两个 Span 发送到 Zipkin,服务器接收到,服务器发送到,客户接收到,客户发送注释,那么它们将显示为一个 Span 。

在这种情况下,为什么七个和四个 Span 有区别?

  • 一个 Span 来自 http:/start Span 。它让服务器接收(sr)和服务器发送(ss)注释。
  • 从 service1 到 service2 到 http:/foo 端点的 RPC 调用有两个 Span 。客户端发送(cs)和客户端接收(cr)事件发生在 service1 端。服务器接收(sr)和服务器发送(ss)事件发生在 service2 端。这两个 Span 构成了一个与 RPC 调用相关的逻辑 Span 。
  • 从 service2 到 service3 到 http:/bar 端点的 RPC 调用有两个 Span 。客户端发送(cs)和客户端接收(cr)事件发生在 service2 端。服务器接收(sr)和服务器发送(ss)事件发生在 service3 端。这两个 Span 构成了一个与 RPC 调用相关的逻辑 Span 。
  • 从 service2 到 service4 到 http:/baz 端点的 RPC 调用有两个 Span 。客户端发送(cs)和客户端接收(cr)事件发生在 service2 端。服务器接收(sr)和服务器发送(ss)事件发生在 service4 端。这两个 Span 构成了一个与 RPC 调用相关的逻辑 Span 。

因此,如果计算物理 Span ,我们有一个来自 http:/start,两个来自 service1 调用 service2,两个来自 service2 调用 service3,两个来自 service2 调用 service4。总之,我们共有七个 Span 。

从逻辑上讲,我们看到了四个总 Span 的信息,因为我们有一个 Span 与传入的 service1 请求相关,还有三个 Span 与 RPC 调用相关。

50.2.2 可视化错误

Zipkin 可以让你在跟踪中可视化错误。当一个异常被抛出并且没有被捕获时,我们在 Span 上设置了适当的标记,然后 Zipkin 可以正确地着色。可以在记录道列表中看到一条红色的记录道。这是因为引发了异常。

如果单击该跟踪,则会看到类似的图片,如下所示:

Error Traces

如果单击其中一个 Span ,将看到以下内容

Error Traces Info propagation

该 Span 显示了错误的原因以及与之相关的整个堆栈跟踪。

50.2.3 使用 Brave 的分布式追踪

从 2.0.0 版开始,Spring Cloud Sleuth 使用了 Brave 作为跟踪库。因此,Sleuth 不再负责存储上下文,而是将工作委托给 Brave。

由于 Sleuth 的命名和标记约定不同于 Brave,我们决定从现在开始遵循 Brave 的约定。但是,如果要使用传统的 Sleuth 方法,可以将 spring.sleuth.http.legacy.enabled 属性设置为 true。

50.2.4 具体实例

图 50.1. 单击 Pivotal Web Services 图标以实时查看!

Zipkin deployed on Pivotal Web Services

单击这里以实时查看!

Zipkin 中的依赖关系图应类似于下图:

Dependencies

Figure 50.2. 单击 Pivotal Web Services 图标以实时查看!

Zipkin deployed on Pivotal Web Services

单击这里以实时查看!

50.2.5 日志相关性

当使用 grep 通过扫描等于(例如)2485ec27856c56f4 的跟踪 ID 来读取这四个应用程序的日志时,你会得到如下类似的输出:

service1.log:2016-02-26 11:15:47.561  INFO [service1,2485ec27856c56f4,2485ec27856c56f4,true] 68058 --- [nio-8081-exec-1] i.s.c.sleuth.docs.service1.Application   : Hello from service1. Calling service2
service2.log:2016-02-26 11:15:47.710  INFO [service2,2485ec27856c56f4,9aa10ee6fbde75fa,true] 68059 --- [nio-8082-exec-1] i.s.c.sleuth.docs.service2.Application   : Hello from service2. Calling service3 and then service4
service3.log:2016-02-26 11:15:47.895  INFO [service3,2485ec27856c56f4,1210be13194bfe5,true] 68060 --- [nio-8083-exec-1] i.s.c.sleuth.docs.service3.Application   : Hello from service3
service2.log:2016-02-26 11:15:47.924  INFO [service2,2485ec27856c56f4,9aa10ee6fbde75fa,true] 68059 --- [nio-8082-exec-1] i.s.c.sleuth.docs.service2.Application   : Got response from service3 [Hello from service3]
service4.log:2016-02-26 11:15:48.134  INFO [service4,2485ec27856c56f4,1b1845262ffba49d,true] 68061 --- [nio-8084-exec-1] i.s.c.sleuth.docs.service4.Application   : Hello from service4
service2.log:2016-02-26 11:15:48.156  INFO [service2,2485ec27856c56f4,9aa10ee6fbde75fa,true] 68059 --- [nio-8082-exec-1] i.s.c.sleuth.docs.service2.Application   : Got response from service4 [Hello from service4]
service1.log:2016-02-26 11:15:48.182  INFO [service1,2485ec27856c56f4,2485ec27856c56f4,true] 68058 --- [nio-8081-exec-1] i.s.c.sleuth.docs.service1.Application   : Got response from service2 [Hello from service2, response from service3 [Hello from service3] and from service4 [Hello from service4]]

如果使用日志聚合工具(如 Kibana、Splunk 和其他工具),则可以对发生的事件进行排序。Kibana 的一个例子类似于下图:

Log correlation with Kibana

如果要使用 Logstash,下面的列展示 Logstash 的 Grok 模式:

filter {
       # pattern matching logback pattern
       grok {
              match => { "message" => "%{TIMESTAMP_ISO8601:timestamp}\s+%{LOGLEVEL:severity}\s+\[%{DATA:service},%{DATA:trace},%{DATA:span},%{DATA:exportable}\]\s+%{DATA:pid}\s+---\s+\[%{DATA:thread}\]\s+%{DATA:class}\s+:\s+%{GREEDYDATA:rest}" }
       }
}
[Note] Note

如果要将 Grok 与 Cloud Foundry 的日志一起使用,则必须使用以下模式:

filter {
       # pattern matching logback pattern
       grok {
              match => { "message" => "(?m)OUT\s+%{TIMESTAMP_ISO8601:timestamp}\s+%{LOGLEVEL:severity}\s+\[%{DATA:service},%{DATA:trace},%{DATA:span},%{DATA:exportable}\]\s+%{DATA:pid}\s+---\s+\[%{DATA:thread}\]\s+%{DATA:class}\s+:\s+%{GREEDYDATA:rest}" }
       }
}

JSON Logback 与 Logstash

通常,你不希望将日志存储在一个文本文件中,而是存储在一个 JSON 文件中,Logstash 可以立即选择该文件。为此,必须执行以下操作(为了可读性,我们在 groupId:artifactId:version 表示法中传递依赖项)。

依赖项设置

  1. 确认 Logback 在类路径上 (ch.qos.logback:logback-core).
  2. 添加 Logstash Logback encode。例如,要使用 4.6 版,请添加 net.logstash.logback:logstash-logback-encoder:4.6。

Logback 设置

请考虑以下日志配置文件(名为 logback-spring.xml)的示例。

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
	<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
	​
	<springProperty scope="context" name="springAppName" source="spring.application.name"/>
	<!-- Example for logging into the build folder of your project -->
	<property name="LOG_FILE" value="${BUILD_FOLDER:-build}/${springAppName}"/>​

	<!-- You can override this to have a custom pattern -->
	<property name="CONSOLE_LOG_PATTERN"
			  value="%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"/>

	<!-- Appender to log to console -->
	<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
		<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
			<!-- Minimum logging level to be presented in the console logs-->
			<level>DEBUG</level>
		</filter>
		<encoder>
			<pattern>${CONSOLE_LOG_PATTERN}</pattern>
			<charset>utf8</charset>
		</encoder>
	</appender>

	<!-- Appender to log to file -->​
	<appender name="flatfile" class="ch.qos.logback.core.rolling.RollingFileAppender">
		<file>${LOG_FILE}</file>
		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
			<fileNamePattern>${LOG_FILE}.%d{yyyy-MM-dd}.gz</fileNamePattern>
			<maxHistory>7</maxHistory>
		</rollingPolicy>
		<encoder>
			<pattern>${CONSOLE_LOG_PATTERN}</pattern>
			<charset>utf8</charset>
		</encoder>
	</appender>
	​
	<!-- Appender to log to file in a JSON format -->
	<appender name="logstash" class="ch.qos.logback.core.rolling.RollingFileAppender">
		<file>${LOG_FILE}.json</file>
		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
			<fileNamePattern>${LOG_FILE}.json.%d{yyyy-MM-dd}.gz</fileNamePattern>
			<maxHistory>7</maxHistory>
		</rollingPolicy>
		<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
			<providers>
				<timestamp>
					<timeZone>UTC</timeZone>
				</timestamp>
				<pattern>
					<pattern>
						{
						"severity": "%level",
						"service": "${springAppName:-}",
						"trace": "%X{X-B3-TraceId:-}",
						"span": "%X{X-B3-SpanId:-}",
						"parent": "%X{X-B3-ParentSpanId:-}",
						"exportable": "%X{X-Span-Export:-}",
						"pid": "${PID:-}",
						"thread": "%thread",
						"class": "%logger{40}",
						"rest": "%message"
						}
					</pattern>
				</pattern>
			</providers>
		</encoder>
	</appender>
	​
	<root level="INFO">
		<appender-ref ref="console"/>
		<!-- uncomment this to have also JSON logs -->
		<!--<appender-ref ref="logstash"/>-->
		<!--<appender-ref ref="flatfile"/>-->
	</root>
</configuration>

该 Logback 配置文件:

  • 以 JSON 格式将应用程序中的信息记录到 build/${spring.application.name}.json 文件中。
  • 注释掉了另外两个附加文件:控制台和标准日志文件。
  • 与上一节中介绍的日志记录模式相同。
[Note] Note

如果使用自定义 logback-spring.xml,则必须在 bootstrap 而不是 application 属性文件中传递 spring.application.name。否则,自定义日志文件无法正确读取属性。

50.2.6 传播 Span 上下文

Span 上下文是必须跨流程边界传播到任何子 Span 的状态。 Span 上下文的一部分是 Baggage。跟踪和 Span ID 是 Span 上下文的必需部分。Baggage 是可选部分。

Baggage 是存储在 Span 上下文中的一组 key:value 对。Baggage 随跟踪一起移动,并附在每个 Span 上。Spring Cloud Sleuth 理解,如果 HTTP 头前面加上 baggage-,那么头是与其相关的,对于消息传递,它从 baggage_ 开始。

[Important] 重点

目前,baggage 项的数量或大小没有限制。但是,请记住,太多会降低系统吞吐量或增加 RPC 延迟。在极端情况下,过多的 baggage 会导致应用程序崩溃,因为超过了传输级别的消息或报头容量。

下面的示例展示在 Span 上设置 baggage:

Span initialSpan = this.tracer.nextSpan().name("span").start();
ExtraFieldPropagation.set(initialSpan.context(), "foo", "bar");
ExtraFieldPropagation.set(initialSpan.context(), "UPPER_CASE", "someValue");

Baggage 与 Span 标签

Baggage 随跟踪移动(每个子 Span 都包含其父 Span 的 baggage)。Zipkin 不知道 baggage,也不接收这些信息。

[Important] 重点

从 Sleuth 2.0.0 开始,你必须在项目配置中明确传递 baggage 键名。在此处阅读有关该设置的更多信息

标签附加到特定 Span 。换言之,它们只针对特定的 Span 呈现。但是,可以通过标记搜索来查找跟踪,假定存在具有搜索标记值的 Span 。

如果你希望能够基于 baggage 查找一个 Span ,那么应该在根 Span 中添加一个相应的条目作为标记。

[Important] 重点

Span 必须在范围内。

以下列展示了使用 baggage 的集成测试:

设置。

spring.sleuth:
  baggage-keys:
    - baz
    - bizarrecase
  propagation-keys:
    - foo
    - upper_case

代码。

initialSpan.tag("foo",
		ExtraFieldPropagation.get(initialSpan.context(), "foo"));
initialSpan.tag("UPPER_CASE",
		ExtraFieldPropagation.get(initialSpan.context(), "UPPER_CASE"));

50.3 向项目中添加 Sleuth

本节介绍如何使用 Maven 或 Gradle 向项目中添加 Sleuth。

[Important] 重点

要确保你的应用程序名正确显示在 Zipkin 中,请在 bootstrap.yml 中设置 spring.application.name 属性。

50.3.1 仅限 Sleuth(日志相关性)

如果你只想使用 Spring Cloud Sleuth 而不使用 Zipkin 集成,请将 spring-cloud-starter-sleuth 模块添加到项目中。

下面的示例展示如何使用 Maven 添加 Sleuth:

Maven. 

<dependencyManagement> 1
      <dependencies>
          <dependency>
              <groupId>org.springframework.cloud</groupId>
              <artifactId>spring-cloud-dependencies</artifactId>
              <version>${release.train.version}</version>
              <type>pom</type>
              <scope>import</scope>
          </dependency>
      </dependencies>
</dependencyManagement>

<dependency> 2
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>

1

我们建议你通过 Spring BOM 添加依赖性管理,这样就不需要自己管理版本。

2

添加 spring-cloud-starter-sleuth 依赖项。

下面的示例展示如何使用 Gradle 添加 Sleuth:

Gradle. 

dependencyManagement { 1
    imports {
        mavenBom "org.springframework.cloud:spring-cloud-dependencies:${releaseTrainVersion}"
    }
}

dependencies { 2
    compile "org.springframework.cloud:spring-cloud-starter-sleuth"
}

1

我们建议你通过 Spring BOM 添加依赖性管理,这样就不需要自己管理版本。

2

添加 spring-cloud-starter-sleuth 依赖项。

50.3.2 通过 HTTP 使用 Sleuth 与 Zipkin

如果同时需要 Sleuth 和 Zipkin,请添加 spring-cloud-starter-zipkin 依赖项。

下面的示例展示了如何使用 Maven 执行此操作:

Maven. 

<dependencyManagement> 1
      <dependencies>
          <dependency>
              <groupId>org.springframework.cloud</groupId>
              <artifactId>spring-cloud-dependencies</artifactId>
              <version>${release.train.version}</version>
              <type>pom</type>
              <scope>import</scope>
          </dependency>
      </dependencies>
</dependencyManagement>

<dependency> 2
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>

1

我们建议你通过 Spring BOM 添加依赖性管理,这样就不需要自己管理版本。

2

添加 spring-cloud-starter-zipkin 依赖项。

下面的示例展示了如何使用 Gradle 执行此操作:

Gradle. 

dependencyManagement { 1
    imports {
        mavenBom "org.springframework.cloud:spring-cloud-dependencies:${releaseTrainVersion}"
    }
}

dependencies { 2
    compile "org.springframework.cloud:spring-cloud-starter-zipkin"
}

1

我们建议你通过 Spring BOM 添加依赖性管理,这样就不需要自己管理版本。

2

添加 spring-cloud-starter-zipkin 依赖项。

50.3.3 在 RabbitMQ 或 Kafka 上使用 Sleuth 与 Zipkin

如果要使用 RabbitMQ 或 Kafka 而不是 HTTP,请添加 spring-rabbit 或 spring-kafka 依赖项。默认目的地名称是 zipkin。

如果使用 Kafka,则必须相应地设置属性 spring.zipkin.sender.type 属性:

spring.zipkin.sender.type: kafka
[Caution] 警告

spring-cloud-sleuth-stream 已弃用,与这些目标不兼容。

如果你希望在 RabbitMQ 上使用 Sleuth,请添加 spring-cloud-starter-zipkin 和 spring-rabbit 依赖项。

下面的示例展示了如何使用 Gradle 执行此操作:

Maven. 

<dependencyManagement> 1
      <dependencies>
          <dependency>
              <groupId>org.springframework.cloud</groupId>
              <artifactId>spring-cloud-dependencies</artifactId>
              <version>${release.train.version}</version>
              <type>pom</type>
              <scope>import</scope>
          </dependency>
      </dependencies>
</dependencyManagement>

<dependency> 2
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
<dependency> 3
    <groupId>org.springframework.amqp</groupId>
    <artifactId>spring-rabbit</artifactId>
</dependency>

1

我们建议你通过 Spring BOM 添加依赖性管理,这样就不需要自己管理版本。

2

添加 spring-cloud-starter-zipkin 依赖项。这样,就可以下载所有嵌套的依赖项。

3

要自动配置 RabbitMQ,请添加 spring-rabbit 依赖项。

Gradle. 

dependencyManagement { 1
    imports {
        mavenBom "org.springframework.cloud:spring-cloud-dependencies:${releaseTrainVersion}"
    }
}

dependencies {
    compile "org.springframework.cloud:spring-cloud-starter-zipkin" 2
    compile "org.springframework.amqp:spring-rabbit" 3
}

1

我们建议你通过 Spring BOM 添加依赖性管理,这样就不需要自己管理版本。

2

添加 spring-cloud-starter-zipkin 依赖项。这样,就可以下载所有嵌套的依赖项。

3

要自动配置 RabbitMQ,请添加 spring-rabbit 依赖项。

50.4 覆盖 Zipkin 的自动配置

Spring Cloud Sleuth 支持从 2.1.0 版开始向多个跟踪系统发送跟踪。为了使其正常工作,每个跟踪系统都需要有一个 Reporter<Span> 和 Sender。如果想要覆盖提供的 bean,你需要给它们一个特定的名称。为此,可以分别使用 ZipkinAutoConfiguration.REPORTER_BEAN_NAME 和 ZipkinAutoConfiguration.SENDER_BEAN_NAME。

@Configuration
protected static class MyConfig {

	@Bean(ZipkinAutoConfiguration.REPORTER_BEAN_NAME)
	Reporter<zipkin2.Span> myReporter() {
		return AsyncReporter.create(mySender());
	}

	@Bean(ZipkinAutoConfiguration.SENDER_BEAN_NAME)
	MySender mySender() {
		return new MySender();
	}

	static class MySender extends Sender {

		private boolean spanSent = false;

		boolean isSpanSent() {
			return this.spanSent;
		}

		@Override
		public Encoding encoding() {
			return Encoding.JSON;
		}

		@Override
		public int messageMaxBytes() {
			return Integer.MAX_VALUE;
		}

		@Override
		public int messageSizeInBytes(List<byte[]> encodedSpans) {
			return encoding().listSizeInBytes(encodedSpans);
		}

		@Override
		public Call<Void> sendSpans(List<byte[]> encodedSpans) {
			this.spanSent = true;
			return Call.create(null);
		}

	}

}