现在我们已经完成服务注册中心、分布式配置中心、网关服务的搭建,整个工程的完成度变得越来越高了,这是值得欣慰的。废话不多说,开始今天的主题,今天我们要做得是微服务调用链追踪。
随着业务的发展,系统被拆分成一个个微服务,服务与服务之间的调用关系变得错综复杂,有可能一个请求最终需要调用很多个服务才能完成,当整个请求变慢或者不可用时,我们怎么快速定位,找到问题所在。这时,分布式系统调用链追踪的解决方案就很好的解决了我们的困惑。在本篇文章中,我们将详细介绍如何使用Spring Cloud Sleuth + Zipkin来为我们的微服务架构增加分布式服务跟踪的能力。
1. Spring Cloud Sleuth介绍
服务追踪的追踪单元是从客户发起请求(request)抵达被追踪系统的边界开始,到被追踪系统向客户返回响应(response)为止的过程,称为一个 trace
。每个 trace 中会调用若干个服务,为了记录调用了哪些服务,以及每次调用的消耗时间等信息,在每次调用服务时,埋入一个调用记录,称为一个span
。这样,若干个有序的 span 就组成了一个 trace。在系统向外界提供服务的过程中,会不断地有请求和响应发生,也就会不断生成 trace,把这些带有 span 的 trace 记录下来,就可以描绘出一幅系统的服务拓扑图。附带上 span 中的响应时间,以及请求成功与否等信息,就可以在发生问题的时候,找到异常的服务;根据历史数据,还可以从系统整体层面分析出哪里性能差,定位性能优化的目标。
Spring Cloud Sleuth 为服务之间调用提供链路追踪。通过 Sleuth 可以很清楚的了解到一个服务请求经过了哪些服务,每个服务处理花费了多长。从而让我们可以很方便的理清各微服务间的调用关系。此外 Sleuth 可以帮助我们:
- 耗时分析:通过 Sleuth 可以很方便的了解到每个采样请求的耗时,从而分析出哪些服务调用比较耗时;
- 可视化错误:对于程序未捕捉的异常,可以通过集成 Zipkin 服务界面上看到;
- 链路优化:对于调用比较频繁的服务,可以针对这些服务实施一些优化措施。
Spring Cloud Sleuth 可以结合 Zipkin,将信息发送到 Zipkin,利用 Zipkin 的存储来存储信息,利用 Zipkin UI 来展示数据。
2. Zipkin介绍
Zinkin致力于收集服务的定时数据,以解决微服务架构中的延迟问题,包括数据的收集、存储、查找和展现。Zipkin的基本架构如下图(图片来自网络,侵删):
它主要由4个核心组件构成:
- Collector:收集器组件,它主要用于处理从外部系统发送过来的跟踪信息,将这些信息转换为 Zipkin 内部处理的 Span 格式,以支持后续的存储、分析、展示等功能。
- Storage:存储组件,它主要对处理收集器接收到的跟踪信息,默认会将这些信息存储在内存中,我们也可以修改此存储策略,通过使用其他存储组件将跟踪信息存储到数据库中。
- RESTful API:API 组件,它主要用来提供外部访问接口。比如给客户端展示跟踪信息,或是外接系统访问以实现监控等。
- Web UI:UI 组件,基于 API 组件实现的上层应用。通过 UI 组件用户可以方便而有直观地查询和分析跟踪信息。
3. 实战
Zipkin分为两端,一个是Zipkin服务端,一个是Zipkin客户端,其中客户端就是我们一个个微服务。
客户端会配置服务端的 URL 地址,一旦发生服务间的调用的时候,会被配置在微服务里面的 Sleuth 的监听器监听,并生成相应的 Trace 和 Span 信息发送给服务端。
发送的方式主要有两种,一种是 HTTP 报文的方式,还有一种是消息总线的方式如 RabbitMQ。
今天我们将采用第二种方式,使用RabbitMQ发送。为什么?试想一下,假设现在有Zipkin服务端A,然后有客户端B、C、D……那么难道我们每个客户端都要去配置服务端的地址吗?假设有一天服务端A的地址变了,那么是不是每个客户端都要去修改呢?显然这种方式不够人性化,而如果采用RabbitMQ消息中间件,客户端就不需要知道服务端的地址,只要客户端发送出去的数据能够被服务端消费到即可,这正是利用了消息中间件实现服务与服务之间的解耦。
默认情况下Zipkin采用内存方式进行存储,如果服务端重启,那么之前所有的数据都将丢失。因此Zipkin提供了可插拔数据存储方式:In-Memory、MySql、Cassandra 以及 Elasticsearch。今天我们将采用MySQL数据存储方式。
再说一点,关于 Zipkin 的服务端,在使用 Spring Boot 2.x 版本后,官方就不推荐自行定制编译了,反而是直接提供了编译好的 jar 包来给我们使用,所以我们就不用自己搭建Zipkin的服务端了,直接使用编译好的jar即可。
说了这么多,真的要开始实战操练了,请做好准备。
3.1 搭建Zipkin服务端
我这里是从网上下载了zipkin-server-2.16.2-exec.jar
的jar包作为服务端,然后在运行之前,我们需要到数据库创建几张表,sql如下:
--DROP TABLE zipkin_spans;
--DROP TABLE zipkin_annotations;
--DROP TABLE zipkin_dependencies;
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'
) ENGINE=INNODB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;
ALTER TABLE zipkin_spans ADD UNIQUE KEY(`trace_id_high`, `trace_id`, `id`) COMMENT 'ignore INSERT ON duplicate';
ALTER TABLE zipkin_spans ADD INDEX(`trace_id_high`, `trace_id`, `id`) COMMENT 'for joining WITH zipkin_annotations';
ALTER TABLE zipkin_spans ADD INDEX(`trace_id_high`, `trace_id`) COMMENT 'for getTracesByIds';
ALTER TABLE zipkin_spans ADD INDEX(`name`);
ALTER TABLE zipkin_spans ADD INDEX(`remote_service_name`) COMMENT 'for getTraces AND getSpanNames';
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';
ALTER TABLE zipkin_annotations ADD INDEX(`a_key`) COMMENT 'for getTraces';
CREATE TABLE IF NOT EXISTS zipkin_dependencies (
`day` DATE NOT NULL,
`parent` VARCHAR(255) NOT NULL,
`child` VARCHAR(255) NOT NULL,
`call_count` BIGINT
) ENGINE=INNODB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;
ALTER TABLE zipkin_dependencies ADD UNIQUE KEY(`day`, `parent`, `child`);
3.2 启动Zipkin服务端
在zipkin-server-2.16.2-exec.jar
所在的目录打开终端,执行以下语句即可启动服务端。
java -jar zipkin-server-2.16.2-exec.jar --zipkin.collector.rabbitmq.addresses=localhost --STORAGE_TYPE=mysql --MYSQL_DB=leyou_zipkin --MYSQL_HOST=localhost --MYSQL_TCP_PORT=3380 --MYSQL_USER=root --MYSQL_PASS=Anbang713
指定RabbitMQ地址、MySQL数据库信息,看到如下信息,则表示启动成功,默认端口是:9411。
通过浏览器访问http://localhost:9411
则可以看到Zipkin Web UI界面了。
至此Zipkin服务端已完成搭建,接下来看下如何改造客户端。我们将在之前的网关服务上集成Spring Cloud Sleuth + Zipkin。
3.3 搭建Zipkin客户端
3.3.1 引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-rabbit</artifactId>
</dependency>
3.3.2 添加注解
无
3.3.3 添加配置
spring:
cloud:
bus:
trace:
enabled: true
enabled: true
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
virtual-host: /
sleuth:
web:
client:
enabled: true
sampler:
probability: 1.0 # 将采样比例设置为 1.0,也就是全部都需要。默认是 0.1
3.4 启动Zipkin客户端
观察到两个现象:
(1)集成Sleuth+Zipkin的服务启动日志输出有一点点不一样了,如下图:
(2)服务启动成功后,自动创建了一个名为zipkin
的队列,如下图:
3.5 测试
万事俱备,只欠测试了。我们要做的是微服务调用链追踪,那自然要发起请求,才有调用过程,调用链路。还记得我们在上篇做网关黑白名单拦截以及分布式配置中心自动刷新测试地址吗?我们直接调这个地址测试即可。调用完成后,回到Zipkin的Web UI界面可以看到服务名多了个ly-env-gateway-server
,这正是我们网关服务的名称,再点击查找时,可以看到刚刚调用的链路。
点击调用链路,会发现这里有更多的详细内容,比如持续时间、经过的服务数量、span总数等等。