使用Spring Cloud Sleuth跟踪微服务
一、前言
在微服务数量较多的系统架构中,一个完整的HTTP请求可能需要经过好几个微服务。如果想要跟踪一条完整的HTTP请求链路所产生的日志,我们需要到各个微服务上去查看日志并检索出我们需要的信息。随着业务发展,微服务的数量也会越来越多,这个过程也变得愈发困难。不过不用担心,Spring Cloud Sleuth为我们提供了分布式服务跟踪的解决方案,通过使用Sleuth可以让我们快速定位某个服务的问题。简单来说,Sleuth相当于调用链监控工具的客户端,集成在各个微服务上,负责产生调用链监控数据。官方文档地址如下: https://cloud.spring.io/spring-cloud-static/spring-cloud-sleuth/2.0.1.RELEASE/single/spring-cloud-sleuth.html 。为了演示如何使用Spring Cloud Sleuth,我们需要构建一个小型的微服务系统。
二、准备工作
这里我们需要对User-Server和User-Web项目进行改造,它们都具有一个名为testSleuth的REST接口,User-Web的testSleuth接口依赖于User-Server的testSleuth接口。并将这两个服务注册到Eureka-Server服务注册中心集群。
2.1 依赖引入
在User-Server和User-Web项目pom.xml新增一下依赖:
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-sleuth</artifactId></dependency>
2.2 User-Server新增测试请求
@RestController
@RequestMapping("testSleuth")
@Slf4j
@Getter
@Setter
public class TestSleuthController {@Value("${eureka.instance.instance-id}")private String instanceId;@GetMappingpublic String testSleuth() {log.info("调用User-Server-{}的testSleuth接口",instanceId);return instanceId;}
}
2.3 User-Web新增测试请求
@RestController
@RequestMapping("testSleuth")
@Slf4j
@Getter
@Setter
public class TestSleuthController {@Value("${eureka.instance.instance-id}")private String instanceId;@Autowiredprivate RestTemplate restTemplate;@GetMappingpublic String testSleuth() {log.info("调用User-Web-{}的testSleuth接口",instanceId);return this.restTemplate.getForEntity("http://user-server/testSleuth", String.class).getBody();}
}
三、测试Sleuth
我们往User-Web发送 http://localhost:8765/testSleuth 请求,

然后观察各自的日志:
User-Web
2020-09-23 23:21:28.544 INFO [User-Web,2c59102248cb2ec4,2c59102248cb2ec4,true] 14744 --- [nio-8765-exec-2] c.w.c.controller.TestSleuthController : 调用User-Web-user-web的testSleuth接口
User-Server
2020-09-23 23:21:28.763 INFO [User-Server,2c59102248cb2ec4,e395d592eebff9a9,true] 15148 --- [nio-8789-exec-1] c.w.c.controller.TestSleuthController : 调用User-Server-user-server2的testSleuth接口
可以看到,日志里出现了[User-Server,2c59102248cb2ec4,e395d592eebff9a9,true]信息,这些信息由Spring Cloud Sleuth生成,用于跟踪微服务请求链路。这些信息包含了4个部分的值,它们的含义如下:
1.User-Server微服务的名称,与spring.application.name对应;
2.2c59102248cb2ec4称为Trace ID,在一条完整的请求链路中,这个值是固定的。观察上面的日志即可证实这一点;
3.e395d592eebff9a9称为Span ID,它表示一个基本的工作单元;
4.true表示是否将数据输出到其他服务,true则会把信息输出到其他可视化的服务上观察。
四、集成Zipkin
虽然我们已经可以通过Trace ID来跟踪整体请求链路了,但是我们还是得去各个系统中捞取日志。在并发较高得时候,日志是海量的,这个时候我们可以借助Zipkin来代替我们完成日志获取与分析。Zipkin是Twitter的一个开源项目,主要包含了以下四个组件:
4.1 zikpin组件介绍
虽然我们已经可以通过Trace ID来跟踪整体请求链路了,但是我们还是得去各个系统中捞取日志。在并发较高得时候,日志是海量的,这个时候我们可以借助Zipkin来代替我们完成日志获取与分析。Zipkin是Twitter的一个开源项目,主要包含了以下四个组件:
Collector:收集器,负责收集日志信息,以供后续的存储,分析与展示;
Storage:存储模块,我们可以通过它将日志存储到MySQL中;
RESTful API:API组件,它主要用来提供外部访问接口。 比如给客户端展示跟踪信息,或是外接系统访问以实现监控等;
WEB UI:通过web页面,我们可以轻松的分析与跟踪请求日志。
zipkin结构图:

4.2 zikpin概念
Span(跨度):Span是基本的工作单元。Span包括一个64位的唯一ID,一个64位trace码,描述信息,时间戳事件,key-value 注解(tags),span处理者的ID(通常为IP)。最开始的初始Span称为根span,此span中span id和 trace id值相同。
Trance(跟踪):包含一系列的span,它们组成了一个树型结构
Annotation(标注):用于及时记录存在的事件。常用的Annotation如下:
- CS(Client Sent 客户端发送):客户端发送一个请求,表示span的开始
- SR(Server Received 服务端接收) :服务端接收请求并开始处理它。(SR - CS)等于网络的延迟
- SS(Server Sent 服务端发送):服务端处理请求完成,开始返回结束给服务端。(SR - SS)表示服务端处理请求的时间
- CR(Client Received 客户端接收) :客户端完成接受返回结果,此时span结束。(CR - CS)表示客户端接收服务端数据的时间
如果一个服务的调用关系如下:

那么此时将Span和Trace在一个系统中使用Zipkin注解的过程图形化如下:

每个颜色的表明一个span(总计7个spans,从A到G),每个span有类似的信息
Trace Id = X
Span Id = D
Client Sent
此span表示span的Trance Id是X,Span Id是D,同时它发送一个Client Sent事件
spans 的parent/child关系图形化如下:

4.3 引入依赖
我们继续对User-Server和User-Web项目进行改造,需要引入zikpin依赖
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-sleuth-zipkin</artifactId></dependency>
或者将上面已经引入的sleuth取掉,直接引入:
<dependency><groupId>org.spring#477px #152pxframework.cloud</groupId><artifactId>spring-cloud-starter-zipkin</artifactId></dependency>
4.4 zikpin服务端
进入到zikpin官网: https://zipkin.io/ ,下载服务端

注意:zipkin需要java8,请事先确保环境为java8环境
然后启动服务端:
java -jar zipkin-server-2.21.7-exec.jar

启动成功后,访问 http://127.0.0.1:9411/ 。

上面是zikpin默认端口号访问,当然也可以指定端口号启动zipkin服务
java -jar zipkin.jar --server.port=8080
访问地址: http://127.0.0.1:8080/ 。
也可以指定访问rabbitmq 启动
java -jar zipkin.jar --zipkin.collector.rabbitmq.addresses=127.0.0.1
搭建Zipkin集成RabbitMQ异步传输
- 启动RabbitMQ
- 启动Zipkin(自动会创建一个Zipkin 队列)
- 启动ZipkinClient以队列形式异步传输
这样服务端就搭建好了
4.5 配置改造
在User-Server和User-Web项目配置文件中,增加zipkin相关的配置项。如下:
spring:application:name: User-Serverzipkin:base-url: http://127.0.0.1:9411/discovery-client-enabled: falsesender:type: websleuth:sampler:probability: 1 # 设置抽样采集率为100%,默认为0.1,即10%,这里为了测试
然后启动项目
4.6 发起请求测试
请求 http://localhost:8765/testSleuth ,然后在zikpin-server中刷新查询,发现已经监控到请求。

点击下方的跟踪信息,我们可以看到整个链路经过了哪些服务,总耗时等信息:

点击“依赖分析”也可以看到请求里微服务的关系:

4.7 Zipkin数据持久化
Zipkin默认是将监控数据存储在内存的,如果Zipkin挂掉或重启的话,那么监控数据就会丢失。所以如果想要搭建生产可用的Zipkin,就需要实现监控数据的持久化。而想要实现数据持久化,自然就是得将数据存储至数据库。好在Zipkin支持将数据存储至:
- 内存(默认)
- MySQL
- Elasticsearch
- Cassandra
Zipkin数据持久化相关的官方文档地址: https://github.com/openzipkin/zipkin#storage-component ,Zipkin支持的这几种存储方式中,内存显然是不适用于生产的,这一点开始也说了。而使用MySQL的话,当数据量大时,查询较为缓慢,也不建议使用。Twitter官方使用的是Cassandra作为Zipkin的存储数据库,但国内大规模用Cassandra的公司较少,而且Cassandra相关文档也不多。下面我们将请求信息保存到MySQL中。官方文档地址 https://github.com/openzipkin/zipkin/tree/master/zipkin-storage/mysql-v1 。
4.7.1 首先初始化mysql脚本,脚本地址如下:
https://github.com/openzipkin/zipkin/blob/master/zipkin-storage/mysql-v1/src/main/resources/mysql.sql
--
-- Copyright 2015-2019 The OpenZipkin Authors
--
-- Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
-- in compliance with the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software distributed under the License
-- is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
-- or implied. See the License for the specific language governing permissions and limitations under
-- the License.
----创建数据库,用户create database zikpindb default character set utf8 collate utf8_general_ci;
create user 'zikpin'@'%' identified by 'zikpin#123';
create user 'zikpin'@'localhost' identified by 'zikpin#123';
grant all privileges on zikpindb.* to 'zikpin'@'%' identified by 'zikpin#123';
grant all privileges on zikpindb.* to 'zikpin'@'localhost' identified by 'zikpin#123';
flush privileges;use zikpindb;CREATE TABLE IF NOT EXISTS zipkin_spans (`trace_id_high` BIGINT NOT NULL DEFAULT 0 COMMENT 'If non zero, this means the trace uses 128 bit traceIds instead of 64 bit',`trace_id` BIGINT NOT NULL,`id` BIGINT NOT NULL,`name` VARCHAR(255) NOT NULL,`remote_service_name` VARCHAR(255),`parent_id` BIGINT,`debug` BIT(1),`start_ts` BIGINT COMMENT 'Span.timestamp(): epoch micros used for endTs query and to implement TTL',`duration` BIGINT COMMENT 'Span.duration(): micros used for minDuration and maxDuration query',PRIMARY KEY (`trace_id_high`, `trace_id`, `id`)
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;ALTER TABLE zipkin_spans ADD INDEX(`trace_id_high`, `trace_id`) COMMENT 'for getTracesByIds';
ALTER TABLE zipkin_spans ADD INDEX(`name`) COMMENT 'for getTraces and getSpanNames';
ALTER TABLE zipkin_spans ADD INDEX(`remote_service_name`) COMMENT 'for getTraces and getRemoteServiceNames';
ALTER TABLE zipkin_spans ADD INDEX(`start_ts`) COMMENT 'for getTraces ordering and range';CREATE TABLE IF NOT EXISTS zipkin_annotations (`trace_id_high` BIGINT NOT NULL DEFAULT 0 COMMENT 'If non zero, this means the trace uses 128 bit traceIds instead of 64 bit',`trace_id` BIGINT NOT NULL COMMENT 'coincides with zipkin_spans.trace_id',`span_id` BIGINT NOT NULL COMMENT 'coincides with zipkin_spans.id',`a_key` VARCHAR(255) NOT NULL COMMENT 'BinaryAnnotation.key or Annotation.value if type == -1',`a_value` BLOB COMMENT 'BinaryAnnotation.value(), which must be smaller than 64KB',`a_type` INT NOT NULL COMMENT 'BinaryAnnotation.type() or -1 if Annotation',`a_timestamp` BIGINT COMMENT 'Used to implement TTL; Annotation.timestamp or zipkin_spans.timestamp',`endpoint_ipv4` INT COMMENT 'Null when Binary/Annotation.endpoint is null',`endpoint_ipv6` BINARY(16) COMMENT 'Null when Binary/Annotation.endpoint is null, or no IPv6 address',`endpoint_port` SMALLINT COMMENT 'Null when Binary/Annotation.endpoint is null',`endpoint_service_name` VARCHAR(255) COMMENT 'Null when Binary/Annotation.endpoint is null'
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;ALTER TABLE zipkin_annotations ADD UNIQUE KEY(`trace_id_high`, `trace_id`, `span_id`, `a_key`, `a_timestamp`) COMMENT 'Ignore insert on duplicate';
ALTER TABLE zipkin_annotations ADD INDEX(`trace_id_high`, `trace_id`, `span_id`) COMMENT 'for joining with zipkin_spans';
ALTER TABLE zipkin_annotations ADD INDEX(`trace_id_high`, `trace_id`) COMMENT 'for getTraces/ByIds';
ALTER TABLE zipkin_annotations ADD INDEX(`endpoint_service_name`) COMMENT 'for getTraces and getServiceNames';
ALTER TABLE zipkin_annotations ADD INDEX(`a_type`) COMMENT 'for getTraces and autocomplete values';
ALTER TABLE zipkin_annotations ADD INDEX(`a_key`) COMMENT 'for getTraces and autocomplete values';
ALTER TABLE zipkin_annotations ADD INDEX(`trace_id`, `span_id`, `a_key`) COMMENT 'for dependencies job';CREATE TABLE IF NOT EXISTS zipkin_dependencies (`day` DATE NOT NULL,`parent` VARCHAR(255) NOT NULL,`child` VARCHAR(255) NOT NULL,`call_count` BIGINT,`error_count` BIGINT,PRIMARY KEY (`day`, `parent`, `child`)
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;
4.7.2 启动zikpin
然后使用下面命令启动zikpin:
java -jar zipkin-server-2.21.7-exec.jar --STORAGE_TYPE=mysql --MYSQL_HOST=127.0.0.1 --MYSQL_TCP_PORT=3306 --MYSQL_DB=zikpindb --MYSQL_USER=zikpin --MYSQL_PASS=zikpin#123

4.7.3 调用请求
调用 http://localhost:8765/testSleuth 发起请求。
4.7.4 数据库验证

4.8 Zipkin API
正如上面所说的,Zipkin提供了RESTful API供我们调用,可以参考 https://zipkin.io/zipkin-api/ ,请求对应地址可以获取对应信息,如获取服务列表: http://127.0.0.1:9411/api/v2/services

常用接口说明如下:
|接口路径 |请求方式 |接口描述 |
|:--|
|/dependencies |GET |用来获取通过收集到的 Span 分析出的依赖关系 |
|/services |GET |用来获取服务列表 |
|/spans |GET |根据服务名来获取所有的 Span 名 |
|/spans |POST |向 Zipkin Server 上传 Span |
|/dependencies |GET |根据 Trace ID 获取指定跟踪信息的 Span 列表 |
|/dependencies |GET |根据指定条件查询并返回符合条件的 trace 清单 |
|/trace/{traceIdHex} |GET |根据Trace ID获取指定跟踪信息的Span列表 |
|/traces |GET |根据指定条件查询并返回符合条件的trace清单 |