SpringCloud(八)链路追踪Sleuth
在分布式系统中,完成一个外部请求可能需要多个应用之间相互协作,形成复杂的调用链路。而一旦出现问题,更是难以定位问题,也难以直观地获取到各个服务之间的依赖关系。Spring Cloud Sleuth的出现正是为了实现分布式系统的链路追踪。
文章目录
Sleuth的基本概念
Sleuth借鉴了Google Dapper的术语。
有以下几个要素:
Span
:基本的工作单元。比如发送一个RPC以及返回一个RPC应答分别是一个新的Span。Span是根据一个唯一的64-bit ID和所属Trace的64-bit ID区分的。
Trace
:一系列的Span组成的树结构的图
Annotation
:用来记录事件存在的时间。
- cs - Client Sent - client端发送一个请求。Span的开始。
- sr - Server Received - server端接收到请求,并且准备处理
- ss - Server Sent - server端请求处理完成
- cr - Client Received - client端接收到server端的响应结果。Span的结束。
上图中七种颜色分别代表了从A-G7个Span。
将他们转换为父子关系图,如下图所示:
引入Sleuth
我们对web-app-service
,user-service
和sms-service
这三个服务增加Sleuth的支持。
在pom.xml
文件中引入spring-cloud-starter-sleuth
依赖。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
在application.yml
文件中设置
spring:
sleuth:
sampler:
percentage: 1.0
表示设置Sleuth的采样率为100%,即全部采样。
我们在web-app-service
新建一个用户注册的接口,分别请求user-service
和sms-service
的接口,涉及到的接口增加Log输出。
@RequestMapping("/register")
public String register() {
logger.info("user is registering");
userService.register();
smsService.sendRgister();
return "success";
}
请求web-app-service
的/user/register
接口(http://localhost:8101/user/register
),返回成功后可以在三个服务的控制台分别看到的日志打印。
# web-app-service
2018-10-10 16:51:11.454 INFO [web-app-service,dd3aaab11dddbca1,dd3aaab11dddbca1,true] 3760 --- [nio-8101-exec-9] c.c.s.web.controller.UserController : user is registering
# user-service
2018-10-10 16:51:11.463 INFO [user-service,dd3aaab11dddbca1,436fe40b80a2bdb1,true] 8608 --- [nio-8002-exec-9] c.c.s.user.controller.UserController : invoke user register endpoint
# sms-service
2018-10-10 16:51:11.983 INFO [sms-service,dd3aaab11dddbca1,ff559b20a835e679,true] 5920 --- [nio-8001-exec-3] c.c.s.controller.SMSController : send sms to registered user
注意日志的输出分别代表[appname,traceId,spanId,exportable]
。
exportable
:表示日志是否应该导入到Zipkin中。
Sleuth集成ELK实现日志分析
对于分布式系统,我们希望将日志纳入统一管理。ELK(Elasticsearch+Logstash+Kibana)是日志管理分析的一系列组件,实现了日志收集、检索和可视化查询。我们通过logback
和logstash-logback-encoder
将日志按照指定格式传输给Logstash,经过处理后传输给Elasticsearch,再通过Kibana的web界面进行日志查询。
在Logstash上设置输入和输出,并重启Logstash服务。在输入配置里,tcp.host
就是我们的Logstash所在IP,tcp.port
是我们使用TCP传输日志时的指定端口。输出配置我们的Elasticsearch IP和端口,并指定索引(不能使用大写字母)。
input {
tcp {
mode => "server"
host => "192.168.126.128"
port => 3333
}
}
filter {
}
output {
elasticsearch {
action => "index"
hosts => "192.168.126.128:9200"
index => "spring_cloud_demo_logs"
}
}
logback日志配置文件logback-spring.xml
,destination
需要配置上面的Logstash IP和TCP端口,并指定编码器LoggingEventCompositeJsonEncoder
及格式。
<?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}"/>
<!--Logstash 服务器地址和TCP端口-->
<property name="LOGSTASH_TCP_DESTINATION" value="192.168.126.128:3333" />
<!-- 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 name="logstash"
class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<destination>${LOGSTASH_TCP_DESTINATION}</destination>
<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>
请求web-app-service
的/user/register
接口(http://localhost:8101/user/register
),自动向ELK发送日志。
在Kibana web界面创建索引
采集到的日志就是我们各个服务中的输出。
根据某一个traceId
,对e7df1589e5a2a73f
进行搜索显示整个调用链路的日志信息。
展开某一条日志后可以看到其详细信息
使用Sleuth和Zipkin实现链路追踪
Zipkin作为分布式链路跟踪系统,提供了可视化页面极大地方便了对请求的监控,更容易明确服务之间的依赖。新建一个zipkin-server
服务作为链路追踪服务。我们需要将Sleuth采集的日志信息通过Http或者Spring Cloud Stream方式传输给Zipkin服务端。
通过Http传输Sleuth信息
zipkin-server
的pom.xml
需要引入zipkin-server
和zipkin-autoconfigure-ui
依赖。
<!--注册到Eureka,可以自动发现Zipkin服务-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<!--使用Http方式获取链路信息-->
<dependency>
<groupId>io.zipkin.java</groupId>
<artifactId>zipkin-server</artifactId>
</dependency>
<!--zipkin ui页面-->
<dependency>
<groupId>io.zipkin.java</groupId>
<artifactId>zipkin-autoconfigure-ui</artifactId>
</dependency>
将服务端口设置为8400并注册到eureka,保证其他服务能够发现Zipkin服务。
使用@EnableZipkinServer
启用Zipkin服务。
@EnableEurekaClient
@EnableZipkinServer
@SpringBootApplication
public class ZipkinApplicationStarter {
public static void main(String[] args) {
SpringApplication.run(ZipkinApplicationStarter.class, args);
}
}
启动成功后我们访问http://localhost:8400
就能直接访问Zipkin服务的首页了。
接着改造我们的web-app-service
,user-service
和sms-service
服务使其支持使用Http向Zipkin传输链路信息。pom.xml
去掉之前的spring-cloud-starter-sleuth
依赖增加spring-cloud-sleuth-zipkin
(包含spring-cloud-starter-sleuth
)。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-zipkin</artifactId>
</dependency>
然后需要在application.yml
中配置Zipkin服务的地址和Sleuth的采样率。我们可以使用Eureka发现Zipkin服务或者使用url配置Zipkin的路径。Sleuth的采样率继续使用1,保证所有的请求都会被收集。
spring:
zipkin:
locator:
discovery:
enabled: true
#base-url: http://localhost:8400
sleuth:
sampler:
percentage: 1.0
请求web-app-service
的/user/register
接口(http://localhost:8101/user/register
)
此时我们打开Zipkin首页,查看收集到的信息。图中蓝色的都是正常的请求,红色则是出现错误的请求。
打开一个正常的请求可以看到请求的服务接口及请求时间。如图我们可以看出web-app-service
请求了user-service
之后再请求了sms-service
。
点开user-service
Span显示请求状况
关闭sms-service
服务,并重新发起请求。观察异常请求链路,发现sms-service
关闭后,请求异常后又重试了6次,最终失败。
点击下面红色的Span查看错误信息。从日志中不难发现出现错误的原因是/sms/sendRegister
超时了。
点击Dependencies页面显示服务之间的依赖关系。
点开某个服务可以看到被哪些应用使用
使用SpringCloud Sleuth Stream传输Sleuth信息
使用Http方式可以很容易的实现Sleuth信息的上传,但是当服务访问量较高时Http的势必会影响服务性能。再者,当我们的Zipkin服务端出现异常,这些Sleuth信息也不能正常上传,影响服务的监控。这时候使用消息队列就能轻松的解决高吞吐量和异步解耦这两个问题。
Spring Cloud Stream是一个构建基于消息微服务的框架,通过简单的注解方式就能实现消息的发送。目前已经支持Kafka和RabbitMQ。Spring Cloud Sleuth Stream正是利用Spring Cloud Stream实现使用消息队列传输Sleuth信息。
下面我们演示使用RabbitMQ作为中间件,实现Stream传输Sleuth信息。
在zipkin-server
增加Spring Cloud Sleuth Stream的依赖。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-zipkin-stream</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-rabbit</artifactId>
</dependency>
在启动类上增加@EnableZipkinStreamServer
注解。
@EnableEurekaClient
//@EnableZipkinServer //Http方式传输sleuth信息
@EnableZipkinStreamServer //Stream方式传输Sleuth信息
@SpringBootApplication
public class ZipkinApplicationStarter {
public static void main(String[] args) {
SpringApplication.run(ZipkinApplicationStarter.class, args);
}
}
在application.yaml
文件中增加RabbitMQ的配置
spring:
rabbitmq:
host: 172.16.4.39
username: rabbitmq
password: rabbitmq
服务端完成,启动之。
我们发现RabbitMQ的中已经有了一个叫做sleuth
的exchange。
在web-app-service
,user-service
和sms-service
这三个服务中增加Sleuth Stream采集支持。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-stream</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-rabbit</artifactId>
</dependency>
同样地,增加RabbitMQ地址、用户名和密码配置。这三个应用集成配置中心的时候已经进行了配置。
启动这三个服务,继续请求我们的测试接口,观察到RabbitMQ的web界面有消息的生产和消费,打开Zipkin界面也能看到调用链路信息。
信息存储方式
默认地,Zipkin将链路信息存储在内存中。在正式使用中这种做法是不可取的,随着数据的堆积会大量消耗内存,一旦服务重启所有的监控信息都将随之消失。我们需要将链路信息进行持久化。
打开io.zipkin.java:zipkin-server
pom依赖,我们可以看到Zipkin支持的存储方式有mysql,cassandra,elasticsearch。
下面演示以mysql作为信息的存储方式。我们在Zipkin服务的pom.xml
文件中引入mysql存储支持
<!--使用mysql存储Span信息-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>io.zipkin.java</groupId>
<artifactId>zipkin-autoconfigure-storage-mysql</artifactId>
</dependency>
在application.yaml
中设置数据源、存储方式以及Zipkin DDL。
spring:
datasource:
schema: classpath:ysql.sql
# zipkin-autoconfigure-storage-mysql包中默认包含了HikariDataSource
type: com.zaxxer.hikari.HikariDataSource
url: jdbc:mysql://localhost:3306/test
username: chenxyz
password: 123
# Switch this on to create the schema on startup:
initialize: true
continueOnError: true
# 设置存储类型为mysql
zipkin:
storage:
type: mysql
服务启动的时候会自动生成三张表。
后面我们再传输链路信息,Zipkin会将Span信息持久化到mysql中。