1. 简介
Spring Cloud Sleuth 实现了为 Spring Cloud 的分布式追踪解决方案。
1.1. Terminology
Spring Cloud Sleuth 采用 Dapper 的术语。
跨度:工作单位。例如,发送 RPC 是新的跨度,向 RPC 发送响应也是跨度。 跨度由跨度的唯一 64 位 ID 和其包含的跟踪的另一个 64 位 ID 唯一标识。 跨度还有其他数据,如描述、带时间戳的事件、键值注释(标记)、引起它们的跨度的 ID,以及进程 ID(通常为 IP 地址)。
可以开始和停止跨度,它们会跟踪其计时信息。</p><p>一旦创建了一个跨度,就必须在将来某个时候停止它。
初始跨度开始跟踪时,该跨度称为 |
强**调:一组形成树状结构的跨度。
例如,如果您运行分布式大型数据存储,则跟踪可能由PUT请求组成。
注解:用于记录时间上的事件。通过使用Brave注器,我们不再需要为Zipkin设置特殊的事件来了解客户端和服务器是谁,请求从哪里开始,以及它在哪里结束。然而,为了学习的目的,我们标记这些事件,以强调发生了什么类型的行动。
-
cs: 客户端发送。客户端已发出请求。此注解指示该跨度的开始。
-
sr: 服务器接收到:服务器端已经接收到请求并开始处理它。从这个时间戳减去
cs时间戳就可以得出网络延迟。 -
ss: 已发送至服务器。在请求处理完成(将响应发送回客户端)时进行注释。 从该时间戳减去
sr时间戳可显示服务器端处理请求所需的时间。 -
cr: 已接收客户端。表示跨度结束。 客户端已成功从服务器端接收响应。将此时间戳减去
cs,即可得出客户端从服务器接收响应所需的全部时间。
下图显示了Span 和 Trace 在系统中的样子,以及 Zipkin 注解:
音符的每种颜色代表一个音程(共有七个音程——从A到G)。
考虑以下音符:
Trace Id = X
Span Id = D
Client Sent
此备注表明当前跨度的跟踪 ID设置为X,跨度 ID设置为D。此外,事件Client Sent已经发生。
下图显示了跨度的父-子关系:<br>
1.2. 目的
下述章节参考了前图中的示例。
1.2.1. 使用 Zipkin 的分布式跟踪
此示例包含七个 span。如果您转到 Zipkin 中的 traces,可以在第二个 trace 中看到此数字,如以下图像所示:
但是,如果您选择特定的跟踪,则可以看到四个跨度,如以下图像所示:
| 选择特定跟踪时,您会看到合并后的跨度。</p><p>这意味着,如果使用 Server Received 和 Server Sent 或 Client Received 和 Client Sent 注解向 Zipkin 发送了两个跨度,则它们将显示为单个跨度。 |
在这种情况下,为什么七个span和四个span之间存在差异?
-
一个 span 来自
http:/startspan。它具有服务器接收(sr)和服务器发送(ss)注释。 -
两个跨度来自从
service1到service2的RPC调用到http:/foo端点。
客户端发送 (cs) 和客户端接收 (cr) 事件发生在service1端。
服务器端接收 (sr) 和服务器端发送 (ss) 事件发生在service2端。
这两个跨度构成一个与RPC调用相关的逻辑跨度。 -
两个跨度来自从
service2到service3端点的RPC调用。
客户端发送(cs)和客户端接收(cr)事件发生在service2侧。
服务器接收(sr)和服务器发送(ss)事件发生在service3侧。
这两个跨度构成一个与RPC调用相关的逻辑跨度。 -
两个跨度来自从
service2到service4的RPC调用到http:/baz端点。
客户端发送 (cs) 和客户端接收 (cr) 事件发生在service2端。
服务器端接收 (sr) 和服务器端发送 (ss) 事件发生在service4端。
这两个跨度构成一个与RPC调用相关的逻辑跨度。
因此,如果我们计算物理跨度,我们有从 http:/start 开始的一个跨度,从 service1 调用到 service2 的两个跨度,从 service2 调用到 service3 的两个跨度,以及从 service2 调用到 service4 的两个跨度。总共有七个跨度。
逻辑上,我们看到四个总跨度的信息,因为我们有一个与传入请求相关的跨度service1和三个与RPC调用相关的跨度。
1.2.2. 可视化错误
Zipkin 允许您可视化跟踪中的错误。当抛出异常且未被捕获时,我们会在跨度上设置适当的标记,以便 Zipkin 正确着色。在跟踪列表中,您可以看到一个红色的跟踪。这是因为抛出了异常。
如果您点击该跟踪,您会看到类似的图片,如下所示:
然后,如果您单击其中一个跨度,您将看到以下内容
该 span 显示了错误的原因以及与之相关的整个堆栈跟踪。
1.2.3. 使用Brave进行分布式跟踪
从版本 2.0.0 开始,Spring Cloud Sleuth 使用 Brave 作为跟踪库。因此,Sleuth 不再负责存储上下文,而是将这项工作委托给 Brave。
由于Sleuth的命名和标记约定与Brave不同,我们决定从现在开始遵循Brave的约定。但是,如果你想使用旧版的Sleuth方法,可以将spring.sleuth.http.legacy.enabled属性设置为true。
1.2.5. 日志相关性
当使用grep通过扫描跟踪ID(例如2485ec27856c56f4)来读取这四个应用程序的日志时,您会得到类似于以下内容的输出:
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]]
你可以使用 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}" }
}
date {
match => ["timestamp", "ISO8601"]
}
mutate {
remove_field => ["timestamp"]
}
}
| 如果您想将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}" }
}
date {
match => ["timestamp", "ISO8601"]
}
mutate {
remove_field => ["timestamp"]
}
}
JSON格式的Logback与Logstash
通常情况下,您并不希望将日志存储在文本文件中,而是存储在一个JSON文件中,以便Logstash能够立即提取。
为此,您需要执行以下操作(为了便于阅读,我们使用groupId:artifactId:version表示法传递依赖项)。
依赖设置
-
确保 Logback 在类路径上(
ch.qos.logback:logback-core)。 -
添加 Logstash Logback 编码。例如,要使用版本
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>
{
"timestamp": "@timestamp",
"severity": "%level",
"service": "${springAppName:-}",
"trace": "%X{traceId:-}",
"span": "%X{spanId:-}",
"baggage": "%X{key:-}",
"pid": "${PID:-}",
"thread": "%thread",
"class": "%logger{40}",
"rest": "%message"
}
</pattern>
</pattern>
</providers>
</encoder>
</appender>
<springProfile name="logzio">
<!-- Use shutdownHook so that we can close gracefully and finish the log drain -->
<shutdownHook class="ch.qos.logback.core.hook.DelayingShutdownHook"/>
<appender name="LogzioLogbackAppender" class="io.logz.logback.LogzioLogbackAppender">
<token>${LOGZ_IO_API_TOKEN}</token>
<logzioUrl>https://listener.logz.io:8071</logzioUrl>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<debug>true</debug>
<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
<providers>
<timestamp>
<timeZone>UTC</timeZone>
</timestamp>
<pattern>
<pattern>
{
"timestamp": "@timestamp",
"severity": "%level",
"service": "${springAppName:-}",
"trace": "%X{traceId:-}",
"span": "%X{spanId:-}",
"baggage": "%X{key:-}",
"pid": "${PID:-}",
"thread": "%thread",
"class": "%logger{40}",
"rest": "%message"
}
</pattern>
</pattern>
</providers>
</encoder>
</appender>
<root level="info">
<!-- IMPORTANT: make sure to include this line, otherwise the appender won't be used -->
<appender-ref ref="LogzioLogbackAppender"/>
</root>
</springProfile>
<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文件中。 -
已注释两个额外的appenders:console 和 standard log file.
-
与上一节中介绍的日志记录模式相同。
如果您使用自定义的 logback-spring.xml,则必须通过spring.application.name传递bootstrap而不是在application属性文件中进行传递。否则,您的自定义 logback 文件无法正确读取该属性。 |
1.2.6.传播跨度上下文
跨进程边界的任何子跨度都必须传播的跨度上下文是必须传递的状态。 跨度上下文的一部分是行李。跟踪和跨度 ID 是跨度上下文的必要部分。 行李是可选的部分。
行李是一种存储在跨度上下文中的键:值对。 行李随跟踪一起传递,并附加到每个跨度上。 如果HTTP标头以baggage-为前缀,Spring Cloud Sleuth会理解该标头与行李相关;对于消息传递,则以baggage_开头。
| 行李件数和大小目前没有限制。但是请记住,太多行李会降低系统吞吐量或增加RPC延迟。在极端情况下,过多的行李可能会由于超过传输层消息或头容量而使应用程序崩溃。 |
以下示例显示如何在跨度上设置行李:
Span initialSpan = this.tracer.nextSpan().name("span").start();
ExtraFieldPropagation.set(initialSpan.context(), "foo", "bar");
ExtraFieldPropagation.set(initialSpan.context(), "UPPER_CASE", "someValue");
行李标签与跨度标签
行李会随追踪信息一起传递(每个子跨度都包含其父跨度的行李信息)。<br/>Zipkin 对行李毫不知情,也不会接收这些信息。
| 从 Sleuth 2.0.0 开始,您必须在项目配置中显式传递行李键名称。有关该设置的更多详细信息,请点击这里 |
标签被附加到特定的跨度上。换句话说,它们仅对该特定跨度显示。<br/>但是,您可以按标签搜索以找到跟踪信息,假设存在具有所搜索标签值的跨度。
如果您希望根据行李查找跨度,应在根跨度中添加相应的标记条目。
| 必须在范围内使用 span。 |
以下列表显示了使用行李箱的集成测试:
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"));
1.3. 将Sleuth添加到项目中
本节介绍如何使用Maven或Gradle将Sleuth添加到您的项目中。
为了确保您的应用程序名称在 Zipkin 中正确显示,请在 bootstrap.yml 中设置 spring.application.name 属性。 |
1.3.1. 仅 Sleuth(日志关联)
如果您只想使用 Spring Cloud Sleuth 而不使用 Zipkin 集成,请将 spring-cloud-starter-sleuth 模块添加到您的项目中。
以下示例显示了如何使用Maven添加Sleuth:
<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:
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。 |
1.3.2. Sleuth 通过 HTTP 与 Zipkin 的集成
如果您既想使用 Sleuth 又想使用 Zipkin,请添加 spring-cloud-starter-zipkin 依赖。
下面的例子显示了如何为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执行此操作:
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。 |
1.3.3. 使用 RabbitMQ 或 Kafka 的 Sleuth 和 Zipkin
如果您想使用 RabbitMQ 或 Kafka 而不是 HTTP,请添加 spring-rabbit 或 spring-kafka 依赖。
默认目标名称是 zipkin。
如果使用 Kafka,您必须相应地设置属性spring.zipkin.sender.type:
spring.zipkin.sender.type: kafka
spring-cloud-sleuth-stream 已弃用且与这些目标不兼容。 |
如果您想要使用Sleuth配合RabbitMQ,请添加spring-cloud-starter-zipkin和spring-rabbit依赖。
以下示例显示了如何为Gradle执行此操作:
<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 依赖。 |
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 依赖。 |
1.4. 覆盖Zipkin的自动配置
从版本 2.1.0 开始,Spring Cloud Sleuth 支持将跟踪信息发送到多个跟踪系统。
为了实现此功能,每个跟踪系统都需要具有一个 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);
}
}
}
