分布式服务跟踪:Spring Cloud Sleuth
通过之前各章节介绍的 Spring Cloud 组件, 实际上我们已经能够通过使用它们搭建起 一个基础的微服务架构系统来实现业务需求了。 但是, 随着业务的发展, 系统规模也会变得越来越大, 各微服务间的调用关系也变得越来越错综复杂。 通常一个由客户端发起的请求在后端系统中会经过多个不同的微服务调用来协同产生最后的请求结果, 在复杂的微服务架构系统中, 几乎每一个前端请求都会形成一条复杂的分布式服务调用链路, 在每条链路中任何一个依赖服务出现延迟过高或错误的时候都有可能引起请求最后的失败。这时候, 对于每个请求, 全链路调用的跟踪就变得越来越重要, 通过实现对请求调用的跟踪可以帮助我们快速发现错误根源以及监控分析每条请求链路上的性能瓶颈等。
针对上面所述的分布式服务跟踪问题, Spring Cloud Sleuth 提供了 一套完整的解决方 案。 在本章中, 我们将详细介绍如何使用 Spring Cloud Sleuth 来为微服务架构增加分布式服务跟踪的能力。
快速入门
在介绍各种概念与原理之前, 我们先通过实现一个简单的示例, 为存在服务调用的应用增加一些 Sleuth 的配置以实现基本的服务跟踪功能, 以此来对 Spring Cloud Sleuth 有一 个初步的了解, 随后再逐步展开, 介绍实现过程中的各个细节。
准备工作
在引入 Sleuth 之前, 我们先按照之前章节学习的内容来做一些准备工作, 构建一些基础的设施和应用。
• 服务注册中心: eureka-server, 这里不做赘述, 直接使用之前构建的工程即可。
• 微服务应用: trace-1, 实现一个 REST 接口 /trace-1, 调用该接口后将触发对 trace-2 应用的调用。 具体实现如下所述。
<parent> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-parent</artifactid> <version>l.3.7.RELEASE</version> <relativePath/> </parent> <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-web</artifactid> </dependency> <dependency> <groupid>org.springframework.cloud</groupid> <artifactid>spring-cloud-starter-eureka</artifactid> </dependency> <dependency> <groupid>org.springframework.cloud</groupid> <artifactid>spring-cloud-starter-ribbon</artifactid> </dependency> <dependencyManagement> <dependencies> <dependency> <groupid>org.springframework.cloud</groupid> <artifactid>spring-cloud-dependencies</artifactid> <version>Brixton.SRS</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
• 创建应用主类, 实现/trace-1 接口, 并使用 RestTemplate 调用 trace-2 应用的接口。 具体如下:
@RestController @EnableDiscoveryCilent @SpringBootAppliation public class TraceApplication{ private final Logger logger= Logger.getLogger.getLogger(getClass());
@Bean @LoadBalanced RestTemplate restTemplate(){ return new RestTemplate(); }
@RequestMapping(value="/trace-1",method=RequestMethod.GET) public String trace(){ logger.info("==========call trace-1========="); return restTemplate().getForEntity("http://tace-2",String.class).getBody(); }
public static void main(String[] args){ SpringApplication.run(TraceApplication.class,args); } }
• 在application.properties 中,将 eureka.client.serviceUrl.defaultZone 参数指向 eureka-server 的地址, 具体如下:
spring.application.name=trace-1 server.port=9101 eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka
• 微服务应用: trace-2, 实现一个 REST 接口/trace-2, 供 trace-1 调用。 具 体实现如下所示。
. 创建一个基础的 Spring Boot 应用, pom.xml 中的依赖与 trace-1 相同。
. 创建应用主类, 并实现/trace-2 接口, 具体实现如下:
@RestController @EnableDiscoveryClient @SpringBootApplication public class TraceApplication { private final static Logger logger= Logger.getLogger(getClass()); @RequestMapping(value="/trace-2",mehtod=RequestMethod.GET) public String trace(){ logger.info("===<call trace-2>==="); return "Trace"; } pulic static void main(String[] args){ SpringApplication.run(TraceApplicaion.class,args) } }
. 在application.properties 中,将eureka.client.serviceUrl .defaultZone 参数指向 eureka-server 的地址,另外还需要设置不同的应用名和端口号,具 体如下:
spring.application.name=trace-2 server.port=9102 eureka.client.serviceOrl.defaultZone=http://localhost:llll/eureka/
在实现了上面的内容之后, 我们可以将 eureka-server、 trace-1、 trace-2三 个应用都启动起来, 并通过 postman 或 curl 等工具来对 trace-1 的接口发送请求 http://localhost:9101/trace-1。 可以得到返回值 Trace, 同时还能在它们的控 制台中分别获得下面的输出:
-- trace-1 INFO 25272 --- [nio-9101-exec-2] ication$$EnhancerBySpringCGLIB$$36el2c68: ===<call trace-1>===
-- trace-2 INFO 7136 --- [nio-9102-exec-1] ication$$EnhancerBySpringCGLIB$$52a02f0b: ===<call trace-2>===
实现跟踪
在完成了准备工作之后, 接下来我们开始进行本章的主题内容, 为上面的trace-1 和 trace-2 添加服务跟踪功能。 通过 Spring Cloud Sleuth 的封装, 我们为应用增加服务跟踪能力的操作非常简单, 只需在 trace-1 和 trace-2 的 porn.xrnl 依赖管理中增加 spring-cloud-starter-sleuth 依赖即可,具体如下:
<dependency> <groupid>org.springframework.cloud</groupid> <artifactid>spring-cloud-starter-sleuth</artifactid> </dependency>
到这里, 实际上我们已经为trace-1 和 trace-2 实现服务跟踪做好了基础的准备, 重启trace-1 和 trace-2, 再对 trace-1 的接口发送请求 http://localhost: 9101/trace-1。 此时, 我们可以从它们的控制台输出中窥探到 Sleuth 的一些端倪。
-- trace-1 INFO [trace-1, f410ab57afd5cl45, a9f2118fa2019684, false] 25028 --- [nio-9101-exec-l] ication$$EnhancerBySpringCGLIB$$d8228493 : ===<call trace-1>===
-- trace-2 INFO [trace-2, f410ab57afd5cl45, e9a377dc2268bc29, false] 23112 --- [nio-9102-exec-l] ication$$EnhancerBySpringCGLIB$$e6cb4078 : ===<call trace-2>===
从上面的控制台输出内容 中 , 我们可以看到多了 一 些形如[trace-1, f410ab57afd5c145, a9f2118fa2019684, false]的日志信息, 而这些元素正是实现分布式服务跟踪的重要组成部分, 每个值的含义如下所述。
• 第一个值: trace-1, 它记录了应用的名称,也就是 application.properties 中 spring.application.name参数配置的属性。
• 第二个值: f410ab57afd5c145, Spring Cloud Sleuth生成的一个ID,称为Trace ID, 它用来标识一条请求链路。 一条请求链路中包含一个TraceID, 多个SpanID。
• 第三个值: a9f2118fa2019684, Spring Cloud Sleuth生成的另外一个 ID, 称为 Span ID, 它表示一个基本的工作单元, 比如发送一个HTTP请求。
• 第四个值: false, 表示是否要将该信息输出到Zipkin等服务中来收集和展示 。
跟踪原理
分布式系统中的服务跟踪在理论上并不复杂, 它主要包括下面两个关键点。
• 为了实现请求跟踪, 当请求发送到分布式系统的入口端点时, 只需要服务跟踪框架为该请求创建一个唯一的跟踪标识, 同时在分布式系统内部流转的时候,框架始终保待传递该唯一标识, 直到返回给请求方为止, 这个唯一标识就是前文中提到的 Trace ID。 通过TraceID的记录, 我们就能将所有请求过程的日志关联起来。
• 为了统计各处理单元的时间延迟, 当请求到达各个服务组件时, 或是处理逻辑到达某个状态时, 也通过一个唯一标识来标记它的开始、 具体过程以及结束, 该标识就是前文中提到的SpanID。 对于每个Span来说, 它必须有开始和结束 两个节点, 通 过记录开始 Span和结束Span的时间戳,就能统计出该Span的时间延迟,除了时间戳记录之外, 它还可以包含一些其他元数据, 比如事件名称、 请求信息等。
在快速入门示 例中, 我们轻松实现了日志级别的跟踪信息接入, 这完全归功于spring-cloud-starter-sleuth 组件的实现 。 在 Spring Boot 应用中, 通过在工程中引入 spring-cloud-starter-sleuth 依赖之后, 它会自动为当前应用构建起各通信通道的跟踪机制, 比如:
• 通过诸如 RabbitMQ、 Kafka(或者其他任何 Spring Cloud Stream 绑定器实现的消息中间件) 传递的请求。
• 通过 Zuul 代理传递的请求。
• 通过 RestTemplate 发起的请求。
在快速入门示例中, 由于 trace-1对trace-2 发起的请求是通过 RestTemplate 实现的, 所以 spring-cloud-starter-sleuth 组件会对该请求进行处理 。 在发送到 trace-2 之前, Sleuth会在该请求的Header中增加实现跟踪需要的重要信息,主要有下面 这几个(更多关于头信息的定义可以通过查看 org.springframework.cloud. sleuth.Span的源码获取)。
• X-B3-Traceld:一条请求链路 (Trace) 的唯一标识, 必需的值。
• X-B3-Spanld:一个工作单元 (Span) 的唯一标识, 必需的值。
• X-B3-ParentSpanld:标识当前工作单元所属的上一个工作单元 , Root Span (请求链路的第一个工作单元) 的该值为空。
• X-Span-Name:工作单元的名称。
可以通过对trace-2 的实现做一些修改来输出这些头部信息, 具体如下:
@RequestMapping(value="/trace-2",method=RequestMethod.GET) public String trace (HttpServletRequest request){
logger.info("===<call trace-2,Trace={},SpanId{}===>",request.getHeader("X-B3-TraceId"),request.getHeader("X-B3-SpanId")); return "Trace"; }
通过上面的改造, 我们再运行快速入门的示例内容, 并发起对trace-1 的接口访问, 可以得到如下输出内容。 其中在trace-2 的控制台中, 输出了当前正在处理的TraceID 和 SpanId 信息。
-- trace-1 INFO [trace-1, a6e9175ffd5d2c88, 8524f519b8a9e399, true] 10532 --- [nio-9101-exec-2] icationEnhancerBySpringCGLIB27aa9624 : ===<call trace-1>=== -- trace-2 INFO [trace-2,a6e9175ffd5d2c88,ce60dcfle2ed918f,true] 1208 --- [nio-9102-exec-3] icationEnhancerBySpringCG1IBa7d84797 : ===<call trace-2, Traceid=a6e9175ffd5d2c88, Spanid=be4949ecll5e554e>===
为了更直观地观察跟踪信息, 我们还可以在 application.properties 中增加下面的配置:
logging.level.org.springframework.web.servlet.DispatcherServlet=DEBUG
通过将SpringMVC的请求分发日志级别调整为DEBUG级别, 我们可以看到更多跟踪 信息:
-- trace-1 2016-11-27 09:26:52.663 DEBUG [trace-l,a6e9175ffd5d2c88,a6e9175ffd5d2c88,true] 10532 --- [nio-9101-exec-2) o.s.web.servletDispatcherServlet DispatcherServlet with name 'dispatcherServlet' processing GET request for [/trace-1] 2016-11-27 09:26:52.666 DEBUG [trace-l,a6e9175ffd5d2c88,a6e9175ffd5d2c88,true] 10532 --- [nio-9101-exec-2) o.s.web.servlet.DispatcherServlet : Last-Modified value for [/trace-1) is: -1 2016-11-27 09:26:52.685 DEBUG [trace-l,a6e9175ffd5d2c88,8524f519b8a9e399,true] 10532 --- [nio-9101-exec-2) o. s. web. servlet. DispatcherServlet : Null ModelAndView returned to DispatcherServlet with name 'dispatcherServlet': assuming HandlerAdapter completed request handling 2016-11-27 09:26:52.685 DEBUG [trace-l,a6e9175ffd5d2c88,a6e9175ffd5d2c88,true] 10532 --- [nio-9101-exec-2] o.s.web.servlet.DispatcherServlet : Successfully completed request -- trace-2 2016-11-27 09:26:52.673 DEBUG [trace-2,a6e9175ffd5d2c88,be4949ec115e554e,true] 1208 --- [nio-9102-exec-3) o. s. web. servlet. DispatcherServlet : DispatcherServlet with name 'dispatcherServlet' processing GET request for [/trace-2] 2016-11-27 09:26:52.679 DEBUG [trace-2,a6e9175ffd5d2c88,be4949ec115e554e,true] 1208 --- [nio-9102-exec-3] o.s.web.servlet.DispatcherServlet : Last-Modified value for [/trace-2) is: -1 2016-11-27 09:26:52.682 DEBUG [trace-2,a6e9175ffd5d2c88,ce60dcfle2ed918f,true] 1208 --- [nio-9102-exec-3) o.s.web.servlet.DispatcherServlet : Null ModelAndView returned to DispatcherServlet with name 'dispatcherServlet': assuming HandlerAdapter completed request handling 2016-11-27 09:26:52.683 DEBUG [trace-2,a6e9175ffd5d2c88,be4949ec115e554e,true] 1208 --- [nio-9102-exec-3] o.s.web.servlet.DispatcherServlet : Successfully completed request
抽样收集
通过Trace ID和Span ID已经实现了对分布式系统中的请求跟踪, 而记录的跟踪信息最终会被分析系统收集起来, 并用来实现对分布式系统的监控和分析功能, 比如, 预警延迟过长的请求链路、 查询请求链路的调用明细等。 此时, 我们在对接分析系统时就会碰到 一个问题: 分析系统在收集跟踪信息的时候, 需要收集多少跟踪信息才合适呢?
理论上来说, 我们收集的跟踪信息越多就可以越好地反映出系统的实际运行情况, 并给出更精准的预警和分析。 但是在高并发的分布式系统运行时, 大量的请求调用会产生海量的跟踪日志信息, 如果收集过多的跟踪信息将会对整个分布式系统的性能造成一定的影 响, 同时保存大量的日志信息也需要不少的存储开销。 所以, 在 Sleuth中采用了抽象收集的方式来为跟踪信息打上收集标记, 也就是我们之前在日志信息中看到的第4个布尔类型 的值, 它代表了该信息是否要被后续的跟踪信息收集器获取和存储。
Sleuth 中的抽样收集策略是通过 Sampler 接口实现的, 它的定义如下:
public interface Sampler { /** * @return true if the span is not null and should be exported to the 七racing system */ boolean isSampled(Span span); }
通过实现 isSampled 方法,Spring Cloud Sleuth 会在产生跟踪信息的时候调用它来为跟踪信息生成是否要被收集的标志。 需要注意的是, 即使 isSampled 返回了 false, 它 仅代表该跟踪信息不被输出到后续对接的远程分析系统(比如 Zipkin), 对于请求的跟踪活动依然会进行, 所以我们在日志中还是能看到收集标识为 false 的记录。
默认情况下,Sleuth 会使用 PercentageBasedSarnpler 实现的抽样策略,以请求百分比的方式配置和收集跟踪信息。 我们可以通过在 application.properties 中配置 下面的参数对其百分比值进行设置, 它的默认值为0 .1, 代表收集10%的请求跟踪信息。
spring.sleuth.sampler.percentage=0.1
在开发调试期间, 通常会收集全部跟踪信息并输出到远程仓库, 我们可以将其值设置 为1, 或者也可以通过创建 AlwaysSampler 的 Bean (它实现的 isSampled 方法始终返 回 true) 来覆盖默认的 PercentageBasedSampler 策略, 比如:
@Bean public AlwaySampler defaultSampler(){ return new AlwaysSampler(); }
在实际使用时,通过与 Span 对象中存储信息的配合,我们可以根据实际情况做出更贴近需求的抽样策略, 比如实现一个仅包含指定Tag的抽样策略:
public class TagSampler implements Sampler{
private String tag;
public TagSampler(String tag){ this.tag=tag; }
@Override public boolean isSampled(Span span){ return span.tags().get(tag)!=null; } }
由于跟踪日志信息数据的价值往往仅在最近的一段时间内非常有用, 比如 一周。 那么 我们在设计抽样策略时, 主要考虑在不对系统造成明显性能影响的情况下, 以在日志保留时间窗内充分利用存储空间的原则来实现抽样策略。
与Logstash整合
通过之前的准备与整合,我们已经为trace-1和trace-2引入了Spring Cloud Sleuth 的基础模块spring-cloud-starter-sleuth, 实现了在各个微服务的日志信息中添加跟踪信息的功能。 但是, 由于日志文件都离散地存储在各个服务实例的文件系统之上, 仅仅通过查看日志文件来分析我们的请求链路依然是一件相当麻烦的事, 所以我们还需要一 些工具来帮助集中收集、 存储和搜索这些跟踪信息。 引入基于日志的分析系统是一个不错的选择, 比如ELK平台, 它可以轻松地帮助我们收集和存储这些跟踪日志, 同时在需要的时候我们也 可以根据Trace ID来轻松地搜索出对应请求链路相关的明细日志。
ELK平台主要由ElasticSearch、 Logstash和Kibana三个开源工具组成。
• ElasticSearch是一个开源分布式搜索引擎, 它的特点有: 分布式, 零配置, 自动发现, 索引自动分片, 索引副本机制,RESTful风格接口, 多数据源, 自动搜索负载 等。
• Logstash是一个完全开源的工具, 它可以对日志进行收集、 过滤, 并将其存储供以后使用。
• Kibana 也是一个开源和免费的工具, 它可以为Logstash 和ElasticSearch提供日志分析友好的Web界面, 可以帮助汇总、 分析和搜索重要数据日志。
Spring Cloud Sleuth 在与ELK平台整合使用时, 实际上只要实现与负责日志收集的 Logstash完成数据对接即可, 所以我们需要为Logstash准备JSON格式的日志输出。 由于 Spring Boot 应用默认使用logback来记录日志,而Logstash自身也有对logback日志工具的支持工具, 所以我们可以直接通过在logback的配置中增加对Logstash的Appender, 就能 非常方便地将日志转换成以JSON的格式存储和输出了。
下面我们来详细介绍一下在快速入门示例的基础上, 如何实现面向Logstash的日志输出配置。
• 在pom.xml依赖中引入logstash-logback-encoder依赖, 具体如下:
<dependency> <groupid>net.logstash.logback</groupid> <artifactid>logstash-logback-encoder</artifactid> <version>4.6</version> </dependency>
• 在工程/resource 目录下创建 bootstrap.properties 配置文件,将 spring. application.name= trace-1 配置移动到该文件中去。由于 logback-spring. xml 的加载在 application.properties 之前, 所以之前的配置 logbackspring.xml 无法获取 spring.application.name 属性, 因此这里将该属性移动到最先加载的 bootstrap.properties 配置文件中。
• 在工程/resource 目录下创建 logback 配置文件 logback-spring.xml, 具体内容如下:
<?xml version="l. 0" encoding="UTF - 8"?> <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(yyyy-MM-dd HH:mm:ss.SSS}) (faint}%clr(${LOG_LEVEL_PATTERN: -%5p}) %clr([${springAppName:-},%X{X-B3-TraceId:-},%X(X-B3-SpanId:-},%X{X-Span-Export:-}]) {yellow} %clr(${PID:- }) {magenta} %clr(---) {faint}%clr([%15.15t)) { faint) %c lr(%-40.40logger{39}) {cyan) %clr(:) (faint}%m%n${LOG_EXCEPTI0N_CONVERSION一_WORD:-%wEx} "/> <!-- 控制台Appender --> <appender name="console" class="ch.qos.logback.core.ConsoleAppender"> <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> <level>INFO</level> </filter> <encoder> <pattern>${CONS0LE_LOG—PATTERN}</pattern> <charset>utf8</charset> </encoder> </appender> <!-- 为logstash输出的JSON格式的Appender --> <appender name=" logstash" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${LOG_ FILE}. json</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>${LOG—FILE}. json. %d{yyyy-MM-dd}. gz</fileNamePattern> <maxHistory>7</maxHistory> </rollingPolicy> <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:-} ", "exportable": "%X{X-Span-Export: 一}", "pid": "${PIO:-}", "thread": "%thread", "class": "%logger{40}", "rest": "%message" } </pattern> </pattern> </providers> </encoder> </appender> <root level="INFO"> <appender-ref ref="console"/> <appender-ref ref="logstash"/> </root>
对 Logstash 的支持主要通过名为 logstash 的 Appender 实现, 内容并不复杂, 主要是对日志信息的格式化处理, 上面为了方便调试和查看, 我们先将 JSON 格式的日志输出到文件中。
完成上面的改造之后, 我们再将快速入门的示例运行起来, 并发起对 trace-1 的接 口访间。 此时可以在 trace-1 和 trace-2 的工程目录下发现有一个 build 目录, 下面分别创建了以各自应用名称命名的 JSON 文件, 该文件就是在 logback-spring.xml 中配置的名为 logstash 的 Appender输出的日志文件,其中记录了类似下面格式的 JSON日志:
{ “ @timestamp":"2016-12-04T06:57:58.970+00:00","severity":"INFO","service":"trac e-l","trace":"589ee5f7b860132f","span":"a9e891273affb7fc","exportable":"false","pid ":"19756","thread":"http-nio-9101-exec-1","class":"c.d.TraceApplication$$EnhancerBy SpringCGLIB$$a9604da6","rest":"===<call trace-1>===" } {"@timestamp":"2016-12-04T06:57:59.061+00:00","severity":"INFO","service":"trac e-1","trace":"589ee5f7b860132f","span":"2df8511ddf3d79a2","exportable":"false","pid ":"19756","thread":"http-nio-9101-exec-l","class":"o.s.c.a.AnnotationConfigApplicat ionContext","rest":"Refreshing org. springframework. context. annotation. Annotation- ConfigApplicationContext@64951f38: startup date [Sun Dec 04 14: 57: 59 CST 2016]; parent: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationCon text@4b8c8f15"}
除了可以通过上面的方式生成 JSON 文件外, 还可以使用 LogstashTcpSocketAppender 将日志内容直接通过 TcpSocket 输出到 Logstash 服务端, 比如:
<appender name="logstash" class="net.logstash.logback.appender.LogstashTcpSocketAppender"> <destination>127.0.0.1:9250</destination> ... </appender>
与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>l.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.SRS</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, 所以此时可以在ZipkinServer的管理页面中选择合适的查询条件, 单击Find Traces按钮, 就可以查询出刚才在日志中出现的跟踪信息了(也可以根据日志中的Trace ID, 在页面右上角的输入框中来搜索), 页面如下所示。
单击下方trace-1端点的跟踪信息, 还可以得到Sleuth跟踪到的详细信息, 其中包括我们关注的请求时间消耗等。
单击导航栏中的Dependencies菜单, 还可以查看ZipkinServer根据跟踪信息分析生成的系统请求链路依赖关系图, 如下图所示。
消息中间件收集
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-sleuth-stream 以及基于 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的前端依赖zipkin- autoconfigure-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 的时间戳之差, 我们可以得到当前服务端处理请求的时间消耗。
• er (Client Received):该 Annotation 用来记录客户端接收到服务端的回复, 同时 它也标识了这个 HTTP 请求的结束。 通过计算 er 与 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、 SpanID以及A nnotation。 由千这些标签都源自 一个请求, 所 以它们的TraceID相同,而标签1和 标签10 是起始和结束节点,它们的TraceID与SpanID 是相同的。
根据SpanID, 我们可以 发现在这些标签中一共产生了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 的总体依赖时间(er - 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 ZipkinMessageLisrener{ final Collector colletor;
@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的列表中, 具体内容如下所示:
上图展示了转换后的ZipkinSpan对象的详细内容, 可以看到很多熟悉的名称, 也就是之前介绍的关于Zipkin中的各个基本概念。而这些基本概念的值我们也都可以在之前Sleuth 的原始Span中找到, 其中Annotations和BinaryAnnotations有一些特殊。 在Sleuth定义的Span中没有使用相同的名称,而是使用了logs和 tags来命名。从这里的详细信息中, 我们 可以直观地看到Annotations和BinaryAnnotations的作用, 其中Annotations中存储了当前 Span包含的各种事件状态以及对应事件状态的时间戳,而BinaryAnnotations则存储了对事 件的补充信息, 比如上图中存储的就是该HTTP请求的细节描述信息, 除此之外, 它也可以存储对调用函数的详细描述(如下图所示) 。
下面我们再来详细看看通过调试消息监听程序接收到的这5个Span内容 。首先, 可以 发现, 每个Span中都包含有3个ID 信息, 其中除了标识 Span 自身的ID 以及用来标识整条链路的traceld之外, 还有一个之前没有提过的parentld, 它是用来标识各 Span父子关系 的ID( 它的值来自于上 一步执行单元 Span的ID)。 通过 parentld的定义我们可以为每个 Span找到它的前置节点, 从而定位每个Span在请求调用链中的确切位置 。 在每条调用链路中都有一个特殊的Span, 它的parentld为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代表客户端发送请 求, er代表客户端接收请求), 省略了 一些其他细节信息, 比如服务名、 时间戳、 IP 地址、 端口号等信息) ;Binary Annotation代表了事件的补充信息(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发送给ZipkinServer。
• Span ID = B的标签有4个, 分别是序号 3、 4、 7、 8, 该Span比较特殊, 它的产生跨越了两个实例, 其中标签3和8是由trace-1生成的, 而标签4和7则是 由trace-2生成的, 所以该标签会拆分成两个 Span内容发送给ZipkinServer。 trace-1会在标签8结束的时候将标签3和8合并成一个Span发送给ZipkinServer, 而trace-2会在标签7结束的时候将标签4和7合并成一个Span发送给Zipkin Server。
• Span ID = C的标签有2个, 分别是序号5和6, 它们分别表示了trace-2请求 接收后,具体处理方法调用的开始和结束。 该Span的内容在标签6执行结束后,由 trace-2将标签2和9合并成一个Span发送给ZipkinServer。
所以, 根据上面的分析, Zipkin总共会收到5个Span: 一个SpanT, 一个SpanA, 两 个SpanB, 一个SpanC。 结合之前请求链路的标签图和这里的Span记录, 我们可以总结 出如下图所示的Span收集过程, 读者可以参照此图来理解Span收集过程 的处理逻辑以及各个Span之间的关系。
虽然,Zipkin服务端接收到了5个Span, 但就如前文中分析的那样,其中有两个Span ID=B的标签, 由于它们来自于同一个HTTP请求(trace-1对trace-2的服务调用), 概念上它们属于同一个工作单元, 因此Zipkin 服务端在前端展现分析详情时会将这两个 Span合并显示,而合并后的Span数量就是在请求链路详情页面中TotalSpans的数量。
下图是本章示例的一个请求链路详情页面,在页面中显示了各个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> <groupld>org.jooq</groupld> <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<:114ffd85950bl17b, e7aca79a855ff80a.114ffd85950bl17b<: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.MessagingSpanEx七ractor : 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, Bce053ed8c2410a0.9ebc8131ebdb07fc<:Bce053ed8c2410a0, 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=l23456 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, 当有被抽样收集的跟踪信息时(调试时可以设置Always Sampler抽样机制来让每个跟踪信息都被收集),查看MySQL中的两张表, 可以得到类似下面的数据信息。
• zipkin_spans表
• zipkin_annotations表
表中所存储的信息我们已经非常熟悉, 之前分析的内容都可以在这两张表中体现出来 。 比如Span的数量问题,从zipkin_spans表中, 我们可以看到一次请求调用链路的跟踪信息产生了4条span数据,也就是说,在入zipkin_spans表的时候,已经对收集的span 信息进行了合并, 所以在查询详细信息时, 不需要每次都来计算合并 span。 而在 zipkin_annotations表中, 通过span_id字段可以关联到每个具体工作单元的详细信息, 同时根据endpoint_service_narne和span_id字段还可以计算出一次请求调用链路中总共接收到的span数量。
Zipkin在存储方面除了对MySQL有扩展组件之外, 还实现了对Cassandra和 ElasticSearch的支持扩展。 具体的整合方式与MySQL的整合类似, 读者可自行查阅Zipkin 的官方文档做进一步的了解。
API接口
Zipkin不仅提供了UI模块让用户可以使用Web页面来方便地查看跟踪信息, 它还提供了丰富的RESTful API接口供用户在第三方系统中调用来定制自己的跟踪信息展示或监控。 我们可以在ZipkinServer启动时的控制台或日志中找到Zipkin服务端提供的RESTful API定义, 比如下面的日志片段:
s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/api/vl/dependencies),methods=[GET),produces=[application/json)}"... s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{ [/api/vl/trace/{traceid}],methods=[GET),produces=[application/json))"... s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{ [/api/vl/traces],methods=[GET),produces=[application/json))"... s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{ [/api/vl/services],methods=[GET))" ... s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{ [/api/vl/spans) ,methods=[GET) }" ... s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{ [/api/vl/spans), methods= [POST)} 11 ... s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/api/vl/spans),methods=[POST],consumes=[application/x-thrift]}"...
可以看到 Zipkin Server 提供的API 接口都以/api/v1 路径作为前缀,它们的具体功能整理如下:
更多关于接口的请求参数和请求返回格式等细节说明, 可以通过访问 Zipkin 官方的 API 页面 http://zipkin.io/zipkin-api/来查看, 帮助我们根据自身系统架构来访问 Zipkin Server 以定制自己的 Dashboard 或监控系统。 实际上, Zipkin 的 UI 模块也是基于 RESTfulAPI 接口来实现的,有兴趣的读者可以通过浏览器的开发者模式来查看每个页面发起的请求, 以此作为调用样例来参考。