最近升级改造我们链路跟踪系统Log2,花了将近一周时间调研一些开源的链路跟踪系统,在此调研过程中,做了一些笔记和总结分享出来,若有误请指教。
《分布式链路跟踪1: 理论知识》
《分布式链路跟踪2:Zipkin实践》
《分布式链路跟踪3:skywalking原理和实践》
《分布式链路跟踪4:自研组件Log2》
《架构设计(12) 分布式链路跟踪》提到,Google的Dapper论文,介绍了如何进行服务追踪分析。其基本思路是在服务调用的请求和响应中加入ID,标明上下游请求的关系。利用这些信息,可以可视化地分析服务调用链路和服务间的依赖关系。
Zipkin是基于 Dapper 论文实现,由Twitter公司开发并开源一个分布式的跟踪系统。Zipkin支持多种语言包括JavaScript,Python,Java, Scala, Ruby, C#, Go等。其中Java由多种不同的库来支持。
官网:https://github.com/openzipkin/zipkin/
Zipkin使用Trace结构表示一次请求的跟踪,从请求到达跟踪系统的边界开始,到被跟踪系统返回响应为止的过程。一次请求可能由后台的若干服务负责处理,每个服务的处理是一个Span,Span之间有依赖关系,Trace就是树结构的Span集合;.包含一系列的span,它们组成了一个树型结构.
每个 trace中会调用若干个服务,为了记录调用了哪些服务,以及每次调用的消耗时间等信息,在每次调用服务时,埋入一个调用记录,称为一个“span”。
span是基本的工作单元,包含了一些描述信息:id,parentId,name,timestamp,duration,annotations等
最开始的初始Span称为根span,此span中span id和 trace id值相同。
标注用于及时记录事件;有一组核心注释用于定义RPC请求的开始和结束;
旨在提供有关RPC的额外信息,可以存放用户自定义信息,比如:sessionID、userID、userIP、异常等。
一个完成trace信息如下:
traceId:标记一次请求的跟踪,相关的Spans都有相同的traceId;
id:span id;
name:span的名称,一般是接口方法的名称;
parentId:可选的id,当前Span的父Span id,通过parentId来保证Span之间的依赖关系,如果没有parentId,表示当前Span为根Span;
timestamp:Span创建时的时间戳,使用的单位是微秒(而不是毫秒),所有时间戳都有错误,包括主机之间的时钟偏差以及时间服务重新设置时钟的可能性,出于这个原因,Span应尽可能记录其duration;
duration:持续时间使用的单位是微秒(而不是毫秒);
{
"traceId": "423f62ed10f826bd",
"id": "09d324b8d0563b0c",
"name": "http:/hello",
"parentId": "423f62ed10f826bd",
"timestamp": 1593490361270000,
"duration": 19000,
"annotations": [
{
"timestamp": 1593490361270000,
"value": "cs",
"endpoint": {
"serviceName": "zipkin-client",
"port": 8080
}
},
{
"timestamp": 1593490361278000,
"value": "sr",
"endpoint": {
"serviceName": "zipkin-service",
"port": 9081
}
},
{
"timestamp": 1593490361289000,
"value": "cr",
"endpoint": {
"serviceName": "zipkin-client",
"port": 8080
}
},
{
"timestamp": 1593490361290000,
"value": "ss",
"endpoint": {
"serviceName": "zipkin-service",
"port": 9081
}
}
],
"binaryAnnotations": [
{
"key": "http.host",
"value": "localhost",
"endpoint": {
"serviceName": "zipkin-client",
"port": 8080
}
},
{
"key": "http.method",
"value": "GET",
"endpoint": {
"serviceName": "zipkin-client",
"port": 8080
}
}
]
}
跟踪器(Tracer):位于你的应用程序中,并记录发生的操作的时间和元数据,提供了相应的类库,对用户的使用来说是透明的,收集的跟踪数据称为Span;
报告器 (Reporter):将跟踪数据发送到Zipkin server的应用程序组件称为Reporter. 比如通过MQ发送,即MQ为reporter,或者跟踪数据先落地,通过logstash发送,logstash为reporter。
Transport传输:Reporter通过几种传输方式之一将追踪数据发送到Zipkin收集器(collector),有三个主要的传输方式:HTTP, Kafka和Scribe;
Zipkin server:collector收到数据后将跟踪数据进行存储(storage),由API查询存储以向UI提供数据。
Zipkin server有4个组件组成:collector,storage,search,web UI
collector:一旦跟踪数据到达Zipkin collector守护进程,它将被验证,存储和索引,以供Zipkin收集器查找;
storage:Zipkin最初数据存储在Cassandra上,因为Cassandra是可扩展的,具有灵活的模式,并在Twitter中大量使用;但是这个组件可插入,除了Cassandra之外,还支持ElasticSearch和MySQL; 存储,zipkin默认的存储方式为in-memory,即不会进行持久化操作。如果想进行收集数据的持久化,可以存储数据在Cassandra,因为Cassandra是可扩展的,有一个灵活的模式,并且在Twitter中被大量使用,我们使这个组件可插入。除了Cassandra,我们原生支持ElasticSearch和MySQL。其他后端可能作为第三方扩展提供。
search:一旦数据被存储和索引,我们需要一种方法来提取它。查询守护进程提供了一个简单的JSON API来查找和检索跟踪,主要给Web UI使用;
web UI:创建了一个GUI,为查看痕迹提供了一个很好的界面;Web UI提供了一种基于服务,时间和注释查看跟踪的方法。
架构图如下:
一个应用的代码发起HTTP get请求,经过Trace框架拦截,然后
1)、把当前调用链的Trace信息添加到HTTP Header里面
2)、记录当前调用的时间戳
3)、发送HTTP请求,把trace相关的header信息携带上
4)、调用结束之后,记录当前调用耗时
5)、然后把上面流程产生的 信息汇集成一个span,把这个span信息上传到zipkin的Collector模块
该信息来源:https://www.jianshu.com/p/d75226c8f943
┌─────────────┐ ┌───────────────────────┐ ┌─────────────┐ ┌──────────────────┐
│ User Code │ │ Trace Instrumentation │ │ Http Client │ │ Zipkin Collector │
└─────────────┘ └───────────────────────┘ └─────────────┘ └──────────────────┘
│ │ │ │
┌─────────┐
│ ──┤GET /foo ├─▶ │ ────┐ │ │
└─────────┘ │ record tags
│ │ ◀───┘ │ │
────┐
│ │ │ add trace headers │ │
◀───┘
│ │ ────┐ │ │
│ record timestamp
│ │ ◀───┘ │ │
┌─────────────────┐
│ │ ──┤GET /foo ├─▶ │ │
│X-B3-TraceId: aa │ ────┐
│ │ │X-B3-SpanId: 6b │ │ │ │
└─────────────────┘ │ invoke
│ │ │ │ request │
│
│ │ │ │ │
┌────────┐ ◀───┘
│ │ ◀─────┤200 OK ├─────── │ │
────┐ └────────┘
│ │ │ record duration │ │
┌────────┐ ◀───┘
│ ◀──┤200 OK ├── │ │ │
└────────┘ ┌────────────────────────────────┐
│ │ ──┤ asynchronously report span ├────▶ │
│ │
│{ │
│ "traceId": "aa", │
│ "id": "6b", │
│ "name": "get", │
│ "timestamp": 1483945573944000,│
│ "duration": 386000, │
│ "annotations": [ │
│--snip-- │
└────────────────────────────────┘
1、brave则是zipkin官方出品的Java语言的链路数据采集插件:官方提供的brave插件列表非常多,基本上涵盖了日常用到的链路:http、rpc、db等。
2、spring cloud提供了spring-cloud-sleuth-zipkin来方便集成zipkin实现zipkin client追踪器,该jar包可以通过spring-cloud-starter-zipkin依赖来引入。Spring Cloud Sleuth是采集应用Span、Trace等信息,通过HTTP Request,向Zipkin Server发送采集信息等全部自动完成。
我们新建三个项目。一个zipkinServer。另外两个是普通的业务应用,分别叫service和client。
zipkinServer功能SHI 收集调用数据,并展示;
service对外暴露接口;
client : 调用service接口;
Zipkin的使用比较简单,官网有说明几种方式:
1、容器 :Docker Zipkin
项目能够建立docker
镜像,提供脚本和docker-compose.yml
来启动预构建的图像。最快的开始是直接运行最新镜像:
docker run -d -p 9411:9411 openzipkin/zipkin
2、下载jar :最快的方法是下载一个zipkinserver.xx.jar并运行,详情参看
3、使用源码代码运行
1)、新建Spring Boot项目,工程取名为zipkin-server,在其pom引入依赖:
spring boot 版本:
org.springframework.boot
spring-boot-starter-parent
1.4.3.RELEASE
spirng cloud 版本:
org.springframework.cloud
spring-cloud-dependencies
Camden.SR4
pom
import
我们使用spirng cloud 版本:
4.0.0
com.demo.zipkin
demo-zipkin-server
1.0-SNAPSHOT
jar
demo-zipkin-server
zipkin-server project for Spring Boot
UTF-8
1.8
2.7
org.springframework.cloud
spring-cloud-dependencies
Camden.SR4
pom
import
io.zipkin.java
zipkin-server
io.zipkin.java
zipkin-autoconfigure-ui
org.springframework.boot
spring-boot-maven-plugin
2)、在其程序入口类, 加上注解@EnableZipkinServer,开启ZipkinServer的功能:
package com.demo.zipkin.web;
/**
* Created by huangguisu on 2019/10/28.
*/
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ImportResource;
import zipkin.server.EnableZipkinServer;
@SpringBootApplication
@EnableZipkinServer
@ImportResource(locations = {"classpath:applicationContext.xml"})
public class ZipkinServerApplication {
public static void main(String[] args) throws Exception {
SpringApplication.run(ZipkinServerApplication.class, args);
}
}
3)、在配置文件application.yml指定,配置Zipkin服务端口、名称等:
server.port=9411
spring.application.name=demo-zipkin-server
如果启动报错:
Exception in thread "main" java.lang.AbstractMethodError: org.springframework.boot.context.config.ConfigFileApplicationListener.supportsSourceType(Ljava/lang/Class;)Z
去掉工程里面的pom.xml文
parent
依赖spring-boot-starter-parent
,因为已经使用dependencyManagement配置,
org.springframework.cloud spring-cloud-dependencies Camden.SR4 pom import 如果再添加上
parent
依赖的话就会造成spring boot版本冲突,产生错误。
4)访问http://localhost:9411/:
1)、增加依赖
org.springframework.cloud
spring-cloud-starter-zipkin
2)、client的application.properties设置zipkin server
server.port=8080
server.address=0.0.0.0
spring.application.name=zipkin-client
spring.zipkin.base-url=http://localhost:9411
spring.sleuth.sampler.percentage=1
3)client启动代码:
package com.demo.zipkin.web;
/**
* Created by huangguisu on 2019/10/28.
*/
import org.springframework.boot.SpringApplication;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.client.RestTemplate;
import org.springframework.context.annotation.Bean;
@RestController
@SpringBootApplication
public class ClientApplication {
// @Autowired
// RestTemplate restTemplate;
@Bean
RestTemplate restTemplate(){
return new RestTemplate();
}
@GetMapping("/hello")
public String hello(){
return this.restTemplate().getForEntity("http://localhost:9081/hello",String.class).getBody();
}
public static void main(String[] args) {
SpringApplication.run(ClientApplication.class, args);
}
}
4)service的application.properties设置zipkin server
server.port=9081
server.address=0.0.0.0
spring.application.name=zipkin-client
spring.zipkin.base-url=http://localhost:9411
spring.sleuth.sampler.percentage=1
5)service启动代码:
package com.demo.zipkin.web;
/**
* Created by huangguisu on 2019/10/28.
*/
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ImportResource;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.GetMapping;
@SpringBootApplication
@RestController
@ImportResource(locations = {"classpath:applicationContext.xml"})
public class SerivceApplication {
@GetMapping("/hello")
public String hello(){
return "Hello World";
}
public static void main(String[] args) throws Exception {
SpringApplication.run(SerivceApplication.class, args);
}
}
然后我们启动client和service后,访问:http://localhost:8080/hello
可以看到如下依赖关系:
spring-cloud-sleuth收集信息是有一定的比率的,默认的采样率是0.1.可能在刚刚启动时候,看不到数据。
配置此值的方式在配置文件中增加spring.sleuth.sampler.percentage参数配置(如果不配置默认0.1),如果我们调大此值为1,可以看到信息收集就更及时。
如果spring.sleuth.sampler.percentage是1,由于采样频率过高,如果一次请求的链路有5个,那就需要5次http请求zipkin server,这种方式追踪服务调用链路会给我们业务程序性能带来一定的影响。
#sleuth采样率,默认为0.1,值越大收集越及时,但性能影响也越大
spring.sleuth.sampler.percentage=1
span细节指标展示:
使用默认zipkin方式的问题:
问题1:zipkin客户端向zipkin-server程序发送数据使用的是http的方式通信,每次发送的时候涉及到连接和发送过程。
问题2:当我们的zipkin-server程序关闭或者重启过程中,因为客户端收集信息的发送采用http的方式会被丢失。
针对以上两个问题,可以改进的办法是:
1、通信采用socket或者其他效率更高的通信方式。
2、客户端数据的发送尽量减少业务线程的时间消耗,采用异步等方式发送收集信息。
3、客户端与zipkin-server之间增加缓存类的中间件,例如redis、MQ等,在zipkin-server程序挂掉或重启过程中,客户端依旧可以正常的发送自己收集的信息。
相信采用以上三种方式会很大的提高我们的效率和可靠性。其实spring-cloud已经为我们提供采用MQ或redis等其他的采用socket方式通信,利用消息中间件或数据库缓存的实现方式。
这些配置跟版本有关,仅作参考:
spring.zipkin.base-url: http://192.168.84.146:9411/ #指定zipkin的服务器
#默认的service name是读取spring.application.name的值.
spring.zipkin.service.name=service1
#支持通过服务发现定位主机名
spring.zipkin.locator.discovery.enabled: true
#支持压缩的能力。默认是false。开启压缩,这样在发送给zipkin server之前会先把数据进行压缩
spring.zipkin.compression.enabled=true
#spring2.0以上 0.1-1.0 1=100%即集服务的全部追踪数据
spring.zipkin.sleuth.sampler.probability: 1.0
#spring2.0以下 0.1-1.0 1=100%即采集服务的全部追踪数据
spring.zipkin.sleuth.percentage: 1
spring.zipkin.sleuth.stream.enabled: true
#Spring Cloud Sleuth本身就整合了Spring Integration。它发布/订阅事件都是会创建span。可以设置spring.sleuth.integration.enabled=false来禁用这个机制
spring.zipkin.sleuth.stream.integration.enabled: false
#如果不需要跟踪某些@Scheduled,可以在spring.sleuth.scheduled.skipPattern设置一些正则表达式来过滤一些class。
spring.zipkin.sleuth.scheduled.skip-pattern: "^(org.*HystrixStreamTask|com.*ServiceAuthUtil*|com.*DBAuthClientService)$"
#web开启sleuth功能
spring.zipkin.sleuth.web.client.enabled: true
#uri在这个里面配置了,那么就不会上报到zipkin, 每个uri中间 |分割
spring.zipkin.sleuth.web.client.skip-pattern: "/hystrix.stream"
io.zipkin.brave
brave-instrumentation-dubbo-rpc
brave dubbo filter添加到dubbo的filter链中:
dubbo.consumer.filter=tracing
dubbo.provider.filter=tracing
使用到的brave依赖
io.zipkin.brave
brave
io.zipkin.reporter2
zipkin-sender-okhttp3
io.zipkin.reporter2
zipkin-sender-kafka
io.zipkin.brave
brave-context-slf4j
io.zipkin.brave
brave-spring-beans
io.zipkin.brave
brave-instrumentation-spring-webmvc
io.zipkin.brave
brave-instrumentation-httpclient
io.zipkin.brave
brave-instrumentation-dubbo-rpc
io.zipkin.brave
brave-instrumentation-mysql8