参考:https://cloud.spring.io/spring-cloud-static/Edgware.SR5/multi/multi__introduction.html
Spring Cloud Sleuth为Spring Cloud实现了分布式跟踪解决方案。
1. 术语
Spring Cloud Sleuth借用了Dapper的术语。
Span:基本工作单元。例如,发送RPC是一个新的span,就像向RPC发送响应一样。spans由一个唯一64位ID(span)和另一个64位ID(trace,span为其一部分)来标识。 Spans还有其他数据,例如描述、带时间戳的事件,键值注解(标签),导致它们的span的ID以及进程ID(通常是IP地址)。
Spans启动和停止,他们跟踪他们的时间信息。创建span后,必须在将来的某个时刻停止它。
提示:启动跟踪的初始范围称为root span。该span的span id值等于trace id。
Trace:一组span形成树状结构。例如,如果正在运行分布式大数据存储,则可能会由put请求形成跟踪。
Annotation:用于及时记录事件的存在。用于定义请求的开始和停止的一些核心注解是:
- cs:Client Sent - 客户端已发出请求。此注解描述了span的开始。
- sr:Server Received - 服务器端获得请求并将开始处理它。如果从该时间戳中减去cs时间戳,则会收到网络延迟。
- ss:Server Sent - 在请求处理完成时注解(当响应被发送回客户端时)。如果从该时间戳中减去sr时间戳,则将接收服务器端处理请求所需的时间。
- cr:Client Received - 表示span结束。客户端已成功收到服务器端的响应。如果从该时间戳中减去cs时间戳,则将接收客户端从服务器接收响应所需的全部时间。
使用Zipkin注解可视化Span和Trace在系统中的变化:
注解的每种颜色表示跨度(7个跨度 - 从A到G)。 如果在说明中有此类信息:
Trace Id = X
Span Id = D
Client Sent
这意味着当前跨度的Trace-Id设置为X,Span-Id设置为D.它还发出了Client Sent事件。
span的父/子关系可能情况的可视化图如下所示:
2. 目的
在以下部分中,将思考上图中的示例。
2.1 使用Zipkin进行分布式跟踪
总共有7个span。 如果你去查看Zipkin的跟踪记录,你会在第二个跟踪中看到这个数字:
但是,如果选择特定的跟踪,那么将看到4个span:
注:选择特定跟踪时,将看到合并的span。这意味着,如果通过Server Received和Server Sent / Client Received以及Client Sent 注解向Zipkin发送了2个span,那么它们将显示为单个span。
在这种情况下,为什么7和4个span之间存在差异?
- 2个span来自http:/start span。它具有服务器接收(SR)和服务器发送(SS)注解。
- 2个span来自从service1到service2的RPC调用到http:/foo端点。它在service1端具有客户端发送(CS)和客户端接收(CR)注解。它还在service2端具有服务器接收(SR)和服务器发送(SS)注解。物理上有2个span,但它们形成与RPC调用相关的1个逻辑span。
- 从Service2到service3到http:/bar端点的RPC调用有2个span。它在service2端具有客户端发送(CS)和客户端接收(CR)注解。它还在service3端具有服务器接收(SR)和服务器发送(SS)注释。物理上有2个span,但它们形成与RPC调用相关的1个逻辑span。
- 2个span来自从service2到service4的RPC调用到http:/baz端点。它在service2端具有客户端发送(CS)和客户端接收(CR)注解。它还在service4端具有服务器接收(SR)和服务器发送(SS)注解。物理上有2个span,但它们形成与RPC调用相关的1个逻辑span。
因此,如果我们计算物理span,我们从http:/start得到1,从service1调用service2得到2,service2调用service3的到2,从service2调用service4得到2。总共7个span。
从逻辑上讲,我们看到 Total Spans: 4 的信息,因为我们有1个与service1的传入请求相关的span和3个与RPC调用相关的span。
2.2 可视化错误
Zipkin允许可视化跟踪中的错误。当抛出一个异常并且没有被捕获时,我们就在Zipkin可以正确着色的span上设置适当的标签。可以在跟踪列表中看到一条红色的迹线,那是因为抛出了异常。
如果单击该跟踪,则会看到类似的图片:
然后,如果单击其中一个span,将看到以下内容
如图所示,可以轻松查看错误的原因以及与之相关的整个堆栈跟踪。
2.3 实例
Zipkin中的依赖关系图如下所示:
2.4 日志关联
trace 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,这里是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}" }
}
}
如果要将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 格式表示依赖项)。
(1)依赖关系设置
- 确保Logback位于类路径上(ch.qos.logback:logback-core)
- 添加Logstash Logback - 4.6版本示例:net.logstash.logback:logstash-logback-encoder:4.6
(2)Logback设置
- 将来自应用程序的信息以JSON格式记录到build/$ {spring.application.name}.json文件中
- 已注释掉两个额外的appender - 控制台和标准日志文件
- 具有与上一节中介绍的相同的日志记录模式
示例:
<?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(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"/>
<!-- 控制台日志 -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<!-- 控制条展示的最小日志级别-->
<level>DEBUG</level>
</filter>
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
<charset>utf8</charset>
</encoder>
</appender>
<!-- 文件日志 -->
<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>
<!-- 以JSON格式添加至日志文件 -->
<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-spring.xml,则必须在bootstrap中传递 spring.application.name 而不是 application属性文件。否则,自定义logback文件将无法正确读取该属性。
2.5 传播span上下文
span上下文是必须传播到跨进程边界的任何子span的状态。span上下文的一部分是Baggage。trace和span ID是span上下文的必需部分。Baggage是可选部分。
Baggage是存储在span上下文中的一组键值对。Baggage与trace一起移动并附在每个span上。如果HTTP报头以 baggage- 为前缀,Spring Cloud Sleuth将理解报头是Baggage相关的,并且对于消息,它以 baggage_ 开头。
注:目前对baggage的数量或大小没有限制。但是,太多可能会降低系统吞吐量或增加RPC延迟。在极端情况下,由于超出传输级别的消息或报头容量,它可能会导致应用程序崩溃。
在span上设置baggage的示例:
Span initialSpan = this.tracer.createSpan("span");
initialSpan.setBaggageItem("foo", "bar");
initialSpan.setBaggageItem("UPPER_CASE", "someValue");
Baggage与span标签
- Baggage和trace一起移动(即每个子span包含其父span的baggage)。 Zipkin不了解baggage,甚至不会收到这些信息。
- 标签附加到特定的span --- 它们仅针对特定span呈现。 但是,可以按标记搜索以查找trace,其中存在具有搜索标记值的span。
如果希望能够根据baggage查找span,则应在root span中添加相应的条目作为标记。
@Autowired Tracer tracer;
Span span = tracer.getCurrentSpan();
String baggageKey = "key";
String baggageValue = "foo";
span.setBaggageItem(baggageKey, baggageValue);
tracer.addTag(baggageKey, baggageValue);
3. 集成到项目中
注:要确保在Zipkin中正确显示应用程序名称,在bootstrap.yml中设置spring.application.name属性。
3.1只有Sleuth(日志相关)
如果想在没有Zipkin集成的情况下仅从Spring Cloud Sleuth获取,只需将spring-cloud-starter-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>
3.2 通过HTTP使用集成了Zipkin的Sleuth
如果想要Sleuth和Zipkin,只需添加spring-cloud-starter-zipkin依赖项。
<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>
3.3 通过RabbitMQ或Kafka使用集成了Zipkin的Sleuth
如果想使用RabbitMQ或Kafka而不是http,添加spring-rabbit或spring-kafka依赖项。默认目标名称是 zipkin。
注意:spring-cloud-sleuth-stream已经废弃了,并且与这些目标不兼容。
如果想要在RabbitMQ上使用Sleuth,添加spring-cloud-starter-zipkin和spring-rabbit依赖项。
<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>