一、前言

在微服务数量较多的系统架构中,一个完整的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新增一下依赖:

1
2
3
4
        <dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>

2.2 User-Server新增测试请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController
@RequestMapping("testSleuth")
@Slf4j
@Getter
@Setter
public class TestSleuthController {

@Value("${eureka.instance.instance-id}")
private String instanceId;

@GetMapping
public String testSleuth() {
log.info("调用User-Server-{}的testSleuth接口",instanceId);
return instanceId;
}
}

2.3 User-Web新增测试请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@RestController
@RequestMapping("testSleuth")
@Slf4j
@Getter
@Setter
public class TestSleuthController {

@Value("${eureka.instance.instance-id}")
private String instanceId;

@Autowired
private RestTemplate restTemplate;

@GetMapping
public 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依赖

1
2
3
4
        <dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-zipkin</artifactId>
</dependency>

或者将上面已经引入的sleuth取掉,直接引入:

1
2
3
4
        <dependency>
<groupId>org.springframework.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相关的配置项。如下:

1
2
3
4
5
6
7
8
9
10
11
spring:
application:
name: User-Server
zipkin:
base-url: http://127.0.0.1:9411/
discovery-client-enabled: false
sender:
type: web
sleuth:
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
--
-- 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

常用接口说明如下:

接口路径请求方式接口描述
/dependenciesGET用来获取通过收集到的 Span 分析出的依赖关系
/servicesGET用来获取服务列表
/spansGET根据服务名来获取所有的 Span 名
/spansPOST向 Zipkin Server 上传 Span
/dependenciesGET根据 Trace ID 获取指定跟踪信息的 Span 列表
/dependenciesGET根据指定条件查询并返回符合条件的 trace 清单
/trace/{traceIdHex}GET根据Trace ID获取指定跟踪信息的Span列表
/tracesGET根据指定条件查询并返回符合条件的trace清单