微服务架构是一个分布式架构,它按业务划分服务单元,一个分布式系统往往有很多个服务单元。由于服务单元数量众多,业务的复杂性,如果出现了错误和异常,很难去定位。主要体现在,一个请求可能需要调用很多个服务,而内部服务的调用复杂性,决定了问题难以定位。所以微服务架构中,必须实现分布式链路追踪,去跟进一个请求到底有哪些服务参与,参与的顺序又是怎样的,从而达到每个请求的步骤清晰可见,出了问题,很快定位。
Sleuth的意思是大侦探,顾名思义,侦探就是查找信息搜集线索,顺着线索链找到所有上下游的关联,这恰好正是Seluth在Spring Cloud中的工作
借助Sleuth的链路追踪能力,我们还可以完成一些其他的任务,比如说:
每一个微服务都有自己的Log组件(slf4j,lockback等各不相同),当我们集成了Sleuth之后,它便会将链路信息传递给底层Log组件,同时Log组件会在每行Log的头部输出这些数据,这个埋点动作主要会记录两个关键信息:
我们需要把链路追踪信息加入到业务Log中,这些业务Log是我们研发人员写在具体服务里的,而不是Sleuth单独打印的log,因此Sleuth需要找到一个合适的切入点,让底层Log组件可以获取链路信息,并且我们的业务代码还不需要做任何改动。
如果有对Log框架做过深度定制的同学可能一下就能想到实现方式,就是使用MDC + Format Pattern的方式输出信息,我们先来看一下Log组件打印信息到文件的过程:
当我们使用"log.info"打印日志的时候,Log组件会将“写入”动作封装成一个LogEvent事件,而这个事件的具体表现形式由Log Format和MDC共同控制,Format决定了Log的输出格式,而MDC决定了输出什么内容。
Log组件定义了日志输出格式,这和我们平时使用“String.format”的方式差不多,集成了Sleuth后的Log输出格式是下面这个样子:
%5p [sleuth-traceA,%X{X-B3-TraceId:-},%X{X-B3-SpanId:-},%X{X-Span-Export:-}]
同学们发现上面有几个X开头的占位符,这就是我们需要写入Log的链路追踪信息
MDC是通过InheritableThreadLocal来实现的,它可以携带当前线程的上下文信息。它的底层是一个Map结构,存储了一系列Key-Value的值。Sleuth就是借助Spring的AOP机制,在方法调用的时候配置了切面,将链路追踪数据加入到了MDC中,这样在打印Log的时候,就能从MDC中获取这些值,填入到Log Format中的占位符里。
由于MDC基于InheritableThreadLocal而不是ThreadLocal实现,因此假如在当前线程中又开启了新的子线程,那么子线程依然会保留父线程的上下文信息。
一个Span可以包含多个Annotation,每个Annotation表示一个特殊事件,比如:
每个Annotation同样有一个时间戳字段,这样我们就能分析一个Span内部每个事件的起始和结束时间。这里我选取了Spring Cloud官网的一张图来展示Trace、Span和Annotation的关系。
我们知道了Trace ID和Span ID,眼下的问题就是如何在不同服务节点之间传递这些ID。我想这一步大家很容易猜到是怎么做的,因为在Eureka的服务治理下所有调用请求都是基于HTTP的,那我们的链路追踪ID也一定是HTTP请求中的一部分。可是把ID加在HTTP哪里好呢?Body里可以吗?NoNoNo,一来GET请求压根就没有Body,二来加入Body还有可能影响后台服务的反序列化。那加在URL后面呢?似乎也不妥,因为某些服务组件对URL的长度可能做了限制(比如Nginx可以设置最大URL长度)。
那剩下的只有Header了!Sleuth正是通过Filter向Header中添加追踪信息,我们来看下面表格中Header Name和Trace Data的对应关系:
HTTP Header Name | Trace Data | |
---|---|---|
X-B3-TraceId | Trace ID | 链路全局唯一ID |
X-B3-SpanId | Span ID | 当前Span的ID |
X-B3-ParentSpanId | Parent Span ID | 前一个Span的ID |
X-Span-Export | Can be exported for sampling or not | 是否可以被采样 |
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-sleuthartifactId>
dependency>
dependencies>
@EnableDiscoveryClient
@SpringBootApplication
public class SleuthTraceA{
public static void main(String[] args){
new SpringApplicationBuilder(SleuthTraceA.class)
.web(WebApplicationType.SERVLET)
.run(args);
}
@LoadBalanced
@Bean
public RestTemplate lb(){
return new RestTemplate();
}
}
spring.application.name=sleuth-traceA
server.port=62000
eureka.client.serviceUrl.defaultZone=http://localhost:20000/eureka/
logging.file=${spring.application.name}.log
#日志采样率, 1:所有的日志都会被采样
spring.sleuth.sampler.probability=1
info.app.name=sleuth-traceA
info.app.description=test
management.security.enabled=false
management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always
<configuration>
<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{HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}" />
<appender name="consoleLog" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFOlevel>
filter>
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}pattern>
<charset>utf8charset>
encoder>
appender>
<root level="DEBUG">
root>
configuration>
//lombok的注解 用来打印日志,用其他的方式打印日志也是一样的
@Slf4j
@RestController
public class ControllerA{
@Autowired
private RestTemplate restTemplate;
@GetMapping(value = "/traceA")
public String traceA(){
log.info("--------Trace A");
return restTemplate.getForEntity("http://sleuth-traceB/traceB",String.class).getBody();
}
}
1.修改以下配置文件中的3个参数
spring.application.name=sleuth-traceB
server.port=62001
info.app.name=sleuth-traceB
2.修改一下Controller
//lombok的注解 用来打印日志,用其他的方式打印日志也是一样的
@Slf4j
@RestController
public class ControllerB{
@Autowired
private RestTemplate restTemplate;
@GetMapping(value = "/traceB")
public String traceB(){
log.info("--------Trace B");
return "traceB";
}
}
1.启动服务发现中心(示例中没有需要自己配置下)
2.启动traceB和traceA
3.调用/traceA接口参看日志
Zipkin是一套分布式实时数据追踪系统,它主要关注的是时间维度的监控数据,比如某个调用链路下各个阶段所花费的时间,同时还可以从可视化的角度帮我们梳理上下游系统之间的依赖关系。
Sleuth为什么需要一个搭档?大家难道没发现Sleuth空有一身本领,可是没个页面可以show出来吗?而且Sleuth似乎只是自娱自乐在log里埋点,却没有一个汇聚信息的能力,不方便对整个集群的调用链路进行分析。Sleuth目前的情形就像Hystrix一样,也需要一个类似Turbine的组件做信息聚合+展示的功能。在这个背景下,Zipkin就是一个不错的选择。
Zipkin的主要作用是收集Timing维度的数据,以供查找调用延迟等线上问题。所谓Timing其实就是开始时间+结束时间的标记,有了这两个时间信息,我们就能计算得出调用链路每个步骤的耗时。Zipkin的核心功能有以下两点
Zipkin分为服务端和客户端,服务端是一个专门负责收集数据、查找数据的中心Portal,而每个客户端负责把结构化的Timing数据发送到服务端,供服务端做索引和分析。这里我们重点关注一下“Timing数据”到底用来做什么,前面我们说过Zipkin主要解决调用延迟情况的线上排查,它通过收集一个调用链上下游所有工作单元的独立用时,Zipkin就能知道每个环节在服务总用时中所占的比重,再通过图形化界面的形式,让开发人员知道性能瓶颈出在哪里。
Zipkin提供了多种维度的查找功能用来检索Span的耗时,最直观的是通过Trace ID查找整个Trace链路上所有Span的前后调用关系和每阶段的用时,还可以根据Service Name或者访问路径等维度进行查找
<dependencies>
<dependency>
<groupId>io.zipkin.javagroupId>
<artifactId>zipkin-serverartifactId>
<version>2.8.4version>
dependency>
<dependency>
<groupId>io.zipkin.javagroupId>
<artifactId>zipkin-autoconfigure-uiartifactId>
<version>2.8.4version>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<configuration>
<mainClass>>com.test.spring.ZipkinApplicationmainClass>
configuration>
<executions>
<execution>
<goals>
<goal>repackagegoal>
goals>
execution>
executions>
plugin>
plugins>
build>
//选哪个没有被标记为Deprecated的
@EnableZipkinServer
@SpringBootApplication
public class ZipkinApplication{
public static void main(String[] args){
new SpringApplicationBuilder(ZipkinApplication.class)
.web(WebApplicationType.SERVLET)
.run(args);
}
}
spring.application.name=zipkin-server
server.port=62100
#允许bean重载
spring.main.allow-bean-definition-overriding=true
management.metrics.web.server.auto-time-requests=false
localhost:62100/zipkin/
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-zipkinartifactId>
dependency>
#zipkin的地址
spring.zipkin.base-url=http://localhost:62100
#使用http上传数据到zipkin
#因为有可能后引入bus依赖导入了rabbitmq的依赖,zipkin会自动切换到RabbitMq上
spring.zipkin.sender.type=web
1.使用docker下载elk镜像(很慢需要耐心)
docker pull sebp/elk
2.创建Docker容器(第一次使用的时候才需要创建,启动很慢 5-10分钟)
#kibana的端口 ElasticSearch端口 LogStash端口 容器名称 启动的镜像
docker run -p 5601:5601 -p 9200:9200 -p5044:5044 -e ES_MIN_MEM=128m -e ES_MAX_MEM=1025m -it --name elk sebp/elk
3.再次启动的命令docker容器(启动很慢 5-10分钟)
docker start elk
4. 进入docker容器:
docker exec -it elk /bin/bash
5.修改配置文件
配置文件位置: /etc/logstash/conf.d/02-beats-input.conf
将内容全部删除,替换成下面的配置
input{
tcp{
port =>5044
codec => json_lines
}
}
output{
elasticsearch{
hosts => ["localhost:9200"]
}
}
6.重启docker容器(启动很慢 5-10分钟)
docker restart elk
7.访问Kibana
http://localhost:5601/
1.修改之前写的sleuth-traceA和B,修改pom文件
<dependency>
<groupId>net.logstash.logbackgroupId>
<artifactId>logstash-logback-encoderartifactId>
<version>5.2version>
dependency>
2.修改日志配置文件logback-spring.xml,下面是完整的配置,添加logstashLog了
<configuration>
<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{HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}" />
<appender name="consoleLog" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFOlevel>
filter>
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}pattern>
<charset>utf8charset>
encoder>
appender>
<appender name="logstashLog" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<destination>127.0.0.1:5044destination>
<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
<providers>
<timestamp>
<timeZone>UTCtimeZone>
timestamp>
<pattern>
<pattern>
{
"severity":"%level",
"service":"${springAppName:-}",
"trace":"%X{X-B3-TraceId:-}",
"span":"%X{X-B3-SpanId:-}",
"exportable":"%X{X-SpanId-Export:-}",
"pid":"${PID:-}",
"thread":"%thread",
"class":"%logger{40}",
"rest":"%message"
}
pattern>
pattern>
providers>
encoder>
appender>
<root level="DEBUG">
root>
configuration>
3.配置完成,发起一个调用,查看Kibana