Spring Cloud Sleuth

随着分布式服务架构的流行,特别是微服务等设计理念在系统中的应用,业务的调用链越来越复杂。
系统规模也会变得越来越大,各微服务间的调用关系也变得越来越复杂。通常一个由客户端发起的请求在后端系统中会经过多个不同的微服务调用来协同产生最后的请求结果,在复杂的微服务架构系统中,几乎每一个前端请求都会形成一个复杂的分布式服务调用链路,在每条链路中任何一个依赖服务出现延迟过高或者错误都有可能引起请求最后的失败。同时,缺乏一个自上而下全局的调用id,如何有效的进行相关的数据分析工作?
分布式服务跟踪是整个分布式系统中跟踪一个用户请求的过程(包括数据采集、数据传输、数据存储、数据分析、数据可视化),捕获此类跟踪让我们构建用户交互背后的整个调用链的视图,这是调试和监控微服务的关键工具。Spring Cloud Sleuth是Spring Cloud为分布式服务跟踪提供的解决方案,有了它,我们可以:
提供链路追踪,故障快速定位:可以通过调用链结合业务日志快速定位错误信息。
可视化各个阶段耗时,进行性能分析
各个调用环节的可用性、梳理服务依赖关系以及优化
数据分析,优化链路:可以得到用户的行为路径,汇总分析应用在很多业务场景。

一个简单例子

准备注册中心,创建一个微服务应用:sleuth-trace-1,实现rest接口trace1,并且这个接口调用了sleuth-trace-2。
代码见sleuth-trace-1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spring-cloud-sleuth</artifactId>
<groupId>com.hu</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>sleuth-trace-1</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>4.6</version>
</dependency>
</dependencies>

</project>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RestController
@EnableDiscoveryClient
@SpringBootApplication
public class TraceApplicationOne {
private final Logger logger = Logger.getLogger(getClass());

public static void main(String[] args) {
SpringApplication.run(TraceApplicationOne.class, args);
}

@Bean
@LoadBalanced//负载
RestTemplate restTemplate() {
return new RestTemplate();
}

@RequestMapping(value = "/trace1", method = RequestMethod.GET)
public String trace() {
logger.info("---------call sleuth-trace-1------------");
return restTemplate().getForEntity("http://sleuth-trace-2/trace2", String.class).getBody();
}

}

配置:

1
2
3
4
5
6
7
8
9
10
11
12
server:
port: 7904
spring:
application:
name: sleuth-trace-1
eureka:
client:
serviceUrl:
defaultZone: http://user:123456@localhost:8761/eureka
logging:
level:
root: INFO

创建sleuth-trace-2
依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spring-cloud-sleuth</artifactId>
<groupId>com.hu</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>sleuth-trace-1</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>4.6</version>
</dependency>
</dependencies>

</project>

配置:

1
2
3
4
5
6
7
8
9
10
11
12
server:
port: 7906
spring:
application:
name: sleuth-trace-2
eureka:
client:
serviceUrl:
defaultZone: http://user:123456@localhost:8761/eureka
logging:
level:
root: info
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RestController
@EnableDiscoveryClient
@SpringBootApplication
public class TraceApplicationTwo {
private final Logger logger = Logger.getLogger(getClass());

public static void main(String[] args) {
SpringApplication.run(TraceApplicationTwo.class, args);
}

@RequestMapping(value = "/trace2", method = RequestMethod.GET)
public String trace(HttpServletRequest request) {
logger.info("---------------call sleuth-trace-2 -------------------");
logger.info("----------------------"+ request.getHeader("X-B3-TraceId")+ request.getHeader("X-B3-SpanId"));
return "Trace";
}
}

之后启动sleuth-trace-2和sleuth-trace-1
通过postman访问http://localhost:7904/trace1
控制台分别显示信息:

1
2
[nio-7904-exec-4] tionOne$$EnhancerBySpringCGLIB$$a7e32e75 : ---------call sleuth-trace-1------------
[nio-7906-exec-1] tionTwo$$EnhancerBySpringCGLIB$$178e18f5 : ---------------call sleuth-trace-2 -------------------

以上准备工作完成后,来实现跟踪

实现跟踪

为sleuth-trace-2和sleuth-trace-1服务添加跟踪功能,通过Spring Cloud Sleuth,只需要在两个服务中增加依赖spring-cloud-starter-sleuth即可。

重启后,在访问,日志信息中就显示一些sleuth的信息,如

1
2
3
4
5
6
 [sleuth-trace-2,ced24759781b822f,b2c24565840b3207,false] 15288 --- [nio-7906-exec-9] ationTwo$$EnhancerBySpringCGLIB$$1b09342 : ---------------call sleuth-trace-2 -------------------

第一个值:sleuth-trace-2,它记录了应用的名称,也就是application.properties中spring.application.name参数配置的属性。
第二个值:ced24759781b822f,Spring Cloud Sleuth生成的一个ID,称为Trace ID,它用来标识一条请求链路。一条请求链路中包含一个Trace ID,多个Span ID。
第三个值:b2c24565840b3207,Spring Cloud Sleuth生成的另外一个ID,称为Span ID,它表示一个基本的工作单元,比如:发送一个HTTP请求。
第四个值:false,表示是否要将该信息输出到Zipkin等服务中来收集和展示。

上面四个值中的Trace ID和Span ID是Spring Cloud Sleuth实现分布式服务跟踪的核心。在一次服务请求链路的调用过程中,会保持并传递同一个Trace ID,从而将整个分布于不同微服务进程中的请求跟踪信息串联起来,以上面输出内容为例,sleuth-trace-1和sleuth-trace-2同属于一个前端服务请求来源,所以他们的Trace ID是相同的,处于同一条请求链路中。

跟踪原理

分布式系统中的服务跟踪在理论上并不复杂,它主要包括下面两个关键点:

  • 为了实现请求跟踪,当请求发送到分布式系统的入口端点时,只需要服务跟踪框架为该请求创建一个唯一的跟踪标识,同时在分布式系统内部流转的时候,框架始终保持传递该唯一标识,直到返回给请求方为止,这个唯一标识就是前文中提到的Trace ID。通过Trace ID的记录,我们就能将所有请求过程日志关联起来。
  • 为了统计各处理单元的时间延迟,当请求达到各个服务组件时,或是处理逻辑到达某个状态时,也通过一个唯一标识来标记它的开始、具体过程以及结束,该标识就是我们前文中提到的Span ID,对于每个Span来说,它必须有开始和结束两个节点,通过记录开始Span和结束Span的时间戳,就能统计出该Span的时间延迟,除了时间戳记录之外,它还可以包含一些其他元数据,比如:事件名称、请求信息等。

在项目中引入spring-cloud-starter-sleuth依赖之后, 它会自动的为当前应用构建起各通信通道的跟踪机制,比如:

  • 通过诸如RabbitMQ、Kafka(或者其他任何Spring Cloud Stream绑定器实现的消息中间件)传递的请求
  • 通过Zuul代理传递的请求
  • 通过RestTemplate发起的请求

之前的例子在发送到sleuth-trace-2之前会为在该请求的Header中增加实现跟踪需要的重要信息,主要有下面这几个(org.springframework.cloud.sleuth.Span的源码获取):

1
2
3
4
5
X-B3-TraceId:一条请求链路(Trace)的唯一标识,必须值
X-B3-SpanId:一个工作单元(Span)的唯一标识,必须值
X-B3-ParentSpanId::标识当前工作单元所属的上一个工作单元,Root Span(请求链路的第一个工作单元)的该值为空
X-B3-Sampled:是否被抽样输出的标志,1表示需要被输出,0表示不需要被输出
X-Span-Name:工作单元的名称

还可以在代码中获取这些信息:

1
2
3
4
5
6
@RequestMapping(value = "/trace2", method = RequestMethod.GET)
public String trace(HttpServletRequest request) {
logger.info("---------------call sleuth-trace-2 -------------------");
logger.info("----------------------"+ request.getHeader("X-B3-TraceId")+ request.getHeader("X-B3-SpanId"));
return "Trace";
}

再次运行:

1
2
---------------call sleuth-trace-2 -------------------
----------------------3f5b35a06e93292b4fac896c8cecede8
整合Logstash

ELK平台主要有由ElasticSearch、Logstash和Kiabana三个开源免费工具组成:

  • Elasticsearch是个开源分布式搜索引擎,它的特点有:分布式,零配置,自动发现,索引自动分片,索引副本机制,restful风格接口,多数据源,自动搜索负载等。
  • Logstash是一个完全开源的工具,他可以对你的日志进行收集、过滤,并将其存储供以后使用。
  • Kibana 也是一个开源和免费的工具,它Kibana可以为 Logstash 和 ElasticSearch 提供的日志分析友好的 Web 界面,可以帮助您汇总、分析和搜索重要数据日志。

实现与负责日志收集的Logstash完成数据对接即可,所以我们需要为Logstash准备json格式的日志输出。由于Spring Boot应用默认使用了logback来记录日志,而Logstash自身也有对logback日志工具的支持工具,所以我们可以直接通过在logback的配置中增加对logstash的appender,就能非常方便的将日志转换成以json的格式存储和输出了。

在之前的基础上,实现面向logstash日志输出配置:在pom.xml依赖中引入logstash-logback-encoder依赖

在sleuth-trace-1创建配置文件,之前的配置,移到新创建的文件中。
这是因为:logback-spring.xml的加载在application.yml之前,所以之前的配置logback-spring.xml无法获取到spring.application.name属性,因此这里将该属性移动到最先加载的bootstrap配置文件中。

/resource目录下创建logback配置文件logback-spring.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<?xml version="1.0" encoding="UTF-8"?>
<configuration>

<!--对logstash支持主要通过名为logstash的appender实现,主要是对日志信息的格式化处理,为了方便调试查看我们先将json日志输出到文件中。-->
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>

<springProperty scope="context" name="springAppName" source="spring.application.name"/>
<!-- 日志在工程中的输出位置 -->
<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-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 -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
<charset>utf8</charset>
</encoder>
</appender>

<!-- 为logstash输出的json格式的Appender -->
<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:-}",
"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"/>
</root>
</configuration>

运行,访问后,工程目录下发现有一个build目录,下面分别创建了以各自应用名称命名的json文件,该文件就是在logback-spring.xml中配置的名为logstash的appender输出的日志文件。
除了这种方式生成json文件之外,也可以使用LogstashTcpSocketAppender将日志内容直接通过Tcp Socket输出到logstash服务端。

1
2
3
4
<appender name="logstash" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<destination>127.0.0.1:9250</destination>
...
</appender>
整合zipkin

利用ELK平台提供的收集、存储、搜索等强大功能,对跟踪信息的管理和使用已经变得非常便利。但是,在ELK平台中的数据分析维度缺少对请求链路中各阶段时间延迟的关注,如需要追溯请求链路找出整个调用链路中出现延迟过高的瓶颈源,亦或是为了实现对分布式系统做延迟监控等与时间消耗相关的需求,这时候类似ELK这样的日志分析系统就显得有些乏力。对于这样的问题,我们就可以引入Zipkin来得以轻松解决。

Zipkin是Twitter的一个开源项目,它基于Google Dapper实现。我们可以使用它来收集各个服务器上请求链路的跟踪数据,并通过它提供的REST API接口来辅助我们查询跟踪数据以实现对分布式系统的监控程序,从而及时地发现系统中出现的延迟升高问题并找出系统性能瓶颈的根源。除了面向开发的API接口之外,它也提供了方便的UI组件来帮助我们直观的搜索跟踪信息和分析请求链路明细,比如:可以查询某段时间内各用户请求的处理时间等。

Zipkin主要有4个核心组件构成:

  • Collector:收集器组件,它主要用于处理从外部系统发送过来的跟踪信息,将这些信息转换为Zipkin内部处理的Span格式,以支持后续的存储、分析、展示等功能。
  • Storage:存储组件,它主要对处理收集器接收到的跟踪信息,默认会将这些信息存储在内存中,我们也可以修改此存储策略,通过使用其他存储组件将跟踪信息存储到数据库中。
  • RESTful API:API组件,它主要用来提供外部访问接口。比如给客户端展示跟踪信息,或是外接系统访问以实现监控等。
  • Web UI:UI组件,基于API组件实现的上层应用。通过UI组件用户可以方便而有直观地查询和分析跟踪信息。
    HTTP收集
    搭建Zipkin Server
    创建一个基础的Spring Boot应用,命名为zipkin-server,并在pom.xml中引入Zipkin Server的相关依赖。
    依赖:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
    <artifactId>spring-cloud-sleuth</artifactId>
    <groupId>com.hu</groupId>
    <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>zipkin-server</artifactId>

    <dependencies>
    <dependency>
    <groupId>io.zipkin.java</groupId>
    <artifactId>zipkin-server</artifactId>
    </dependency>
    <dependency>
    <groupId>io.zipkin.java</groupId>
    <artifactId>zipkin-autoconfigure-ui</artifactId>
    </dependency>
    </dependencies>
    </project>
    配置:
    1
    2
    3
    4
    5
    spring:
    application:
    name: zipkin-server
    server:
    port: 4343
    启动类:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    @EnableZipkinServer//启动Zipkin Server
    @SpringBootApplication
    public class ZipkinApplication {

    public static void main(String[] args) {
    SpringApplication.run(ZipkinApplication.class, args);
    }

    }
    启动项目,就可以浏览。

完成了Zipkin Server的搭建之后,对之前的应用实现将跟踪信息输出到Zipkin Server。

在之前的两个应用中,接入依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-zipkin</artifactId>
</dependency>

配置文件中增加Zipkin 的地址spring.zipkin.base-url=http://localhost:3332

再次启动应用,收集的信息就会在Zipkin Server显示。

收集跟踪消息中间件

1、修改应用
Spring Cloud Sleuth在整合Zipkin时,不仅实现了以HTTP的方式收集跟踪信息,还实现了通过消息中间件来对跟踪信息进行异步收集的封装。通过结合Spring Cloud Stream,我们可以非常轻松的让应用客户端将跟踪信息输出到消息中间件上,同时Zipkin服务端从消息中间件上异步地消费这些跟踪信息。
为了让sleuth-trace-2和sleuth-trace-1在产生跟踪信息之后,能够将抽样记录输出到消息中间件中,我们除了需要之前引入的spring-cloud-starter-sleuth依赖之外,还需要引入zipkin对Spring Cloud Stream的扩展依赖spring-cloud-sleuth-stream以及基于Spring Cloud Stream实现的消息中间件绑定器依赖,以使用RabbitMQ为例,我们可以加入如下依赖:

1
2
3
4
5
6
7
8
9
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-stream</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>

在配置中去掉HTTP方式实现时使用的spring.zipkin.base-url参数,并根据实际部署情况,增加消息中间件的相关配置。

2、修改zipkin-server服务端
让zipkin-server服务端能够从消息中间件中获取跟踪信息,我们只需要在pom.xml中引入针对消息中间件收集封装的服务端依赖spring-cloud-sleuth-zipkin-stream,同时为了支持具体使用的消息中间件,我们还需要引入针对消息中间件的绑定器实现,比如以使用RabbitMQ为例,我们可以在依赖中增加如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-zipkin-stream</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>

<dependency>
<groupId>io.zipkin.java</groupId>
<artifactId>zipkin-autoconfigure-ui</artifactId>
</dependency>

其中,spring-cloud-sleuth-zipkin-stream依赖是实现从消息中间件收集跟踪信息的核心封装,其中包含了用于整合消息中间件的核心依赖、zipkin服务端的核心依赖、以及一些其他通常会被使用的依赖(比如:用于扩展数据存储的依赖、用于支持测试的依赖等)。但是,需要注意的是这个包里并没有引入zipkin的前端依赖zipkin-autoconfigure-ui,为了方便使用,我们在这里也引用了它。

把项目都启动后,可以在RabbitMQ的控制页面中看到一个名为sleuth的交换器,它就是zipkin的消息中间件收集器实现使用的默认主题。
当发送请求时,有被抽样收集的跟踪信息,我们可以在RabbitMQ的控制页面中发现有消息被发送到了sleuth交换器中,同时我们再到zipkin服务端的Web页面中也能够搜索到相应的跟踪信息。

收集原理

暂停