传统单机系统在使用过程当中,若是某个请求响应过慢或是响应出错,开发人员能够清楚知道某个请求出了问题,查看日志能够定位到具体方法。可是在分布式系统中,假若客户端一个请求到达服务器后,由多个服务协做完成。好比:服务A调用服务B,服务B又调用服务C和服务D,服务D又调用服务E,那么想要知道是哪一个服务处理时间过长或是处理异常致使这个请求响应缓慢或中断的话,就须要开发人员一个服务接一个服务的去机器上查看日志,先定位到出问题的服务,再定位出问题的具体地方。试想一下,随着系统愈来愈壮大,服务愈来愈多,一个请求对应处理的服务调用链愈来愈长,这种排查方式何其艰难。为了解决这种问题,便诞生了各类分布式场景中追踪问题的解决方案,zipkin就是其中之一。html
一个独立的分布式追踪系统,客户端存在于应用中(即各服务中),应具有追踪信息生成、采集发送等功能,而服务端应该包含如下基本的三个功能:java
zipkin 总体结构图以下: git
zipkin(服务端)包含四个组件,分别是collector、storage、search、web UI。github
zipkin的客户端主要负责根据应用的调用状况生成追踪信息,而且将这些追踪信息发送至zipkin由收集器接收。各语言支持均不一样,具体能够查看zipkin官网,java语言的支持就是brave。上面结构图中,有追踪器就是指集成了brave。web
在使用zipkin以前,先了解一下Trace和Span这两个基本概念。一个请求到达应用后所调用的全部服务全部服务组成的调用链就像一个树结构(以下图),咱们追踪这个调用链路获得的这个树结构能够称之为Trace。 spring
追踪器位于应用程序上,负责生成相关ID、记录span须要的信息,最后经过传输层传递给服务端的收集器。咱们首先思考下面几个问题:sql
一个 span 表示一次服务调用,那么追踪器一定是被服务调用发起的动做触发,生成基本信息,同时为了追踪服务提供方对其余服务的调用状况,便须要传递本次追踪链路的traceId和本次调用的span-id。服务提供方完成服务将结果响应给调用方时,须要根据调用发起时记录的时间戳与当前时间戳计算本次服务的持续时间进行记录,至此此次调用的追踪span完成,就能够发送给zipkin服务端了。可是须要注意的是,发送span给zipkin collector不得影响这次业务结果,其发送成功与否跟业务无关,所以这里须要采用异步的方式发送,防止追踪系统发送延迟与发送失败致使用户系统的延迟与中断。下图就表示了一次http请求调用的追踪流程(基于zipkin官网提供的流程图): apache
上文对基于zipkin实现分布式追踪系统的原理作了全面的说明,这里简单介绍一下zipkin的安装方法,下载jar包,直接运行。简单粗暴,但要注意必须jdk1.8及以上。其他两种安装方式见官方介绍。json
wget -O zipkin.jar 'https://search.maven.org/remote_content?g=io.zipkin.java&a=zipkin-server&v=LATEST&c=exec'
java -jar zipkin.jar
复制代码
启动成功后,打开浏览器访问zipkin的webUI,输入http://ip:9411/,显示页面以下图。具体使用后面介绍。 后端
java版客户端 Brave的官方文档不多,都在github里。小白当时找的那叫个头疼啊,网上各路大神写的博客中的代码你扒下来换最新的依赖后都会显示那些类被标记为过期,不建议使用。
<?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">
<modelVersion>4.0.0</modelVersion>
<groupId>com.ycg</groupId>
<artifactId>zipkin_client</artifactId>
<version>1.0-SNAPSHOT</version>
<name>zipkin_client</name>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>6</source>
<target>6</target>
</configuration>
</plugin>
</plugins>
</build>
<packaging>jar</packaging>
<properties>
<java.version>1.8</java.version>
<spring-boot.version>2.1.1.RELEASE</spring-boot.version>
<brave.version>5.6.0</brave.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>io.zipkin.brave</groupId>
<artifactId>brave-bom</artifactId>
<version>${brave.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<!-- zipkin客户端依赖 -->
<dependency>
<groupId>io.zipkin.brave</groupId>
<artifactId>brave</artifactId>
</dependency>
<dependency>
<groupId>io.zipkin.reporter2</groupId>
<artifactId>zipkin-sender-okhttp3</artifactId>
</dependency>
<!-- 添加记录MVC的类、方法名到span的依赖 -->
<dependency>
<groupId>io.zipkin.brave</groupId>
<artifactId>brave-instrumentation-spring-webmvc</artifactId>
</dependency>
<!-- 添加brave的httpclient依赖 -->
<dependency>
<groupId>io.zipkin.brave</groupId>
<artifactId>brave-instrumentation-httpclient</artifactId>
</dependency>
<!-- 集成Brave上下文的log -->
<dependency>
<groupId>io.zipkin.brave</groupId>
<artifactId>brave-context-slf4j</artifactId>
</dependency>
</dependencies>
</project>
复制代码
package com.ycg.zipkin_client;
import brave.CurrentSpanCustomizer;
import brave.SpanCustomizer;
import brave.Tracing;
import brave.context.slf4j.MDCScopeDecorator;
import brave.http.HttpTracing;
import brave.httpclient.TracingHttpClientBuilder;
import brave.propagation.B3Propagation;
import brave.propagation.ExtraFieldPropagation;
import brave.propagation.ThreadLocalCurrentTraceContext;
import brave.servlet.TracingFilter;
import brave.spring.webmvc.SpanCustomizingAsyncHandlerInterceptor;
import org.apache.http.impl.client.CloseableHttpClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.client.RestTemplateCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import zipkin2.Span;
import zipkin2.reporter.AsyncReporter;
import zipkin2.reporter.Sender;
import zipkin2.reporter.okhttp3.OkHttpSender;
import javax.servlet.Filter;
/**
* 针对mvc controller 和 restTemplate 的 zipkin客户端配置
*/
@Configuration
@Import(SpanCustomizingAsyncHandlerInterceptor.class)
public class ZipkinClientConfiguration implements WebMvcConfigurer {
/**
* 配置如何向 zipkin 发送 span
*/
@Bean
Sender sender() {
// 注意这里更换为本身安装的zipkin所在的主机IP
return OkHttpSender.create("http://10.150.27.36:9411/api/v2/spans");
}
/**
* 配置如何把 span 缓冲到给 zipkin 的消息
*/
@Bean
AsyncReporter<Span> spanReporter() {
return AsyncReporter.create(sender());
}
/**
* 配置跟踪过程当中的Trace信息
*/
@Bean
Tracing tracing(@Value("${spring.application.name}") String serviceName) {
return Tracing.newBuilder()
.localServiceName(serviceName) // 设置节点名称
.propagationFactory(ExtraFieldPropagation.newFactory(B3Propagation.FACTORY, "user-name"))
.currentTraceContext(ThreadLocalCurrentTraceContext.newBuilder()
.addScopeDecorator(MDCScopeDecorator.create()) // puts trace IDs into logs
.build()
)
.spanReporter(spanReporter()).build();
}
/** 注入可定制的Span */
@Bean
SpanCustomizer spanCustomizer(Tracing tracing) {
return CurrentSpanCustomizer.create(tracing);
}
/** 决定如何命名和标记span。 默认状况下,它们的名称与http方法相同 */
@Bean
HttpTracing httpTracing(Tracing tracing) {
return HttpTracing.create(tracing);
}
/** 导入过滤器,该过滤器中会为http请求建立span */
@Bean
Filter tracingFilter(HttpTracing httpTracing) {
return TracingFilter.create(httpTracing);
}
/**
* 导入 zipkin 定制的 RestTemplateCustomizer
*/
@Bean
RestTemplateCustomizer useTracedHttpClient(HttpTracing httpTracing) {
final CloseableHttpClient httpClient = TracingHttpClientBuilder.create(httpTracing).build();
return new RestTemplateCustomizer() {
@Override public void customize(RestTemplate restTemplate) {
restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory(httpClient));
}
};
}
@Autowired
SpanCustomizingAsyncHandlerInterceptor webMvcTracingCustomizer;
/** 使用应用程序定义的Web标记装饰服务器span */
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(webMvcTracingCustomizer);
}
}
复制代码
<dependency>
<groupId>com.ycg</groupId>
<artifactId>zipkin_client</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
复制代码
@SpringBootApplication
@Import(ZipkinClientConfiguration.class)
public class Service1Application {
public static void main(String[] args) {
SpringApplication.run(Service1Application.class, args);
}
}
复制代码
@EnableAutoConfiguration
@RestController
public class Service1Controller {
private RestTemplate restTemplate;
@Autowired Service1Controller(RestTemplateBuilder restTemplateBuilder) {
this.restTemplate = restTemplateBuilder.build();
}
@GetMapping(value = "/service1")
public String getService() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "service1 sleep 100ms ->" + restTemplate.getForObject("http://localhost:8882/service2",String.class);
}
}
复制代码
到这里,就完成了一个springboot整合zipkin简单的demo,分别启动三个boot应用后,在浏览器访问http://localhost:8881/service1,浏览器显示以下图:
打开zipkin-webUI,点击查询,即可以查到刚才请求的追踪链路,以下图。
继续点击查到的链路信息,即可查看该条追踪链路的详细信息。这里采用缩进的形式展现了整条调用链路,而且再每一个调用后代表了所花费时间。点击右上角json按钮,便能看到本次trace的json数据。
一次追踪链路会包含不少个span,所以一个trace即是一个数组,其标准的json结构以下:
[
{
"traceId": "string", // 追踪链路ID
"name": "string", // span名称,通常为方法名称
"parentId": "string", // 调用者ID
"id": "string", // spanID
"kind": "CLIENT", // 替代zipkin v1的注解中的四个核心状态,详细介绍见下文
"timestamp": 0, // 时间戳,调用时间
"duration": 0, // 持续时间-调用的服务所消耗的时间
"debug": true,
"shared": true,
"localEndpoint": { // 本地网络节点上下文
"serviceName": "string",
"ipv4": "string",
"ipv6": "string",
"port": 0
},
"remoteEndpoint": { // 远端网络节点上下文
"serviceName": "string",
"ipv4": "string",
"ipv6": "string",
"port": 0
},
"annotations": [ // value一般是缩写代码,对应的时间戳表示代码标记事件的时间
{
"timestamp": 0,
"value": "string"
}
],
"tags": { // span的上下文信息,好比:http.method、http.path
"additionalProp1": "string",
"additionalProp2": "string",
"additionalProp3": "string"
}
}
]
复制代码
zipkin V1 之 Annotation V1 时Annotation 用于记录一个事件,事件由value标识,事件发生时间则记录对应的时间戳。一些核心注解核心注解用于定义一个请求的开始和结束。主要是以下四种注解:
zipkin V2 之 Kind V2 使用Span.Kind替代了V1的几个表示请求开始与结束的核心注解。kind一共有四种状态,其为不一样状态时,timestamp、duration、remoteEndpoint表明的意义均不相同。
timestamp是请求被发送的时刻,至关于v1中注解 cs。
duration表明发送请求后,接收到服务端响应前的持续时间,也就是整个请求所消耗的时间。
remoteEndpoint表示被调用方的网络节点信息。
timestamp是服务端接到请求并准备开始处理它的时间,至关于v1中的sr。
duration表明服务端接到请求后、发送响应前的持续时间,也就是服务端的净处理时间。
remoteEndpoint表示调用方的网络节点信息。
timestamp是消息被发送的时刻。
duration表明发送方发送后,消息队列结束到消息前的延迟时间,好比批处理的场景。
remoteEndpoint表示消息队列的网络节点信息。
timestamp是消息被消息队列接收到的时刻。
duration表明消息被消息队列接收到,被消费者消费前的持续时间,好比消息积压的场景。
remoteEndpoint表示消费者节点信息,未知则表示service name。
V1 针对消息队列也有ms、mr等注解,这里就再也不详细介绍了。小白以为kind这种替换后,整个追踪链路更为清晰直观,或许这也是zipkin的考虑之一吧。
相信看到这里的小伙伴回头再看demo中链路的json数据,应该能够明白具体的意思了。小白这里再梳理一下。追踪链路的JSON数据以下(建议直接跳过数据看下面分析):
[
{
"traceId": "3857b4a56c99e9f8",
"parentId": "7dd11a047eb02622",
"id": "e5427222edb62a7c",
"kind": "SERVER",
"name": "get /service3",
"timestamp": 1547458424863333,
"duration": 409599,
"localEndpoint": {
"serviceName": "server3",
"ipv4": "172.30.22.138"
},
"remoteEndpoint": {
"ipv4": "127.0.0.1",
"port": 52845
},
"tags": {
"http.method": "GET",
"http.path": "/service3",
"mvc.controller.class": "Service3Controller",
"mvc.controller.method": "getService"
},
"shared": true
},
{
"traceId": "3857b4a56c99e9f8",
"parentId": "7dd11a047eb02622",
"id": "e5427222edb62a7c",
"kind": "CLIENT",
"name": "get",
"timestamp": 1547458424756985,
"duration": 520649,
"localEndpoint": {
"serviceName": "server2",
"ipv4": "172.30.22.138"
},
"tags": {
"http.method": "GET",
"http.path": "/service3"
}
},
{
"traceId": "3857b4a56c99e9f8",
"parentId": "3857b4a56c99e9f8",
"id": "7dd11a047eb02622",
"kind": "SERVER",
"name": "get /service2",
"timestamp": 1547458424446556,
"duration": 880044,
"localEndpoint": {
"serviceName": "server2",
"ipv4": "172.30.22.138"
},
"remoteEndpoint": {
"ipv4": "127.0.0.1",
"port": 52844
},
"tags": {
"http.method": "GET",
"http.path": "/service2",
"mvc.controller.class": "Service2Controller",
"mvc.controller.method": "getService"
},
"shared": true
},
{
"traceId": "3857b4a56c99e9f8",
"parentId": "3857b4a56c99e9f8",
"id": "7dd11a047eb02622",
"kind": "CLIENT",
"name": "get",
"timestamp": 1547458424271786,
"duration": 1066836,
"localEndpoint": {
"serviceName": "server1",
"ipv4": "172.30.22.138"
},
"tags": {
"http.method": "GET",
"http.path": "/service2"
}
},
{
"traceId": "3857b4a56c99e9f8",
"id": "3857b4a56c99e9f8",
"kind": "SERVER",
"name": "get /service1",
"timestamp": 1547458424017344,
"duration": 1358590,
"localEndpoint": {
"serviceName": "server1",
"ipv4": "172.30.22.138"
},
"remoteEndpoint": {
"ipv6": "::1",
"port": 52841
},
"tags": {
"http.method": "GET",
"http.path": "/service1",
"mvc.controller.class": "Service1Controller",
"mvc.controller.method": "getService"
}
}
]
复制代码
咱们从下往上看,这才是请求最开始的地方。首先看最下面的span(3857b4a56c99e9f8)。请求(http://localhost:8881)是由浏览器发出,那么当请求到达服务1时,做为服务端便会生成kind为SERVER的span,其中duration即是本次请求到后端后的净处理时间,localEndpoint是server1的节点信息,remoteEndpoint的调用方也就是浏览器的节点信息。
接着服务1须要调用服务2的服务,这时服务1是做为客户端发出请求的。所以会记录出从下往上第二个span(7dd11a047eb02622),一个客户端span,也就是kind=CLIENT。localEndpoint仍是本身,同时tag里添加了发出的请求信息,duration表示发出/service2的请求后,到接收到server2的响应所消耗的时间。再往上span(7dd11a047eb02622),就是server2接收到server1的请求后记录的SERVER span。剩下的同理,小白就很少说了。
到这里小白就介绍完了基于zipkin实现分布式追踪系统的基本原理与实现,固然这只是一个入门,追踪信息是全量收集仍是采样收集,设置什么样的采样频率,异步发送span使用http仍是kafka,这些问题都是须要在生产环境中根据实际场景综合考量的。就本文而言,小白以为只要你仔细阅读了,认真思考了,必定仍是收获很多的,固然有深刻研究的小伙伴除外。后续小白会深刻Brave的源码了解具体的追踪实现,若有错误,也请多多拍砖多多交流。另,画图、码字、梳理知识不易,如要转载,请注明出处。