- 推荐序一
- 推荐序二
- 推荐序三
- 推荐语
- 前言
- 第1章 基础知识
- 第2章 微服务构建:Spring Boot
- 第3章 服务治理:Spring Cloud Eureka
- 第4章 客户端负载均衡:Spring Cloud Ribbon
- 第5章 服务容错保护:Spring Cloud Hystrix
- 第6章 声明式服务调用:Spring Cloud Feign
- 第7章 API网关服务:Spring Cloud Zuul
- 第8章 分布式配置中心:Spring Cloud Config
- 第9章 消息总线:Spring Cloud Bus
- 第10章 消息驱动的微服务:Spring Cloud Stream
- 附录 A Starter POMs
- 后记
与 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收集
在Spring Cloud Sleuth中对Zipkin的整合进行了自动化配置的封装,所以我们可以很轻松地引入和使用它。下面我们来详细介绍一下Sleuth与Zipkin的基础整合过程,主要分为以下两步。
第一步:搭建Zipkin Server
- 创建一个基础的Spring Boot应用,命名为zipkin-server,并在pom.xml中引入Zipkin Server的相关依赖,具体如下:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.3.7.RELEASE</version>
<relativePath/>
</parent>
<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>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Brixton.SR5</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
- 创建应用主类ZipkinApplication,使用@EnableZipkinServer注解来启动Zipkin Server,具体如下:
@EnableZipkinServer
@SpringBootApplication
public class ZipkinApplication {
public static void main(String[]args){
SpringApplication.run(ZipkinApplication.class,args);
}
}
- 在application.properties中做一些简单配置,比如设置服务端口号为9411(客户端整合时,自动化配置会连接9411端口,所以在服务端设置了端口为9411的话,客户端可以省去这个配置)。
spring.application.name=zipkin-server
server.port=9411
创建完上述工程之后,我们将其启动起来,并访问http://localhost:9411/,可以看到如下图所示的Zipkin管理页面:
第二步:为应用引入和配置Zipkin服务
在完成了Zipkin Server的搭建之后,我们还需要对应用做一些配置,以实现将跟踪信息输出到Zipkin Server。我们以之前实现的trace-1和trace-2为例,对它们做以下改造。
- 在trace-1和trace-2的pom.xml中引入spring-cloud-sleuth-zipkin依赖,具体如下所示。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-zipkin</artifactId>
</dependency>
- 在trace-1和trace-2的application.properties中增加Zipkin Server的配置信息,具体如下所示(如果在zip-server应用中,我们将其端口设置为9411,并且均在本地调试的话,也可以不配置该参数,因为默认值就是http://localhost:9411)。
spring.zipkin.base-url=http://localhost:9411
测试与分析
到这里我们已经完成了接入 Zipkin Server 的所有基本工作,可以继续将eureka-server、trace-1和trace-2启动起来,然后做一些测试,以对它的运行机制有一些初步的理解。
我们先来向trace-1的接口发送几个请求http://localhost:9101/trace-1。当在日志中出现跟踪信息的最后一个值为true 的时候,说明该跟踪信息会输出给 Zipkin Server,所以此时可以在 Zipkin Server 的管理页面中选择合适的查询条件,单击 Find Traces按钮,就可以查询出刚才在日志中出现的跟踪信息了(也可以根据日志中的Trace ID,在页面右上角的输入框中来搜索),页面如下所示。
单击下方trace-1端点的跟踪信息,还可以得到Sleuth 跟踪到的详细信息,其中包括我们关注的请求时间消耗等。
单击导航栏中的Dependencies菜单,还可以查看Zipkin Server根据跟踪信息分析生成的系统请求链路依赖关系图,如下图所示。
消息中间件收集
Spring Cloud Sleuth在整合Zipkin时,不仅实现了以HTTP的方式收集跟踪信息,还实现了通过消息中间件来对跟踪信息进行异步收集的封装。通过结合Spring Cloud Stream,我们可以非常轻松地让应用客户端将跟踪信息输出到消息中间件上,同时Zipkin服务端从消息中间件上异步地消费这些跟踪信息。
接下来,我们基于之前实现的trace-1和trace-2应用以及zipkin-server服务端做一些改造,以实现通过消息中间件来收集跟踪信息。改造的内容非常简单,只需要对项目依赖和配置文件做一些调整马上就能实现。下面我们分别对客户端和服务端的改造内容做详细说明。
第一步:修改客户端trace-1和trace-2
- 为了让trace-1和trace-2在产生跟踪信息之后,能够将抽样记录输出到消息中间件,除了需要之前引入的 spring-cloud-starter-sleuth 依赖之外,还需要引入 Zipkin 对 Spring Cloud Stream 的扩展依赖 spring-cloud-sleuthstream 以及基于 Spring Cloud Stream 实现的消息中间件绑定器依赖。以使用RabbitMQ为例,我们可以加入如下依赖:
<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>
- 在 application.properties 配置中去掉http 方式实现时使用的 spring.zipkin.base-url参数,并根据实际部署情况,增加消息中间件的相关配置,比如下面这些关于RabbitMQ的配置信息:
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=springcloud
spring.rabbitmq.password=123456
第二步:修改zipkin-server服务端
为了让 zipkin-server 服务端能够从消息中间件中获取跟踪信息,我们只需要在pom.xml 中引入针对消息中间件收集封装的服务端依赖 spring-cloud-sleuth-zipkin stream,同时为了支持具体使用的消息中间件,我们还需要引入针对消息中间件的绑定器实现。比如以使用RabbitMQ为例,我们可以在依赖中增加如下内容:
<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 的前端依赖 zipkinautoconfigure-ui,为了方便使用,我们在这里也引用了它。
测试与分析
在完成了上述改造内容之后,我们继续将eureka-server、trace-1和trace-2、zipkin-server 都启动起来,同时确保 RabbitMQ 也处于运行状态。此时,我们可以在RabbitMQ的控制页面中看到一个名为sleuth的交换器,它就是Zipkin的消息中间件收集器实现使用的默认主题。
最后,我们使用之前的验证方法,通过向 trace-1的接口发送几个请求http://localhost:9101/trace-1。当有被抽样收集的跟踪信息时(调试时我们可以设置AlwaysSampler抽样机制来让每个跟踪信息都被收集),我们可以在RabbitMQ的控制页面中发现有消息被发送到了sleuth交换器中。同时再到Zipkin服务端的Web页面中也能够搜索到相应的跟踪信息,那么我们使用消息中间件来收集跟踪信息的任务到这里就完成了。
收集原理
在本节内容之前,我们已经对如何引入Sleuth跟踪信息和搭建Zipkin服务端分析跟踪延迟的过程做了详细的介绍,相信大家对于Sleuth和Zipkin已经有了一定的感性认识。接下来,我们介绍一下关于Zipkin收集跟踪信息的过程细节,以帮助我们更好地理解Sleuth生产跟踪信息以及输出跟踪信息的整体过程和工作原理。
数据模型
我们先来看看 Zipkin 中关于跟踪信息的一些基础概念。由于 Zipkin 的实现借鉴了Google的Dapper,所以它们有着类似的核心术语,主要有下面几项内容。
- Span:它代表了一个基础的工作单元。我们以HTTP请求为例,一次完整的请求过程在客户端和服务端都会产生多个不同的事件状态(比如下面所说的4个核心Annotation所标识的不同阶段)。对于同一个请求来说,它们属于一个工作单元,所以同一HTTP请求过程中的4个Annotation同属于一个Span。每一个不同的工作单元都通过一个64位的ID来唯一标识,称为Span ID。另外,在工作单元中还存储了一个用来串联其他工作单元的 ID,它也通过一个64位的 ID 来唯一标识,称为Trace ID。在同一条请求链路中的不同工作单元都会有不同的Span ID,但是它们的Trace ID是相同的,所以通过Trace ID可以将一次请求中依赖的所有依赖请求串联起来形成请求链路。除了这两个核心的ID之外,Span中还存储了一些其他信息,比如,描述信息、事件时间戳、Annotation的键值对属性、上一级工作单元的Span ID等。
- Trace:它是由一系列具有相同Trace ID的Span串联形成的一个树状结构。在复杂的分布式系统中,每一个外部请求通常都会产生一个复杂的树状结构的Trace。
- Annotation:它用来及时地记录一个事件的存在。我们可以把Annotation理解为一个包含有时间戳的事件标签,对于一个HTTP请求来说,在Sleuth中定义了下面4个核心Annotation来标识一个请求的开始和结束。
- cs(Client Send):该Annotation用来记录客户端发起了一个请求,同时它也标识了这个HTTP请求的开始。
- sr(Server Received):该Annotation用来记录服务端接收到了请求,并准备开始处理它。通过计算sr与cs两个Annotation的时间戳之差,我们可以得到当前HTTP请求的网络延迟。
- ss(Server Send):该Annotation用来记录服务端处理完请求后准备发送请求响应信息。通过计算ss与sr两个Annotation的时间戳之差,我们可以得到当前服务端处理请求的时间消耗。
- cr(Client Received):该Annotation用来记录客户端接收到服务端的回复,同时它也标识了这个HTTP请求的结束。通过计算cr与cs两个Annotation的时间戳之差,我们可以得到该http 请求从客户端发起到接收服务端响应的总时间消耗。
- BinaryAnnotation:它用来对跟踪信息添加一些额外的补充说明,一般以键值对的方式出现。比如,在记录HTTP请求接收后执行具体业务逻辑时,此时并没有默认的Annotation来标识该事件状态,但是有BinaryAnnotation信息对其进行补充。
收集机制
在理解了Zipkin的各个基本概念之后,下面我们结合前面章节中实现的例子来详细介绍和理解Spring Cloud Sleuth是如何对请求调用链路完成跟踪信息的生产、输出和后续处理的。
首先,我们来看看Sleuth在请求调用时是怎样来记录和生成跟踪信息的。下图展示了本节中实现示例的运行全过程:客户端发送了一个HTTP请求到trace-1,trace-1依赖于trace-2的服务,所以trace-1再发送一个HTTP请求到trace-2,待trace-2返回响应之后,trace-1再组织响应结果返回给客户端。
在上图的请求过程中,我们为整个调用过程标记了10个标签,它们分别代表了该请求链路运行过程中记录的几个重要事件状态。根据事件发生的时间顺序我们为这些标签做了从小到大的编号,1代表请求的开始、10代表请求的结束。每个标签中记录了一些上面提到过的核心元素:Trace ID、Span ID以及Annotation。由于这些标签都源自一个请求,所以它们的Trace ID相同,而标签1和标签10是起始和结束节点,它们的Trace ID与Span ID是相同的。
根据Span ID,我们可以发现在这些标签中一共产生了4个不同ID的Span,这4个Span分别代表了这样4个工作单元。
- Span T:记录了客户端请求到达trace-1和trace-1发送请求响应的两个事件,它可以计算出客户端请求响应过程的总延迟时间。
- Span A:记录了trace-1应用在接收到客户端请求之后调用处理方法的开始和结束两个事件,它可以计算出 trace-1应用用于处理客户端请求时,内部逻辑花费的时间延迟。
- Span B:记录了trace-1应用发送请求给trace-2应用、trace-2应用接收请求,trace-2应用发送响应、trace-1应用接收响应4个事件,它可以计算出trace-1调用 trace-2的总体依赖时间(cr-cs),也可以计算出 trace-1到trace-2的网络延迟(sr-cs),还可以计算出trace-2应用用于处理客户端请求的内部逻辑花费的时间延迟(ss-sr)。
- Span C:记录了trace-2应用在接收到trace-1的请求之后调用处理方法的开始和结束两个事件,它可以计算出trace-2应用处理来自trace-1的请求时,内部逻辑花费的时间延迟。
在图中展现的这个4个Span正好对应了Zipkin查看跟踪详细页面中的显示内容,它们的对应关系如下图所示。
仔细的读者可能还有这样一个疑惑:我们在Zipkin服务端查询跟踪信息时(如下图所示),在查询结果页面中显示的spans是5,而单击进入跟踪明细页面时,显示的Total Spans又是4,为什么会出现span数量不一致的情况呢?
实际上这两边的span数量内容有不同的含义,查询结果页面中的5 spans代表了总共接收的Span数量,而在详细页面中的Total Spans则是对接收Span进行合并后的结果,也就是前文中我们介绍的4个不同ID的Span内容。下面我们来详细研究一下Zipkin服务端收集客户端跟踪信息的过程,看看它到底收到了哪些具体的Span内容,从而来理解Zipkin中收集到的Span总数量。
为了更直观地观察Zipkin服务端的收集过程,我们可以对之前实现的消息中间件方式收集跟踪信息的程序进行调试。通过在Zipkin服务端的消息通道监听程序中增加断点,我们能清楚地知道客户端都发送了什么信息到 Zipkin 的服务端。在 spring-cloudsleuth-zipkin-stream依赖包中的代码并不多,很容易就能找到定义消息通道监听的实现类:org.springframework.cloud.sleuth.zipkin.stream.ZipkinMessageListener。它的具体实现如下所示,其中SleuthSink.INPUT定义了监听的输入通道,默认会使用名为sleuth的主题,我们也可以通过Spring Cloud Stream的配置对其进行修改。
@MessageEndpoint
@Conditional(NotSleuthStreamClient.class)
public class ZipkinMessageListener {
final Collector collector;
@ServiceActivator(inputChannel=SleuthSink.INPUT)
public void sink(Spans input){
List<zipkin.Span> converted=ConvertToZipkinSpanList.convert(input);
this.collector.accept(converted,Callback.NOOP);
}
...
}
从通道监听方法的定义中我们可以看到,Sleuth与Zipkin在整合的时候是由两个不同的Span定义的,一个是消息通道的输入对象org.springframework.cloud.sleuth.stream.Spans,它是Sleuth 中定义的用于消息通道传输的Span 对象。每个消息中包含的Span信息定义在org.springframework.cloud.sleuth.Span对象中,但是真正在Zipkin服务端使用的并非这个Span对象,而是Zipkin自己的zipkin.Span对象。所以,在消息通道监听处理方法中,对Sleuth的Span做了处理,每次接收到Sleuth的Span之后就将其转换成Zipkin的Span。
下面我们尝试在 sink(Spans input)方法实现的第一行代码中加入断点,并向trace-1发送一个请求,触发跟踪信息发送到RabbitMQ上。此时我们通过DEBUG模式可以发现消息通道中都接收到了两次输入,一次来自 trace-1,一次来自 trace-2。下面两张图分别展示了来自trace-1和trace-2输出的跟踪消息,其中trace-1的跟踪消息包含了3条Span信息,trace-2的跟踪消息包含了2条Span信息,所以在这个请求调用链上,一共发送了5个Span信息,也就是我们在Zipkin搜索结果页面中看到的Spans的数量信息。
点开一个具体的Span内容,我们可以看到如下所示的结构,它记录了Sleuth中定义的Span详细信息,包括该Span的开始时间、结束时间、Span的名称、Trace ID、Span ID、Tags(对应Zipkin中的BinaryAnnotation)、Logs(对应Zipkin中的Annotation)等之前提到过的核心跟踪信息。
介绍到这里仔细的读者可能会有一个疑惑,在明细信息中展示的Trace ID和Span ID的值为什么与列表展示的概要信息中的Trace ID和Span ID的值不一样呢?实际上,Trace ID和Span ID都是使用long类型存储的,在DEBUG模式下查看其明细时自然是long类型,也就是它的原始值,但是在查看 Span 对象的时候,我们看到的是通过 toString()函数处理过的值。从Sleuth的Span源码中我们可以看到如下定义,在输出Trace ID和Span ID时都调用了idToHex函数将long类型的值转换成了十六进制的字符串值,所以在DEBUG时我们会看到两个不一样的值。
public String toString(){
return "[Trace: "+idToHex(this.traceId)+",Span: "+idToHex(this.spanId)
+",Parent: "+getParentIdIfPresent()+",exportable:"+this.exportable+"]";
}
public static String idToHex(long id){
return Long.toHexString(id);
}
在接收到Sleuth之后,我们将程序继续执行下去,可以看到经过转换后的Zipkin的Span内容,它们保存在一个名为converted的列表中,具体内容如下所示:
上图展示了转换后的Zipkin Span对象的详细内容,可以看到很多熟悉的名称,也就是之前介绍的关于Zipkin中的各个基本概念。而这些基本概念的值我们也都可以在之前Sleuth的原始Span中找到,其中Annotations和BinaryAnnotations有一些特殊。在Sleuth定义的Span中没有使用相同的名称,而是使用了logs和tags来命名。从这里的详细信息中,我们可以直观地看到Annotations和BinaryAnnotations的作用,其中Annotations中存储了当前Span包含的各种事件状态以及对应事件状态的时间戳,而BinaryAnnotations则存储了对事件的补充信息,比如上图中存储的就是该HTTP请求的细节描述信息,除此之外,它也可以存储对调用函数的详细描述(如下图所示)。
下面我们再来详细看看通过调试消息监听程序接收到的这5个Span内容。首先,可以发现,每个Span中都包含有3个ID信息,其中除了标识Span自身的ID以及用来标识整条链路的traceId之外,还有一个之前没有提过的parentId,它是用来标识各Span父子关系的 ID(它的值来自于上一步执行单元 Span 的 ID)。通过 parentId 的定义我们可以为每个Span 找到它的前置节点,从而定位每个 Span 在请求调用链中的确切位置。在每条调用链路中都有一个特殊的Span,它的parentId为null,这类Span我们称它为Root Span,也就是这条请求调用链的根节点。为了弄清楚这些Span之间的关系,我们可以从Root Span开始来整理出整条链路的Span内容。下表展示了我们从Root Span开始,根据各个Span的父子关系整理出的结果:
上表中的Host代表了该Span是从哪个应用发送过来的;Span ID是当前Span的唯一标识;Parent Span ID代表了上一执行单元的Span ID; Annotation代表了该Span中记录的事件(这里主要用来记录http 请求的4个阶段,表中内容进行了省略处理,只记录了Annotation名称(sr代表服务端接收请求,ss代表服务端发送请求,cs代表客户端发送请求,cr代表客户端接收请求),省略了一些其他细节信息,比如服务名、时间戳、IP地址、端口号等信息); BinaryAnnotation代表了事件的补充信息(Sleuth的原始Span记录更为详细,Zipkin的Span处理后会去掉一些内容,对于有Annotation标识的信息,不再使用Binary Annotation 补充,在上表中我们只记录了服务名、类名、方法名,同样省略了一些其他信息,比如时间戳、IP地址、端口号等信息)。
通过收集到的Zipkin Span详细信息,我们很容易将它们与本节开始时介绍的一次调用链路中的10个标签内容联系起来。
- Span ID=T的标签有2个,分别是序号1和10,它们分别表示这次请求的开始和结束。它们对应了上表中ID为e9a933ec50d180d6的Span,该Span的内容在标签10执行结束后,由trace-1将标签1和10合并成一个Span发送给Zipkin Server。
- Span ID=A的标签有2个,分别是序号2和9,它们分别表示了trace-1请求接收后,具体处理方法调用的开始和结束。该Span的内容在标签9执行结束后,由trace-1将标签2和9合并成一个Span发送给Zipkin Server。
- Span ID=B的标签有4个,分别是序号3、4、7、8,该Span比较特殊,它的产生跨越了两个实例,其中标签3和8是由trace-1生成的,而标签4和7则是由 trace-2生成的,所以该标签会拆分成两个 Span 内容发送给 Zipkin Server。trace-1会在标签8结束的时候将标签3和8合并成一个Span发送给Zipkin Server,而trace-2会在标签7结束的时候将标签4和7合并成一个Span发送给Zipkin Server。
- Span ID=C的标签有2个,分别是序号5和6,它们分别表示了trace-2请求接收后,具体处理方法调用的开始和结束。该Span的内容在标签6执行结束后,由trace-2将标签2和9合并成一个Span发送给Zipkin Server。
所以,根据上面的分析,Zipkin总共会收到5个Span:一个Span T,一个Span A,两个Span B,一个Span C。结合之前请求链路的标签图和这里的Span记录,我们可以总结出如下图所示的Span收集过程,读者可以参照此图来理解Span收集过程的处理逻辑以及各个Span之间的关系。
虽然,Zipkin服务端接收到了5个Span,但就如前文中分析的那样,其中有两个Span ID=B的标签,由于它们来自于同一个HTTP请求(trace-1对trace-2的服务调用),概念上它们属于同一个工作单元,因此 Zipkin 服务端在前端展现分析详情时会将这两个Span合并显示,而合并后的Span数量就是在请求链路详情页面中Total Spans的数量。
下图是本章示例的一个请求链路详情页面,在页面中显示了各个Span的延迟统计,其中第三条Span信息就是trace-1对trace-2的HTTP请求调用,通过单击它可以查看该Span的详细信息。单击后会以模态框的方式弹出Span详细信息(如图下半部分所示),在弹出框中详细展示了Span的Annotation和BinaryAnnotation信息,在Annotation区域我们可以看到它同时包含了trace-1和trace-2发送的Span信息,而在BinaryAnnotation区域则展示了该HTTP请求的详细信息。
数据存储
默认情况下,Zipkin Server会将跟踪信息存储在内存中,每次重启Zipkin Server都会使之前收集的跟踪信息丢失,并且当有大量跟踪信息时我们的内存存储也会成为瓶颈,所以通常情况下我们都需要将跟踪信息对接到外部存储组件中去,比如使用MySQL存储。
Zipkin 的 Storage 组件中默认提供了对 MySQL 的支持,所以我们可以很轻松地为zipkin-server 增加 MySQL 存储功能。下面我们详细介绍基于消息中间件实现的zipkin-server应用,对其进行MySQL存储扩展的详细过程。
第一步:为zipkin-server添加依赖
为了让zipkin-server能够访问MySQL数据库,我们需要在它的pom.xml文件中增加如下依赖,以支持对MySQL的访问:
<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>org.jooq</groupId>
<artifactId>jooq</artifactId>
<version>3.8.0</version>
</dependency>
这里需要注意加入org.jooq的依赖,并不是因为缺少这个依赖而引入它,而是为了解决该版本存在的一个 Bug。当不引入该依赖的时候,会使用默认的依赖,但是默认情况下引入的依赖并非3.8.0,当有跟踪信息被zipkin-server收集并入库时会报如下错误:
o.s.c.s.z.stream.ZipkinMessageListener : Cannot store spans
[e7aca79a855ff80a.30671a9177f24801<:114ffd85950b117b,
e7aca79a855ff80a.114ffd85950b117b<:d8ac47672741466c]due to VerifyError(class
zipkin.storage.mysql.internal.generated.tables.ZipkinSpans overrides final method
getSchema.()Lorg/jooq/Schema;)
java.lang.VerifyError: class
zipkin.storage.mysql.internal.generated.tables.ZipkinSpans overrides final method
getSchema.()Lorg/jooq/Schema;
...
o.s.c.s.i.m.MessagingSpanExtractor : Deprecated trace headers detected.
Please upgrade Sleuth to 1.1 or start sending headers present in the TraceMessageHeaders
class
o.s.c.s.z.stream.ZipkinMessageListener : Cannot store spans
[8ce053ed8c2410a0.d6b63783a69485d8<:9ebc8131ebdb07fc,
8ce053ed8c2410a0.9ebc8131ebdb07fc<:8ce053ed8c2410a0,
8ce053ed8c2410a0.8ce053ed8c2410a0<:8ce053ed8c2410a0]due to
VerifyError(zipkin/storage/mysql/internal/generated/tables/ZipkinSpans)
从报错信息中我们可以看到 VerifyError(zipkin/storage/mysql/internal/generated/tables/ZipkinSpans)这一句,从ZipkinSpans的源码中我们可以看到如下内容:
@Generated(
value={
"http://www.jooq.org",
"jOOQ version:3.8.0"
},
comments="This class is generated by jOOQ"
)
从注解中可以看到,ZipkinSpans对象是通过jOOQ 3.8.0生成出来的,但是当工程不加入上面的org.jooq依赖时,jOOQ的版本是3.7.4,正是因为这里的版本不一致导致了上面错误的出现。所以,如果读者在使用其他版本的Spring Cloud Sleuth出现类似错误时,可以看看是否是由这个原因引起的。
第二步:在MySQL中创建用于Zipkin存储的Schema
这里需要注意一点,Zipkin Server 实现关系型数据库存储时,不同版本对数据库表结构都有一些变化。当跨版本使用时很容易出现各种整合问题,所以尽量使用对应版本的脚本来创建Schema。同时,Zipkin实现的MySQL存储仅在MySQL 5.6-5.7版本中测试过,所以尽量使用对应版本的MySQL,以避免产生不可预知的问题。
本书中我们使用了Spring Cloud的Brixton.SR5版本,通过查看依赖关系可以知道它使用的Zipkin版本为1.1.5,所以我们可以从GitHub上找到该版本并下载创建表结构的脚本。另外,也可以在本地依赖中找到该脚本,下图展示了 mysql.sql 脚本在本地依赖中的具体位置:
从上图中我们还可以知道,MySQL 的存储支持是通过 zipkin-autoconfigurestorage-mysql依赖实现的,但是我们之前为什么没有引入该依赖呢?这是由于我们改造的工程基础是消息中间件实现的示例,之前提到过,在该示例中引入的spring-cloud-sleuth-zipkin-stream依赖包含了各个常用的依赖组件,其中就包括了对 MySQL 的支持依赖,所以我们这里并不需要再手工添加它。当然,如果不是基于该示例,而是通过http 实现的收集示例来改造时,就需要自己引入对zipkin-autoconfigure-storage-mysql的依赖了。
在获取创建表结构的SQL文件之后,我们可以在MySQL中手工创建名为zipkin的 Schema,并运行 mysql.sql 脚本来创建表结构。除此之外,也可以通过在程序中进行配置的方式让其自动初始化,只需要在application.properties中增加如下配置:
spring.datasource.schema=classpath:/mysql.sql
spring.datasource.url=jdbc:mysql://localhost:3306/zipkin
spring.datasource.username=root
spring.datasource.password=123456
spring.datasrouce.continueOnError=true
spring.datasrouce.initialize=true
通过启动程序,Spring Boot的JDBC模块会自动地为我们根据指定的SQL文件来创建表结构。我们可以得到如下两张表。
- zipkin_spans:存储Span信息的表。
- zipkin_annotations:存储Annotation信息的表。
第三步:切换存储类型
通过第二步,我们已经让 zipkin-server 连接到 MySQL,并且创建了用于存储跟踪信息的 Schema。下面我们只需要再做一个简单配置,让 zipkin-server 的存储切换到MySQL即可,具体配置如下:
zipkin.storage.type=mysql
测试与验证
到这里,我们就已经完成了将 zipkin-server 从内存存储跟踪信息切换为MySQL存储跟踪信息的改造。最后,我们继续使用之前的验证方法,通过向 trace-1的接口发送几个请求http://localhost:9101/trace-1,当有被抽样收集的跟踪信息时(调试时可以设置AlwaysSampler抽样机制来让每个跟踪信息都被收集),查看MySQL中的两张表,可以得到类似下面的数据信息。
- zipkin_spans表
- zipkin_annotations表
表中所存储的信息我们已经非常熟悉,之前分析的内容都可以在这两张表中体现出来。比如Span的数量问题,从zipkin_spans表中,我们可以看到一次请求调用链路的跟踪信息产生了4条span数据,也就是说,在入zipkin_spans表的时候,已经对收集的span信息进行了合并,所以在查询详细信息时,不需要每次都来计算合并 span。而在zipkin_annotations 表中,通过 span_id 字段可以关联到每个具体工作单元的详细信息,同时根据endpoint_service_name和span_id字段还可以计算出一次请求调用链路中总共接收到的span数量。
Zipkin 在存储方面除了对 MySQL 有扩展组件之外,还实现了对 Cassandra 和ElasticSearch的支持扩展。具体的整合方式与MySQL的整合类似,读者可自行查阅Zipkin的官方文档做进一步的了解。
API接口
Zipkin不仅提供了UI模块让用户可以使用Web页面来方便地查看跟踪信息,它还提供了丰富的RESTful API接口供用户在第三方系统中调用来定制自己的跟踪信息展示或监控。我们可以在Zipkin Server启动时的控制台或日志中找到Zipkin服务端提供的RESTful API定义,比如下面的日志片段:
s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped
"{[/api/v1/dependencies],methods=[GET],produces=[application/json]}"...
s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped
"{[/api/v1/trace/{traceId}],methods=[GET],produces=[application/json]}"...
s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped
"{[/api/v1/traces],methods=[GET],produces=[application/json]}"...
s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped
"{[/api/v1/services],methods=[GET]}"...
s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped
"{[/api/v1/spans],methods=[GET]}"...
s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped
"{[/api/v1/spans],methods=[POST]}"...
s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped
"{[/api/v1/spans],methods=[POST],consumes=[application/x-thrift]}"...
可以看到Zipkin Server提供的API接口都以/api/v1路径作为前缀,它们的具体功能整理如下:
更多关于接口的请求参数和请求返回格式等细节说明,可以通过访问 Zipkin 官方的API页面http://zipkin.io/zipkin-api/来查看,帮助我们根据自身系统架构来访问Zipkin Server以定制自己的Dashboard或监控系统。实际上,Zipkin的UI模块也是基于RESTful API接口来实现的,有兴趣的读者可以通过浏览器的开发者模式来查看每个页面发起的请求,以此作为调用样例来参考。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论