Spring Cloud Sleuth
分布式链路跟踪
Spring Cloud Sleuth是Spring Cloud的分布式链路跟踪解决方案。
7.1 Terminology
Spring Cloud Sleuth借鉴了Dapper的术语。
-
Span :最基本的工作单元。例如:发送一个RPC就是一个新的span,同样一次RPC的应答也是。Span通过一个唯一的,长度64位的ID来作为标识,另一个64位ID用于跟踪。Span也可以带有其他数据,例如:描述,时间戳,键值对标签,起始Span的ID,以及处理ID(通常使用IP地址)等等。 Span有起始和结束,他们跟踪着时间信息。span应该都是成对出现的,有失必有终,所以一旦创建了一个span,那就必须在未来某个时间点结束它。 提示: 起始的span通常被称为:
root span
。它的id通常也被作为一个跟踪记录的id。 -
Trace :一个树结构的Span集合。例如:在分布式大数据存储中,可能每一次请求都是一次跟踪记录。
-
Annotation :用于记录一个事件时间信息。一些基础Annotation用于记录请求的起始和结束,例如:
- cs : Client Sent 客户端发送。这个annotation表示一个span的起始。
- sr : Server Received 服务端接收。表示服务端接收到请求,并开始处理。如果减去cs的时间戳,则表示网络传输时长。
- ss : Server Sent 服务端完成请求处理,应答信息被发回客户端。如果减去sr的时间戳,则表示服务端处理请求的时长。
- cr : Client Received 客户端接收。标志着Span的结束。客户端成功的接收到服务端的应答信息。如果减去cs的时间戳,则表示请求的响应时长。
可以通过下图,可视化的描述了Span和Trace的概念:
每一个颜色都表示着一个span(7个span,从A到G)。他们都有这这些数据信息:
Trace Id = X
Span Id = D
Client Sent
这表示着,这个span的Trace-Id为X,Span-Id为D。事件为Client Sent。
这些Span的上下级关系可以通过下图来表示:
7.2 Purpose 作用
下面内容,将以上面图中的例子作为原型来介绍。
7.2.1 Distributed tracing with Zipkin 通过Zipkin进行分布式链路跟踪
上例中总共有7个span。如果在Zipkin中,将可以看到:
然而当你点看一个某个跟踪记录时,会发现4个span:
注意: 在跟踪记录的视图中,可能会看到某些span被合并了。这也就意味着,有2个span的Server Received,Server Sent / Client Received,Client Sent发送到Zipkin,将被视为同一个span。
为什么7个span只显示了4个呢?
- 1个span来自
http:/start
。包含这Server Received (SR) 和 Server Sent (SS) 标记。 - 2个span来自
service1
到service2
的http:/foo
接口的RPC调用。包含着service1
的Client Sent (CS) 和 Client Received (CR) 标记。也包含着service2
的Server Received (SR) and Server Sent (SS) 标记。实际上有2个span,但是逻辑上是一个RPC调用的span。 - 2个span来自
service2
到service3
的http:/bar
接口的RPC调用。包含着service2
的Client Sent (CS) 和 Client Received (CR) 标记。也包含着service3
的Server Received (SR) 和 Server Sent (SS) 标记。实际上有2个span,但是逻辑上是一个RPC调用的span。 - 2个span来自
service2
到service4
的http:/baz
接口的RPC调用。包含着service2
的Client Sent (CS) 和 Client Received (CR) 标记。也包含着service4
的Server Received (SR) 和 Server Sent (SS) 标记。实际上有2个span,但是逻辑上是一个RPC调用的span。
因此,可以统计一下实际上有多少span,1个来自
http:/start
,2个来自service1
调用service2
,2个来自service2
调用service3
,2个来自service2
调用service4
,总共7个span。
逻辑上则视为4个span,1个外部请求
service1
,3个RPC调用。
7.2.2 Visualizing errors 错误信息的显示
Zipkin可以在跟踪记录中显示错误信息。当异常抛出并且没有捕获,Zipkin就会自动的换个颜色显示。在跟踪记录的清单中,当看到红色的记录时,就表示有异常抛出了。 下图就显示了错误信息:
如果点开其中一个span,可以看到下列信息:
正如你看到的,可以很清晰的显示错误信息。
7.2.3 Live examples
可以点击下图,查看一个在线例子:
点击“dependency”图标,可以看到下图:
7.2.4 Log correlation 相关日志
当使用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]]
如果使用了日志收集工具,如: Kibana, Splunk 等。那就可以按照事件发生的顺序进行显示。例如在Kibana中可以看到下列信息:
如果想要使用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+\[%{DATA:thread}\]\s+%{DATA:class}\s+:\s+%{GREEDYDATA:rest}" }
}
}
注意: 如果想要在Spring Cloud Foundry中整合Grok可以使用下面的规则:
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+\[%{DATA:thread}\]\s+%{DATA:class}\s+:\s+%{GREEDYDATA:rest}" }
}
}
7.2.5 JSON Logback with Logstash
一般在使用Logstash时不会直接保存日志到某个文本文件中,而是使用一个JSON文件(Logstash可以直接使用JSON)。 那就必须添加相关依赖。
Dependencies setup 依赖设置
- 需要确保Logback已经添加到classpath(
ch.qos.logback:logback-core
) - 添加Logstash的Logback编码器:
net.logstash.logback:logstash-logback-encoder:4.6
Logback setup 设置Logback
下面会展示一个Logback配置的例子(文件名为: logback-spring.xml)
- 应用日志信息会被记录成JSON格式到
build/${spring.application.name}.json
文件 - 日志还会有两个额外的输出:控制台和标准日志文件
- 日志格式和上一节中一样
<?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}"/>
<property name="CONSOLE_LOG_PATTERN"
value="%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr([${springAppName:-},%X{X-B3-TraceId:-},%X{X-B3-SpanId:-},%X{X-B3-ParentSpanId:-},%X{X-Span-Export:-}]){yellow} %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"/>
<appender-ref ref="logstash"/>
<!--<appender-ref ref="flatfile"/>-->
</root>
</configuration>
注意: 如果想要自定义logback-spring.xml
,可以通过bootstrap
中的spring.application.name
属性来替代application
的配置。否则,自定义的logback配置文件不会被加载。
7.3 Adding to the project
整合到项目中
7.3.1 Only Sleuth (log correlation) 仅包含Sleuth(日志相关部分)
如果仅仅想使用Spring Cloud Sleuth而不想整合Ziphin,那只需要添加Sleuth的依赖就行:
spring-cloud-starter-sleuth
。
Maven
<dependencyManagement> (1)
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Brixton.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependency> (2)
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
- 由Spring BOM来管理依赖版本
- 添加
spring-cloud-starter-sleuth
依赖
Gradle
dependencyManagement { (1)
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:Brixton.RELEASE"
}
}
dependencies { (2)
compile "org.springframework.cloud:spring-cloud-starter-sleuth"
}
- 由Spring BOM来管理依赖版本
- 添加
spring-cloud-starter-sleuth
依赖
7.3.2 Sleuth with Zipkin via HTTP 通过HTTP整合Sleuth和Zipkin
可以通过
spring-cloud-starter-zipkin
来整合:
Maven
<dependencyManagement> (1)
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Brixton.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependency> (2)
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
- 由Spring BOM来管理依赖版本
- 添加
spring-cloud-starter-zipkin
依赖
Gradle
dependencyManagement { (1)
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:Brixton.RELEASE"
}
}
dependencies { (2)
compile "org.springframework.cloud:spring-cloud-starter-zipkin"
}
- 由Spring BOM来管理依赖版本
- 添加
spring-cloud-starter-zipkin
依赖
7.3.3 Sleuth with Zipkin via Spring Cloud Stream 通过Spring Cloud Stream整合Sleuth和Zipkin
可以通过
spring-cloud-sleuth-stream
来整合:
Maven
<dependencyManagement> (1)
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Brixton.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependency> (2)
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-stream</artifactId>
</dependency>
<dependency> (3)
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<!-- EXAMPLE FOR RABBIT BINDING -->
<dependency> (4)
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-rabbit</artifactId>
</dependency>
- 由Spring BOM来管理依赖版本
- 添加
spring-cloud-sleuth-stream
依赖 - 添加
spring-cloud-starter-sleuth
依赖 - 添加Spring Cloud Stream桥接(例子中使用 Rabbit桥接)
Gradle
dependencyManagement { (1)
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:Brixton.RELEASE"
}
}
dependencies {
compile "org.springframework.cloud:spring-cloud-sleuth-stream" (2)
compile "org.springframework.cloud:spring-cloud-starter-sleuth" (3)
// Example for Rabbit binding
compile "org.springframework.cloud:spring-cloud-stream-binder-rabbit" (4)
}
- 由Spring BOM来管理依赖版本
- 添加
spring-cloud-sleuth-stream
依赖 - 添加
spring-cloud-starter-sleuth
依赖 - 添加Spring Cloud Stream桥接(例子中使用 Rabbit桥接)
7.3.4 Spring Cloud Sleuth Stream Zipkin Collector
如果想要在Zipkin中使用Spring Cloud Sleuth 流式控制,则需要添加
spring-cloud-sleuth-zipkin-stream
依赖:
Maven
<dependencyManagement> (1)
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Brixton.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependency> (2)
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-zipkin-stream</artifactId>
</dependency>
<dependency> (3)
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<!-- EXAMPLE FOR RABBIT BINDING -->
<dependency> (4)
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-rabbit</artifactId>
</dependency>
- 由Spring BOM来管理依赖版本
- 添加
spring-cloud-sleuth-zipkin-stream
依赖 - 添加
spring-cloud-starter-sleuth
依赖 - 添加Spring Cloud Stream桥接(例子中使用 Rabbit桥接)
Gradle
dependencyManagement { (1)
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:Brixton.RELEASE"
}
}
dependencies {
compile "org.springframework.cloud:spring-cloud-sleuth-zipkin-stream" (2)
compile "org.springframework.cloud:spring-cloud-starter-sleuth" (3)
// Example for Rabbit binding
compile "org.springframework.cloud:spring-cloud-stream-binder-rabbit" (4)
}
- 由Spring BOM来管理依赖版本
- 添加
spring-cloud-sleuth-zipkin-stream
依赖 - 添加
spring-cloud-starter-sleuth
依赖 - 添加Spring Cloud Stream桥接(例子中使用 Rabbit桥接)
然后,需要在主类上加上
@EnableZipkinStreamServer
注解:
package example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.sleuth.zipkin.stream.EnableZipkinStreamServer;
@SpringBootApplication
@EnableZipkinStreamServer
public class ZipkinStreamServerApplication {
public static void main(String[] args) throws Exception {
SpringApplication.run(ZipkinStreamServerApplication.class, args);
}
}
7.4 Additional resources 附加资源
关于Spring Cloud Sleuth 和 Zipkin相关介绍,可以观看Marcin Grzejszczak的视频
7.5 Features 特性
- 添加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 发生操作的应用名称
- traceId 此次跟踪的ID
- exportable 是否发送到Zipkin
-
对于分布式链路跟踪,提供一个抽象的通用数据模型:trace,span,annotation,key-value annotation。基本基于HTrace,但是兼容Zipkin(Dapper)
-
记录时间信息,用于后续分析。使用Sleuth,可以快速发现系统中的延迟原因。Sleuth不会写入太多日志,不会引起过多性能开销。
- 包含链路数据,其余可以扩展
- 包含可选的数据展示接口,如HTTP
- 管理卷数据支持多种采样策略
- 能够通过Zipkin进行数据的查询和可视化展示
-
能够跟踪常规Spring应用的访问入口和回应点,如:servlet,filter,async endpoints,rest template,定时任务,消息渠道,zuul filters,feign客户端等等。
-
Sleuth自带一个默认策略,来决定跟踪数据是通过http整合,还是其他通讯方式来传播消息。例如:通过HTTP方式传输时,报文头兼容Zipkin。这些传播逻辑可通过
SpanInjector
和SpanExtractor
自定义或者扩展。 -
对接收/丢弃的span进行简单的统计度量。
-
如果加入
spring-cloud-sleuth-zipkin
,那应用就会自动采用Zipkin兼容的方式来记录和收集跟踪信息。默认情况下,会通过HTTP发送到本地Zipkin服务(端口:9411).可以通过spring.zipkin.baseUrl
来修改这一地址。 -
如果加入
If spring-cloud-sleuth-stream
,那应用会采用Spring Cloud Stream的方式来记录和收集跟踪信息。应用会自动成为跟踪信息的生产者,然后将消息发送给配置的消息代理中间件(如:RabbitMQ,Kafka,Redis)。
重要: 如果使用Zipkin或者Stream,可以配置span记录输出的采样率,配置项为spring.sleuth.sampler.percentage
(默认0.1,也就是10%)。这个可能会让开发者以为丢失了一些span,其实不然。
注意: SLF4J MDC总是会设置,并且如果使用logback,那上面的例子中trace/span的id则会立即显示在日志中。其他的日志系统需要配置各自的格式来达到这样的效果。默认的logging.pattern.level
设置为%clr(%5p) %clr([${spring.application.name:},%X{X-B3-TraceId:-},%X{X-B3-SpanId:-},%X{X-Span-Export:-}]){yellow}
(这也是一个Spring Boot整合logback时有的特性)。 这就意味着,在使用SLF4J时不需要在手工配置这个格式了,自动会这样输出。
7.6 Sampling 采样
在分布式链路跟踪中,跟踪数据可能会非常大,所以采样变的很重要。(一般来说,不需要把每一个发生的动作都导出) Spring Cloud Sleuth有一个
Sampler
策略,可以通过这个实现类来控制采样算法。采样器不会阻碍span相关id的产生,但是会对导出以及附加事件标签的相关操作造成影响。 默认情况下,如果一个span已经激活,则会继续使用策略用以后续跟踪,但是,新的span总是会标记上不用导出。
如果应用是使用这个策略,则会发现日志中跟踪记录是完整的,但是远程存储端则不一定。 经过测试,默认值是足够的,如果你只想使用日志来记录,则更好。(比如,使用ELK来进行日志收集分析方案)。 如果需要导出span数据到Zipkin或者Spring Cloud Stream,那
AlwaysSampler
可以到处全部数据,PercentageBasedSampler
则会到处固定频率的分片,可以根据需要自行选择使用。
注意: 在使用spring-cloud-sleuth-zipkin
或者spring-cloud-sleuth-stream
时,默认使用PercentageBasedSampler
。可以通过spring.sleuth.sampler.percentage
对其进行配置。这个值介于0.0到1.0之间。
如果想要使用其他策略,也很简单,只需要:
@Bean
public Sampler defaultSampler() {
return new AlwaysSampler();
}
7.7 Instrumentation
Spring Cloud Sleuth可以自动的跟踪所有Spring应用,因此,不需要做什么额外的操作。会自动选择相应的方法进行处理,例如:如果是一个servlet的web应用,则会使用一个
Filter
;如果是Spring Integration,则会使用`ChannelInterceptors。
还可以在span标签中自定义一些键。为了限制span数据大小,默认情况下,一次HTTP请求仅仅会带上少量的元数据,如:状态码,主机地址以及URL。可以通过
spring.sleuth.keys.http.headers
进行额外的配置,可以列出想要带上的Header名字。
注意: 标签数据只有当Sampler
允许时,才会收集和导出。默认情况下,是不会收集这些数据的。这些数据一般来说,量很大,也没太多的意义。
注意: Spring Cloud Sleuth的数据采集还是比较积极的,就是说,总是会积极的尝试从线程上下文中获取跟踪数据。同样无论是否需要导出都会捕获时间事件。以后,可能会考虑改成被动模式。
7.8 Span lifecycle 生命周期
通过
org.springframework.cloud.sleuth.Tracer
接口的api,可以观察到Span的各个生命周期操作:
- start 当开始一个span时,就会分配一个名字,以及记录启动时间戳。
- close 当span已经完成(记录截止时间戳),并且如果其符合条件,则导出到Zipkin。同时从当前线程上下文中移除此span。
- continue 作为一个span的副本而创建的一个新的span实例。
- detach 不会停止或者关闭span,仅仅是从当前线程上下文中移除此span。
- create with explicit parent 创建一个新的span,并显示指定其父span。
提示: 通常不需要去操作这些api,Spring会自动创建Tracer
,开发者只需要自动注入就可以使用。
7.8.1 Creating and closing spans 创建和关闭
可以通过
Tracer
手动创建span:
// Start a span. If there was a span present in this thread it will become
// the `newSpan`'s parent.
Span newSpan = this.tracer.createSpan("calculateTax");
try {
// ...
// You can tag a span
this.tracer.addTag("taxValue", taxValue);
// ...
// You can log an event on a span
newSpan.logEvent("taxCalculated");
} finally {
// Once done remember to close the span. This will allow collecting
// the span to send it to Zipkin
this.tracer.close(newSpan);
}
这个例子,展示了如何手工创建一个span实例。如果当前线程上下文中已经存在一个span了,那已存在就span会成为新创建的span的父级。
重要: 创建完span要记住清理!如果想要发送到Zipkin,就不要忘了关闭span。
7.8.2 Continuing spans 持续
有的时候,其实不需要创建一个span,仅仅是需要在现有的span继续一些持续的操作。例如,下列情况:
- AOP 如果在最对已有span的操作,进行AOP时,就不需要再额外创建了
- Hystrix 在执行Hystrix命令时,从逻辑上讲,仍然属于当前操作中的一部分,所以,一般也不需要再次创建span。
接下来就展示,如何在现有的span上继续处理:
Span continuedSpan = this.tracer.continueSpan(spanToContinue);
assertThat(continuedSpan).isEqualTo(spanToContinue);
使用
Tracer
接口:
// let's assume that we're in a thread Y and we've received
// the `initialSpan` from thread X
Span continuedSpan = this.tracer.continueSpan(initialSpan);
try {
// ...
// You can tag a span
this.tracer.addTag("taxValue", taxValue);
// ...
// You can log an event on a span
continuedSpan.logEvent("taxCalculated");
} finally {
// Once done remember to detach the span. That way you'll
// safely remove it from the current thread without closing it
this.tracer.detach(continuedSpan);
}
重要 创建完span要记住清理!在对现有span上继续操作后,不要忘了最后调用detach
。假如:span由线程X创建,然后它等着线程Y,Z来完成后续动作;span在线程Y,Z在完成自己的操作后要调用detach
;这样线程X关闭span时,数据才会被收集。
7.8.3 Creating spans with an explicit parent
有的时候需要创建一个新的span,并显示指定其父span。比如说,在一个线程中已经存在span,然后调用另一个线程,这时候想要一个新的span来独立监控新线程的执行。
Tracer
接口中的startSpan
方法就可以被用到,例如:
// let's assume that we're in a thread Y and we've received
// the `initialSpan` from thread X. `initialSpan` will be the parent
// of the `newSpan`
Span newSpan = this.tracer.createSpan("calculateCommission", initialSpan);
try {
// ...
// You can tag a span
this.tracer.addTag("commissionValue", commissionValue);
// ...
// You can log an event on a span
newSpan.logEvent("commissionCalculated");
} finally {
// Once done remember to close the span. This will allow collecting
// the span to send it to Zipkin. The tags and events set on the
// newSpan will not be present on the parent
this.tracer.close(newSpan);
}
重要: 还是一样,不要忘了关闭span。否在当关闭当前线程时,会在日志中看到很多警告。更糟的是,不关闭span,就不会被Zipkin收集到数据。
7.9 Naming spans 具名
为span命名,可不是个轻松活。Span的名字应该能够表述一个操作。名字应该是代价低廉的(比如,不带有id)。
因此,很多span名字都是按照一定规则造出来的:
controller-method-name
当控制器的某个方法收到请求时:conrollerMethodName
async
为一些异步操作进行包装,如:Callable
,Runnable
@Scheduled
使用简单类名
对于异步处理,还可以手工指定名字。
7.9.1 @SpanName annotation
可以使用
@SpanName
注解来命名。
@SpanName("calculateTax")
class TaxCountingRunnable implements Runnable {
@Override public void run() {
// perform logic
}
}
在这个例子中,当按照这样的方式来执行时:
Runnable runnable = new TraceRunnable(tracer, spanNamer, new TaxCountingRunnable());
Future<?> future = executorService.submit(runnable);
// ... some additional logic ...
future.get();
span就会被命名为:
calculateTax
7.9.2 toString() method
还有一中比较少见的方式,为
Runnable
或者Callable
创建一个独立的class。最常见一般都是使用匿名类。当没有@SpanName
注解时,会检查是否重写了toString()
方法。
Runnable runnable = new TraceRunnable(tracer, spanNamer, new Runnable() {
@Override public void run() {
// perform logic
}
@Override public String toString() {
return "calculateTax";
}
});
Future<?> future = executorService.submit(runnable);
// ... some additional logic ...
future.get();
这样也会创建一个名字为
calculateTax
的span。
7.10 Customizations 定制化
通过
SpanInjector
和SpanExtractor
,可以定制span的创建和传播。
跟踪信息在进程间传播,有两种方式:
- 通过Spring Integration
- 通过HTTP
启动或者合并到一个已有的跟踪记录时,Span的id可以兼容Zipkin头(无论是
Message
头还是HTTP头)。在出站请求时,跟踪信息会自动注入,以便下一跳的继续跟踪。
7.10.1 Spring Integration
对于Spring Integration可以通过带有
Message
以及MessageBuilder
的特殊Bean来完成跟踪信息构建。
@Bean
public SpanExtractor<Message> messagingSpanExtractor() {
...
}
@Bean
public SpanInjector<MessageBuilder> messagingSpanInjector() {
...
}
可以自己实现他们,在自己class上加上
@Primary
就行。
7.10.2 HTTP
对于HTTP方式,则是通过
HttpServletRequest
来完成跟踪信息的构建。
@Bean
public SpanExtractor<HttpServletRequest> httpServletRequestSpanExtractor() {
...
}
可以自己实现他们,在自己class上加上
@Primary
就行。
7.10.3 Example
假如不使用标准的Zipkin方式来命名HTTP头:
- trace id 命名为:
correlationId
- span id 命名为:
mySpanId
则
SpanExtractor
如下:
static class CustomHttpServletRequestSpanExtractor
implements SpanExtractor<HttpServletRequest> {
@Override
public Span joinTrace(HttpServletRequest carrier) {
long traceId = Span.hexToId(carrier.getHeader("correlationId"));
long spanId = Span.hexToId(carrier.getHeader("mySpanId"));
// extract all necessary headers
Span.SpanBuilder builder = Span.builder().traceId(traceId).spanId(spanId);
// build rest of the Span
return builder.build();
}
}
然后,可以这样注册它:
@Bean
@Primary
SpanExtractor<HttpServletRequest> customHttpServletRequestSpanExtractor() {
return new CustomHttpServletRequestSpanExtractor();
}
Spring Cloud Sleuth处于安全的原因,不会在Http Response上,加上trace/span相关的头信息。如果需要加上,则可以自定义一个
SpanInjector
,然后配置一个Servlet Filter来完成:
static class CustomHttpServletResponseSpanInjector
implements SpanInjector<HttpServletResponse> {
@Override
public void inject(Span span, HttpServletResponse carrier) {
carrier.addHeader(Span.TRACE_ID_NAME, span.traceIdString());
carrier.addHeader(Span.SPAN_ID_NAME, Span.idToHex(span.getSpanId()));
}
}
static class HttpResponseInjectingTraceFilter extends GenericFilterBean {
private final Tracer tracer;
private final SpanInjector<HttpServletResponse> spanInjector;
public HttpResponseInjectingTraceFilter(Tracer tracer, SpanInjector<HttpServletResponse> spanInjector) {
this.tracer = tracer;
this.spanInjector = spanInjector;
}
@Override
public void doFilter(ServletRequest request, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) servletResponse;
Span currentSpan = this.tracer.getCurrentSpan();
this.spanInjector.inject(currentSpan, response);
filterChain.doFilter(request, response);
}
}
然后,可以这样注册它们:
@Bean
SpanInjector<HttpServletResponse> customHttpServletResponseSpanInjector() {
return new CustomHttpServletResponseSpanInjector();
}
@Bean
HttpResponseInjectingTraceFilter responseInjectingTraceFilter(Tracer tracer) {
return new HttpResponseInjectingTraceFilter(tracer, customHttpServletResponseSpanInjector());
}
7.10.4 Custom SA tag in Zipkin 在Zipkin中定制SA标签
有的时候想要手工创建一个Span,用于跟踪一个外部服务的调用。那可以使用
peer.service
标签来创建span,标签中可以包含想要调用的值。下面这个例子就是扩展调用Redis服务:
org.springframework.cloud.sleuth.Span newSpan = tracer.createSpan("redis");
try {
newSpan.tag("redis.op", "get");
newSpan.tag("lc", "redis");
newSpan.logEvent(org.springframework.cloud.sleuth.Span.CLIENT_SEND);
// call redis service e.g
// return (SomeObj) redisTemplate.opsForHash().get("MYHASH", someObjKey);
} finally {
newSpan.tag("peer.service", "redisService");
newSpan.tag("peer.ipv4", "1.2.3.4");
newSpan.tag("peer.port", "1234");
newSpan.logEvent(org.springframework.cloud.sleuth.Span.CLIENT_RECV);
tracer.close(newSpan);
}
重要: 记住不要同时添加peer.service
和SA
标签!只需要加上peer.service
就行。
7.10.5 Custom service name 定制服务名
默认情况下,Sleuth会假定span需要发送到Zipkin的
spring.application.name
服务。在实际使用时,可能不想这样。可能需要指定一个服务来接收某个应用的全部span。其实这样只需要简单配置一下就行,如:
spring.zipkin.service.name: foo
7.10.6 Host locator 主机定位
为了可以跨主机来跟踪,需要对主机名和端口进行抉择。默认的策略是通过server的配置属性。如果没有配置,则会尝试从网络中获取。
如果启用了服务发现,且服务实例已经注册了,那就需要设置这个配置项:
spring.zipkin.locator.discovery.enabled: true
7.11 Span Data as Messages
当引入
spring-cloud-sleuth-stream
依赖并加上Channel Binder(如 :spring-cloud-starter-stream-rabbit
或者spring-cloud-starter-stream-kafka
)后,就可以通过Spring Cloud Stream来堆积和发送span数据了。这样就会自动产生消息,并且消息负载会是Spans
类型。
7.11.1 Zipkin Consumer
有一个专门的注解来转换消息,可以让Span数据推送到Zipkin的
SpanStore
中。如:
@SpringBootApplication
@EnableZipkinStreamServer
public class Consumer {
public static void main(String[] args) {
SpringApplication.run(Consumer.class, args);
}
}
这样Span数据就可以通过Spring Cloud Stream来转发给Zipkin了。如果想要UI界面,再加上下面这个依赖就行:
<groupId>io.zipkin.java</groupId>
<artifactId>zipkin-autoconfigure-ui</artifactId>
这样就拥有了一个Zipkin服务,默认端口为9411。
默认的
SpanStore
是通过内存实现的。也可以使用MySQL,加入spring-boot-starter-jdbc
依赖就行。具体配置如下:
spring:
rabbitmq:
host: ${RABBIT_HOST:localhost}
datasource:
schema: classpath:/mysql.sql
url: jdbc:mysql://${MYSQL_HOST:localhost}/test
username: root
password: root
# Switch this on to create the schema on startup:
initialize: true
continueOnError: true
sleuth:
enabled: false
zipkin:
storage:
type: mysql
注意: @EnableZipkinStreamServer
也带有@EnableZipkinServer
,所以,将会以标准的Zipkin服务接口的方式来处理,即:通过HTTP方式收集span数据,通过Zipkin Web来进行查询。
7.11.2 Custom Consumer
跟踪信息的自定义消费端也比较简单,可以使用
spring-cloud-sleuth-stream
来绑定到SleuthSink
。例如:
@EnableBinding(SleuthSink.class)
@SpringBootApplication(exclude = SleuthStreamAutoConfiguration.class)
@MessageEndpoint
public class Consumer {
@ServiceActivator(inputChannel = SleuthSink.INPUT)
public void sink(Spans input) throws Exception {
// ... process spans
}
}
注意: 上例中,明确排除了SleuthStreamAutoConfiguration
,因此,应用本身就不会发送消息了,但这也是可选的,实际使用中,可以根据需要不排除。
7.12 Metrics
当前版本的Spring Cloud Sleuth只是对span进行简单的度量。主要是通过Spring Boot的metrics机制,对span的接收和丢弃数量进行了度量。每次sapn发送到Zipkin时,接收数量就会递增。当有错误时,丢弃数量就会递增。
7.13 Integrations 整合
7.13.1 Runnable and Callable
如果是使用
Runnable
或者Callable
来包装逻辑代码。可以这样:
Runnable runnable = new Runnable() {
@Override
public void run() {
// do some work
}
@Override
public String toString() {
return "spanNameFromToStringMethod";
}
};
// Manual `TraceRunnable` creation with explicit "calculateTax" Span name
Runnable traceRunnable = new TraceRunnable(tracer, spanNamer, runnable, "calculateTax");
// Wrapping `Runnable` with `Tracer`. The Span name will be taken either from the
// `@SpanName` annotation or from `toString` method
Runnable traceRunnableFromTracer = tracer.wrap(runnable);
Callable<String> callable = new Callable<String>() {
@Override
public String call() throws Exception {
return someLogic();
}
@Override
public String toString() {
return "spanNameFromToStringMethod";
}
};
// Manual `TraceCallable` creation with explicit "calculateTax" Span name
Callable<String> traceCallable = new TraceCallable<>(tracer, spanNamer, callable, "calculateTax");
// Wrapping `Callable` with `Tracer`. The Span name will be taken either from the
// `@SpanName` annotation or from `toString` method
Callable<String> traceCallableFromTracer = tracer.wrap(callable);
这样每次执行都会有新的Span的创建和关闭。
7.13.2 Hystrix
7.13.2.1 Custom Concurrency Strategy 定制并发策略
可以注册一个自定义的
HystrixConcurrencyStrategy
,它通过TraceCallable
可以包装Sleuth中所有的Callable
实例。这个策略,会自行判断在之前的Hystrix命令是否已经开始跟踪,来决定是创建还是延续使用span。 也可以通过设置spring.sleuth.hystrix.strategy.enabled
为false
来关闭这个策略。
7.13.2.2 Manual Command setting
假设有下面这样的
HystrixCommand
:
HystrixCommand<String> hystrixCommand = new HystrixCommand<String>(setter) {
@Override
protected String run() throws Exception {
return someLogic();
}
};
为了跟踪,可以用
TraceCommand
对其进行一定的包装:
TraceCommand<String> traceCommand = new TraceCommand<String>(tracer, traceKeys, setter) {
@Override
public String doRun() throws Exception {
return someLogic();
}
};
7.13.3 RxJava
建议自定义一个
RxJavaSchedulersHook
,它使用TraceAction
来包装实例中所有的Action0
。这个钩子对象,会根据之前调度的Action是否已经开始跟踪,来决定是创建还是延续使用span。可以通过设置spring.sleuth.rxjava.schedulers.hook.enabled
为false
来关闭这个对象的使用。
可以定义一组正则表达式来对线程名进行过滤,来选择哪些线程不需要跟踪。可以使用逗号分割的方式来配置
spring.sleuth.rxjava.schedulers.ignoredthreads
属性。
7.13.4 HTTP integration
这个特性的开启,通过
spring.sleuth.web.enabled
属性。当不想使用时,设置为false
就行。
7.13.4.1 HTTP Filter
通过
TraceFilter
可以对所有入站请求进行跟踪。这时候,Span的名字为http:
加上请求的路径。例如,如果请求是/foo/bar
,那span名字就是http:/foo/bar
。通过spring.sleuth.web.skipPattern
配置项,可以配置一个URI规则来跳过监控。如果classpath中有一个ManagementServerProperties
,其中contextPath
也不会被跟踪。
7.13.4.2 HandlerInterceptor
如果需要对span名字进行进一步的控制,可以使用
TraceHandlerInterceptor
,它会对已有的HandlerInterceptor
进行包装,或者直接添加到已有的HandlerInterceptors
中。TraceHandlerInterceptor
会在HttpServletRequest
中添加一个特别的request attribute。如果TraceFilter
没有发现这个属性,就会创建一个额外的“fallback”(保底)span,这样确保跟踪信息完整。
7.13.4.3 Async Servlet support
如果控制器返回了一个
Callable
或者WebAsyncTask
,Spring Cloud Sleuth会延续已有的span,而不是创建一个新的span。
7.13.5 HTTP client integration
7.13.5.1 Synchronous Rest Template
重要: 一个AsyncRestTemplate
Bean被注册时会有一个版本概念。如果需要自己的Bean来替代TraceAsyncRestTemplate
。最好的方式是自定义一个ClientHttpRequestFactory
以及AsyncClientHttpRequestFactory
。如果需要自己的AsyncRestTemplate
而又不想包装它,那这个就不会被跟踪。
自定义span在发送和接收请求时的创建/关闭逻辑,可以自定义
ClientHttpRequestFactory
和AsyncClientHttpRequestFactory
Bean来达到这个目的。记住使用那些能兼容跟踪的实例(不要忘了在TraceAsyncListenableTaskExecutor
中包装一个ThreadPoolTaskScheduler
来使用)。
例如:自定义请求工厂:
@EnableAutoConfiguration
@Configuration
public static class TestConfiguration {
@Bean
ClientHttpRequestFactory mySyncClientFactory() {
return new MySyncClientHttpRequestFactory();
}
@Bean
AsyncClientHttpRequestFactory myAsyncClientFactory() {
return new MyAsyncClientHttpRequestFactory();
}
}
如果需要阻止
AsyncRestTemplate
特性,可以设置spring.sleuth.web.async.client.enabled
为false
。
如果需要禁用默认的
TraceAsyncClientHttpRequestFactoryWrapper
,可以设置spring.sleuth.web.async.client.factory.enabled
为false
。
如果不想创建
AsyncRestClient
,可以设置spring.sleuth.web.async.client.template.enabled
为false
。
7.13.6 Feign
默认情况下,Spring Cloud Sleuth提供了一个
TraceFeignClientAutoConfiguration
来整合Feign。如果需要禁用的话,可以设置spring.sleuth.feign.enabled
为false
。如果禁用,与Feign相关的机制就不会发生。
Feign部分功能是通过
FeignBeanPostProcessor
来完成的。可以设置spring.sleuth.feign.processor.enabled
为false
来禁用这个类。如果禁用,那Spring Cloud Sleuth就不会执行自定义的Feign组件。不过,所有默认的Feign组件还是有效的。
7.13.7 Asynchronous communication
7.13.7.1 @Async annotated methods
在Spring Cloud Sleuth中,有相应的机制来处理异步组件的跟踪,这样在不同的线程之间也能够进行跟踪。可以设置
spring.sleuth.async.enabled
为false
来关闭。
如果在方法上加上
@Async
,那会自动的创建一个新的span,并带有下列特性:
- span 名字会被命名为被注解的方法名
- span 标签中会自动带上方法的类名和方法名
7.13.7.2 @Scheduled annotated methods
在Spring Cloud Sleuth中,有相应的机制来处理调度方法的执行,这样在不同的线程之间也能够进行跟踪。可以设置
spring.sleuth.scheduled.enabled
为false
来关闭。
如果在方法上加上
@Scheduled
,那就会自动创建一个新的span,并带有下列特性:
- span 名字会被命名为被注解的方法名
- span 标签中会自动带上方法的类名和方法名
如果不需要跟踪某些
@Scheduled
,可以在spring.sleuth.scheduled.skipPattern
设置一些正则表达式来过滤一些class。
提示: 如果一起使用spring-cloud-sleuth-stream
和spring-cloud-netflix-hystrix-stream
,那span会被每一个Hystrix metrics创建并发送到Zipkin。这可能不是你想要的。可是进行如下设置,来阻止此行为:spring.sleuth.scheduled.skipPattern=org.springframework.cloud.netflix.hystrix.stream.HystrixStreamTask
7.13.7.3 Executor, ExecutorService and ScheduledExecutorService
Sleuth本身就提供了
LazyTraceExecutor
,TraceableExecutorService
以及TraceableScheduledExecutorService
。这些线程池对于每一次新任务的提交,调用或者调度都会创建新的span。
下面的列子,展示了如何在使用
CompletableFuture
时通过TraceableExecutorService
来处理跟踪信息。
CompletableFuture<Long> completableFuture = CompletableFuture.supplyAsync(() -> {
// perform some logic
return 1_000_000L;
}, new TraceableExecutorService(executorService,
// 'calculateTax' explicitly names the span - this param is optional
tracer, traceKeys, spanNamer, "calculateTax"));
7.13.8 Messaging 消息
Spring Cloud Sleuth本身就整合了Spring Integration。它发布/订阅事件都是会创建span。可以设置
spring.sleuth.integration.enabled
为false
来禁用这个机制。
Spring Cloud Sleuth直到1.0.4版本,使用消息时,还是会发送一些无效的跟踪头。这些头实际上和HTTP头的命名一样(都带有
-
分隔符)。为了向下兼容,从1.0.4版本开始,有效和无效头都会发送。到Spring Cloud Sleuth 1.1版本,将会移除那些不建议使用的头。
从1.0.4版本开始,可以通过
spring.sleuth.integration.patterns
配置哪些消息通道需要跟踪。默认情况下,所有的消息通道都会被跟踪。
7.13.9 Zuul
Sleuth会注册一些Zuul Filter,用于传播跟踪信息(在请求头中带上跟踪信息)。可以设置
spring.sleuth.zuul.enabled
为false
来关闭。
7.14 Running examples
可以找到一些部署在Pivotal Web Services中的例子。可以在下列链接中找到: