前言

在微服务架构下,⼀次请求少则经过三四次服务调用完成,多则跨越⼏⼗个甚⾄是上百个服务节点。那么问题接踵而来: 1)如何动态展示服务的调用链路?(比如A服务调用了哪些其他的服务---依赖关系) 2)如何分析服务调用链路中的瓶颈节点并对其进行调优? 比如A—>B—>C, C服务处理时间特别长) 3)如何快速进行服务链路的故障发现?

分布式链路追踪就是为了解决这些问题而诞生的。如果我们在⼀个请求的调⽤处理过程中,在各个链路节点都能够记录下日志,并最终将日志进⾏集 中可视化展示,那么我们想监控调用链路中的⼀些指标就有希望了。分布式链路追踪就是基于这种思想来实现的。

Spring Cloud Sleuth (追踪服务框架)可以追踪服务之间的调⽤, Sleuth可以记录⼀个服务请求经过哪些服务、服务处理时⻓等,根据这些,我们能够理清各微服务间的调⽤关系及进⾏问题追踪分析。 耗时分析:通过 Sleuth 了解采样请求的耗时,分析服务性能问题(哪些服务调用⽐较耗时); 链路优化:发现频繁调⽤的服务,针对性优化等; Sleuth就是通过记录日志的方式来记录踪迹数据的

注意:我们往往把Spring Cloud SleuthZipkin ⼀起使⽤,把 Sleuth 的数据信息发送给 Zipkin 进行聚合,利⽤ Zipkin 存储并展示数据。

如下图所示,标识了⼀个请求链路,⼀条链路通过TraceId唯⼀标识, span标识发起的请求信息,各span通过parrentId关联起来:

实战

sleuth配置

在每个需要追踪的微服务工程上都引入依赖坐标:

1
2
3
4
5
<!--链路追踪-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>

每个微服务都修改application.yml配置文件,添加日志级别控制:

1
2
3
4
5
#分布式链路追踪
logging:
level:
org.springframework.web.servlet.DispatcherServlet: debug
org.springframework.cloud.sleuth: debug

当我们有请求时,我们可以在控制台观察到sleuth输出的日志(包含全局traceId、spanId等):

1
2
2021-10-17 20:50:22.917 DEBUG [lagou-service-autodeliver,4191b6ed2cdd49d3,4191b6ed2cdd49d3,false] 10340 --- [nio-8090-exec-4] o.s.web.servlet.DispatcherServlet        : GET "/autodeliver/checkStateTimeoutFallBack/1545132", parameters={}
2021-10-17 20:50:25.263 DEBUG [lagou-service-autodeliver,4191b6ed2cdd49d3,4191b6ed2cdd49d3,false] 10340 --- [nio-8090-exec-4] o.s.web.servlet.DispatcherServlet : Completed 200 OK

在控制台输出的日志不易查看,而且还需要手动统计一些时间等,日志还分散在多个不同的服务上,不好处理。zipkin就是为了统一聚合轨迹日志并进行展示的,接下来我们用zipkin来聚合日志并进行存储展示。

zipkin配置

Zipkin包括Zipkin Server和 Zipkin Client两部分, Zipkin Server是⼀个单独的服务, Zipkin Client就是具体的微服务。

ZipkinServer构建

我们需要新建一个module,作为单独的zipkin Server服务。

引入坐标:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
	<!--zipkin-server的依赖坐标-->
<dependency>
<groupId>io.zipkin.java</groupId>
<artifactId>zipkin-server</artifactId>
<version>2.12.3</version>
<exclusions>
<!--排除掉log4j2的传递依赖,避免和springboot依赖的⽇志组件冲突-->
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--zipkin-server ui界⾯依赖坐标-->
<dependency>
<groupId>io.zipkin.java</groupId>
<artifactId>zipkin-autoconfigure-ui</artifactId>
<version>2.12.3</version>
</dependency>

入口启动类:

1
2
3
4
5
6
7
@SpringBootApplication
@EnableZipkinServer //开启ZipKin Server功能
public class ZipkinServerApplication9411 {
public static void main(String[] args) {
SpringApplication.run(ZipkinServerApplication9411.class,args);
}
}

application.yml:

1
2
3
4
5
6
7
server:
port: 9411
management:
metrics:
web:
server:
auto-time-requests: false

启动zipkin服务,我们在浏览器访问http://localhost:9411,可以看到zipkin的ui界面:

ZipKin Client构建

需要在每一个zipkin client中做配置。

第一步,在pom.xml中导入依赖:

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

第二步,在application.yml中添加对zipkin server的引用:

1
2
3
4
5
6
7
8
9
10
11
12
spring:  
zipkin:
base-url: http://127.0.0.1:9411 #zipkin server的请求地址
sender:
# web 客户端将踪迹⽇志数据通过⽹络请求的⽅式传送到服务端,另外还有配置
# kafka/rabbit 客户端将踪迹⽇志数据传递到mq进⾏中转
type: web
sleuth:
sampler:
# 采样率 1 代表100%全部采集 ,默认0.1 代表10% 的请求踪迹数据会被采集
# ⽣产环境下,请求量⾮常⼤,没有必要所有请求的踪迹数据都采集分析,对于⽹络包括 server端压⼒都是⽐较⼤的,可以配置采样率采集⼀定⽐例的请求的踪迹数据进⾏分析即可
probability: 1

可以在zipkin的图形化界面中看到各个服务之间的依赖关系:

还可以看到一些别的信息,比如服务之间调用的时长等:

ZipKin 持久化

我们请求链路的数据都是存储在zipkin server的内存中的,一旦服务重启之后就会丢失,所以我们需要将zipkin中的数据进行持久化。我们可以持久化到elasticsearch以及mysql等数据库中去。这里我们主要是学习将zipkin的数据存储到mysql当中去。

创建数据库

mysql中创建名为zipkin的数据库,并执行如下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
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;
导入依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
		<dependency>
<groupId>io.zipkin.java</groupId>
<artifactId>zipkin-autoconfigure-storage-mysql</artifactId>
<version>2.12.3</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
</dependency>
配置文件application.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/zipkin?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true&serverTimezone=GMT
username: root
password: '000'
druid:
initial-size: 10
min-idle: 10
max-active: 30
max-wait: 50000
zipkin:
storage:
type: mysql
启动类中注入事务管理器
1
2
3
4
  @Bean
public PlatformTransactionManager txManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}

经过这几个步骤之后,我们执行一个服务的请求,可以看到数据库表中多了数据:

执行请求,从网关开始调用: 查看数据库:

再次重启zipkin server服务,会发现之前请求的数据还在。

源码

源码下载

总结

我们主要是学习了链路追踪的相关知识,了解了现在主流的链路追踪的一些方案,简单使用sleuth+zipkin来实现了分布式中链路追踪。