1. 引言
Spring Cloud Sleuth 为 Spring Cloud 实现了分布式跟踪解决方案。
1.1. 术语
Spring Cloud Sleuth 借用了 Dapper 的术语。
Span:基本工作单元。例如,发送 RPC 是一个新的 span,向 RPC 发送响应也是如此。 跨度由跨度的唯一 64 位 ID 和跨度所属的跟踪的另一个 64 位 ID 标识。 Span 还具有其他数据,例如描述、带时间戳的事件、键值注释(标签)、导致它们的 Span 的 ID 和进程 ID(通常是 IP 地址)。
Span 可以启动和停止,并且它们会跟踪其 timing 信息。 创建 Span 后,必须在将来的某个时间点停止它。
启动跟踪的初始范围称为 .ID 的值
等于跟踪 ID。root span |
跟踪:形成树状结构的一组 span。
例如,如果您运行分布式大数据存储,则跟踪可能由请求形成。PUT
注解:用于及时记录事件的存在。使用 Brave 插桩,我们不再需要设置特殊事件 以便 Zipkin 了解客户端和服务器是谁,在哪里 请求开始,以及结束位置。出于学习目的, 但是,我们标记这些事件是为了突出类型事件 发生了操作。
-
cs:客户端已发送。客户端已发出请求。此注释指示 span 的开始。
-
sr: Server Received: 服务端收到请求并开始处理。 从此时间戳中减去时间戳可显示网络延迟。
cs
-
ss:服务器已发送。在请求处理完成时(当响应被发送回客户端时)进行注释。 从此时间戳中减去时间戳可显示服务器端处理请求所需的时间。
sr
-
cr:已接收客户端。表示 span 的结束。 客户端已成功收到来自服务器端的响应。 从此时间戳中减去时间戳可显示客户端从服务器接收响应所需的全部时间。
cs
下图显示了 Span 和 Trace 在系统中的外观,以及 Zipkin 注释:
音符的每种颜色都表示一个跨度(有七个跨度 - 从 A 到 G)。 请考虑以下说明:
Trace Id = X
Span Id = D
Client Sent
此说明表示当前 span 的 Trace Id 设置为 X,Span Id 设置为 D。
此外,事件还发生了。Client Sent
下图显示了 Span 的父子关系的外观:
1.2. 目的
以下部分引用上图中显示的示例。
1.2.1. 使用 Zipkin 进行分布式跟踪
此示例有 7 个 Span。 如果转到 Zipkin 中的跟踪,则可以在第二个跟踪中看到此数字,如下图所示:
但是,如果选择特定跟踪,则可以看到四个范围,如下图所示:
当您选择特定跟踪时,您会看到合并的 span。 这意味着,如果有两个 Span 发送到 Zipkin,其中包含 Server Received 和 Server Sent 或 Client Received 和 Client Sent 注释,则它们将显示为单个 Span。 |
为什么在这种情况下 7 和 4 跨度之间存在差异?
-
一个 span 来自 span。它具有 Server Received () 和 Server Sent () 注释。
http:/start
sr
ss
-
两个 span 来自 RPC 调用 from to to 端点。 Client Sent () 和 Client Received () 事件发生在一侧。 Server Received () 和 Server Sent () 事件发生在一侧。 这两个 span 形成一个与 RPC 调用相关的逻辑 span。
service1
service2
http:/foo
cs
cr
service1
sr
ss
service2
-
两个 span 来自 RPC 调用 from to to 端点。 Client Sent () 和 Client Received () 事件发生在一侧。 Server Received () 和 Server Sent () 事件发生在侧面。 这两个 span 形成一个与 RPC 调用相关的逻辑 span。
service2
service3
http:/bar
cs
cr
service2
sr
ss
service3
-
两个 span 来自 RPC 调用 from to to 端点。 Client Sent () 和 Client Received () 事件发生在一侧。 Server Received () 和 Server Sent () 事件发生在一侧。 这两个 span 形成一个与 RPC 调用相关的逻辑 span。
service2
service4
http:/baz
cs
cr
service2
sr
ss
service4
因此,如果我们计算物理跨度,我们有 1 个 from 、2 个来自 calling 、 2 个来自 calling 和 2 个来自 calling 。总之,我们总共有 7 个 Span。http:/start
service1
service2
service2
service3
service2
service4
从逻辑上讲,我们会看到总共四个 Span 的信息,因为我们有一个与传入请求相关的 Span
to 和三个与 RPC 调用相关的 span。service1
1.2.2. 可视化错误
Zipkin 允许您可视化跟踪中的错误。 当抛出异常但未被捕获时,我们在 span 上设置适当的标签,然后 Zipkin 可以正确地对其进行着色。 您可以在跟踪列表中看到一条红色的跟踪。这是因为引发了异常。
如果单击该跟踪,则会看到类似的图片,如下所示:
如果随后单击其中一个 span,则会看到以下内容
span 显示错误的原因以及与之相关的整个堆栈跟踪。
1.2.3. 使用 Brave 进行分布式跟踪
从 version 开始,Spring Cloud Sleuth 使用 Brave 作为跟踪库。
因此,Sleuth 不再负责存储上下文,而是将工作委托给 Brave。2.0.0
由于 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"]
}
}
使用 Logstash 的 JSON Logback
通常,您不希望将日志存储在文本文件中,而是存储在 Logstash 可以立即选择的 JSON 文件中。
为此,您必须执行以下操作(为了可读性,我们在 notation 中传递依赖项)。groupId:artifactId:version
依赖项设置
-
确保 Logback 位于 Classpath () 上。
ch.qos.logback:logback-core
-
添加 Logstash Logback 编码。例如,要使用 version ,请添加 。
4.6
net.logstash.logback:logstash-logback-encoder:4.6
Logback 设置
考虑以下 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
-
注释掉了两个额外的 appender:console 和 standard log file。
-
具有与上一节中介绍的日志记录模式相同的日志记录模式。
如果使用 自定义 ,则必须在 中传递 ,而不是 属性文件。
否则,您的自定义 logback 文件将无法正确读取该属性。logback-spring.xml spring.application.name bootstrap application |
1.2.6. 传播 Span 上下文
span 上下文是必须跨进程边界传播到任何子 span 的状态。 Span 上下文的一部分是 Baggage。跟踪 ID 和 span ID 是 span 上下文的必需部分。 行李是可选部分。
Baggage 是存储在 span 上下文中的一组键:值对。
行李与轨迹一起移动,并附加到每个跨度上。
Spring Cloud Sleuth 明白,如果 HTTP 标头的前缀为 ,并且对于消息传递,它以 .baggage-
baggage_
目前对行李物品的数量或尺寸没有限制。 但是,请记住,过多会降低系统吞吐量或增加 RPC 延迟。 在极端情况下,过多的行李可能会由于超出传输级消息或标头容量而使应用程序崩溃。 |
以下示例显示了在 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 与 trace 一起旅行(每个子 span 都包含其父 Span 的 baggage)。 Zipkin 对行李一无所知,也不会收到该信息。
从 Sleuth 2.0.0 开始,您必须显式传递 baggage 键名称 在您的项目配置中。在此处阅读有关该设置的更多信息 |
标记附加到特定范围。换句话说,它们仅针对该特定范围显示。 但是,您可以按标签搜索以查找跟踪,前提是存在具有 asked 标签值的 span。
如果你希望能够根据 baggage 来查找 span,你应该在根 span 中添加相应的条目作为标签。
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"));
1.3. 将 Sleuth 添加到项目中
本节介绍如何使用 Maven 或 Gradle 将 Sleuth 添加到您的项目中。
要确保您的应用程序名称在 Zipkin 中正确显示,请在 中设置 属性。spring.application.name bootstrap.yml |
1.3.1. Only Sleuth (log correlation)
如果您只想使用 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. 通过 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. 使用 Zipkin 进行侦探,而不是 RabbitMQ 或 Kafka
如果您想使用 RabbitMQ 或 Kafka 而不是 HTTP,请添加 or 依赖项。
默认目标名称为 .spring-rabbit
spring-kafka
zipkin
如果使用 Kafka,则必须相应地设置 property 属性:spring.zipkin.sender.type
spring.zipkin.sender.type: kafka
spring-cloud-sleuth-stream 已弃用,并且与这些目标不兼容。 |
如果您希望 Sleuth 而不是 RabbitMQ,请添加 and 依赖项。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 的自动配置
Spring Cloud Sleuth 从版本 2.1.0 开始支持将跟踪发送到多个跟踪系统。
为了使其正常工作,每个跟踪系统都需要有一个 和 。
如果要覆盖提供的 bean,则需要为它们指定一个特定名称。
为此,您可以分别使用 和 。Reporter<Span>
Sender
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);
}
}
}