50. Introduction

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

50.1 Terminology

Spring Cloud Sleuth 借用了Dapper's术语。

Span :基本工作单元。例如,发送 RPC 是新的 Span,就像发送响应到 RPC 一样。Span 由 Span 的唯一 64 位 ID 和 Span 所属的跟踪的另一个 64 位 ID 标识。跨区还具有其他数据,例如描述,带有时间戳的事件,键值注解(标签),引起 Span 的跨区 ID 和进程 ID(通常为 IP 地址)。

Span 可以启动和停止,并且可以跟踪其时序信息。创建 Span 后,您必须在将来的某个时间点将其停止。

Tip

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

迹线: 一组 Span 形成树状结构。例如,如果您运行分布式大数据存储,则跟踪可能是由PUT请求形成的。

Comments: 用于及时记录事件的存在。使用Brave工具,我们不再需要为Zipkin设置特殊事件来了解 Client 端和服务器是谁,请求在何处开始以及在何处结束。但是,出于学习目的,我们标记这些事件以突出显示发生了哪种操作。

  • cs :Client 已发送。Client 提出了要求。此 Comments 指示 Span 的开始。

  • sr :接收到服务器:服务器端收到了请求并开始处理它。从此时间戳中减去cs时间戳可显示网络延迟。

  • ss :服务器已发送。在请求处理完成时添加 Comments(当响应被发送回 Client 端时)。从该时间戳中减去sr时间戳将显示服务器端处理请求所需的时间。

  • cr :已收到 Client。表示 Span 结束。Client 端已成功收到服务器端的响应。从该时间戳中减去cs时间戳可显示出 Client 端从服务器接收响应所需的整个时间。

下图显示了系统中 SpanTrace 以及 Zipkin 注解的外观:

跟踪信息传播

音符的每种颜色表示一个 Span(共有七个 Span-从 AG )。请考虑以下注意事项:

Trace Id = X
Span Id = D
Client Sent

此 Comments 表示当前 Span 已将“跟踪 ID”设置为“ X ”,而“SpanID”设置为 D **。另外,发生了Client Sent事件。

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

亲子关系

50.2 Purpose

以下各节引用上图中显示的示例。

50.2.1 使用 Zipkin 进行分布式跟踪

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

Traces

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

跟踪信息传播

Note

选择特定轨迹时,会看到合并的 Span。这意味着,如果有两个 Span 发送到 Zipkin,且带有“服务器已接收和服务器已发送”或“Client 端已接收和 Client 端已发送”Comments,则它们将显示为单个 Span。

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

  • 一个 Span 来自http:/startSpan。它具有服务器已接收(sr)和服务器已发送(ss)注解。

  • service1service2http:/foo端点的 RPC 调用有两个范围。Client 端已发送(cs)和 Client 端已接收(cr)事件发生在service1端。服务器已接收(sr)和服务器已发送(ss)事件发生在service2一侧。这两个 Span 形成一个与 RPC 调用相关的逻辑 Span。

  • service2service3http:/bar端点的 RPC 调用有两个范围。Client 端已发送(cs)和 Client 端已接收(cr)事件发生在service2端。服务器已收到(sr)和服务器已发送(ss)事件发生在service3一侧。这两个 Span 形成一个与 RPC 调用相关的逻辑 Span。

  • service2service4http:/baz端点的 RPC 调用有两个范围。Client 端已发送(cs)和 Client 端已接收(cr)事件发生在service2端。服务器已接收(sr)和服务器已发送(ss)事件发生在service4一侧。这两个 Span 形成一个与 RPC 调用相关的逻辑 Span。

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

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

50.2.2 可视化错误

Zipkin 使您可以可视化跟踪中的错误。当引发异常但未捕获异常时,我们在 Span 上设置了适当的标签,然后 Zipkin 可以对其进行正确着色。您可以在迹线列表中看到一条红色的迹线。之所以出现,是因为引发了异常。

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

Error Traces

如果您再单击其中一个 Span,则会看到以下内容

错误跟踪信息传播

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

50.2.3Brave 的分布式跟踪

从版本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 服务”图标可实时观看!

Zipkin 部署在 Pivotal Web Services 上

点击此处观看直播!

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

Dependencies

图 50.2. 单击“ Pivotal Web 服务”图标可实时观看!

Zipkin 部署在 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]]

如果您使用日志汇总工具(例如KibanaSplunk等),则可以对发生的事件进行排序。来自 Kibana 的示例类似于下图:

与 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

如果要将 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}" }
       }
}

带 Logstash 的 JSON Logback

通常,您不想将日志存储在文本文件中,而是存储在 Logstash 可以立即选择的 JSON 文件中。为此,您必须执行以下操作(出于可读性考虑,我们以groupId:artifactId:version表示法传递依赖项)。

Dependencies Setup

  • 确保 Logback 位于 Classpath(ch.qos.logback:logback-core)上。

  • 添加 Logstash Logback 编码。例如,要使用版本4.6,请添加net.logstash.logback:logstash-logback-encoder:4.6

Logback Setup

考虑以下示例的 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文件中。

  • Comments 了两个附加的附加程序:控制台和标准日志文件。

  • 具有与上一节介绍的记录模式相同的记录模式。

Note

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

50.2.6 传播范围上下文

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

Baggage 是存储在 span 上下文中的一组 key:value 对。Baggage 与踪迹一起旅行,并附着在每个 Span 上。 Spring Cloud Sleuth 理解,如果 HTTPHeaders 以baggage-作为前缀,并且对于消息传递,Headers 以baggage_开头,则 Headers 与 Baggage 相关。

Tip

当前对 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 都包含其 parent 的 Baggage)。 Zipkin 不了解 Baggage,也不会收到该信息。

Tip

从 Sleuth 2.0.0 开始,您必须在项目配置中显式传递 Baggage 密钥名称。详细了解该设置here

标签被附加到特定范围。换句话说,它们仅针对该特定 Span 显示。但是,您可以按标签搜索以找到轨迹,前提是存在具有所搜索标签值的 Span。

如果您希望能够基于 Baggage 查找 Span,则应在根 Span 中添加相应的条目作为标签。

Tip

范围必须在范围内。

以下清单显示了使用 Baggage 的集成测试:

The setup.

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

The code.

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

50.3 在项目中添加 Sleuth

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

Tip

为确保您的应用程序名称正确显示在 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 添加依赖项 Management,这样就不必自己 Management 版本。
  • (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 添加依赖项 Management,这样就不必自己 Management 版本。
  • (2) 将依赖项添加到spring-cloud-starter-sleuth

50.3.2 通过 HTTP 与 Zipkin 一起 Sleuth

如果您同时需要 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 添加依赖项 Management,这样就不必自己 Management 版本。
  • (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 添加依赖项 Management,这样就不必自己 Management 版本。
  • (2) 将依赖项添加到spring-cloud-starter-zipkin

50.3.3 通过 RabbitMQ 或 Kafka 与 Zipkin 一起 Sleuth

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

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

spring.zipkin.sender.type: kafka

Warning

spring-cloud-sleuth-stream已弃用,并且与这些目的地不兼容。

如果要让 Sleuth 优于 RabbitMQ,请添加spring-cloud-starter-zipkinspring-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 添加依赖项 Management,这样就不必自己 Management 版本。
  • (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 添加依赖项 Management,这样就不必自己 Management 版本。
  • (2) 将依赖项添加到spring-cloud-starter-zipkin。这样,所有嵌套的依赖项都将被下载。
  • (3) 要自动配置 RabbitMQ,请添加spring-rabbit依赖项。

50.4 覆盖 Zipkin 的自动配置

从 2.1.0 版开始,Spring Cloud Sleuth 支持将跟踪发送到多个跟踪系统。为了使其正常工作,每个跟踪系统都需要具有Reporter<Span>Sender。如果要覆盖提供的 bean,则需要给它们指定一个特定的名称。为此,您可以分别使用ZipkinAutoConfiguration.REPORTER_BEAN_NAMEZipkinAutoConfiguration.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);
		}

	}

}