创作博客的目的是希望将自己掌握的知识系统地整理一下,并以博客的形式记录下来。这不仅是为了帮助其他有需要的人查阅相关内容,也是为了自己能够更好地巩固和加深对这些知识的理解。创作的时候也是对自己所学的一次复盘和总结,在创作的过程中,自己也会更全面深入的思考和总结,对于知识的理解也更加深刻,这也让我更加愿意花时间和精力去创作。此外,日后在需要的时候,也可以方便地回顾和参考这些记录。
感谢大家的关注和支持,希望我的分享对你们有所帮助。如果有任何建议或问题,欢迎随时与我交流讨论!
一、引言
随着微服务架构的广泛应用,系统的复杂性也随之增加。在这种复杂的系统中,应用通常由多个相互独立的服务组成,每个服务可能分布在不同的主机上。微服务架构虽然提高了系统的灵活性和可扩展性,但也带来了新的挑战,尤其是在故障排查和性能优化方面。这时,链路追踪(Tracing)成为了一个非常重要的工具。
注:图片来自网络
如图,在复杂的调用链路中假设存在一条调用链路响应缓慢,如何定位其中延迟高的服务呢?
- 日志: 通过分析调用链路上的每个服务日志得到结果,这种方式耗时高效率低
- zipkin:Zipkin是Twitter开源的分布式跟踪系统,是开箱即用的产品,主要用来收集系统的时许数据,从而追踪系统的调用问题,使用zipkin的web UI可以一眼看出延迟高的服务。
- SkyWalking等
本文只介绍zipkin的使用方式。
如图所示,各业务系统在彼此调用时,将特定的跟踪消息传递至zipkin,zipkin在收集到跟踪信息后将其聚合处理、存储、展示等,用户可通过web UI方便获得网络延迟、调用链路、系统依赖等等。
使用zipkin涉及到以下几个概念:
- Span:基本工作单元,一次链路调用(可以是RPC,DB等没有特定的限制)创建一个span,通过一个64位ID标识它,span通过还有其他的数据,例如描述信息,时间戳,key-value对的(Annotation)tag信息,parent-id等,其中parent-id可以表示span调用链路来源,通俗的理解span就是一次请求信息
- Trace:类似于树结构的Span集合,表示一条调用链路,存在唯一标识
- Annotation:注解,用来记录请求特定事件相关信息(例如时间),通常包含四个注解信息
- cs: Client Start,表示客户端发起请求
- sr:Server Receive,表示服务端收到请求
- ss:Server Send,表示服务端完成处理,并将结果发送给客户端
- cr:Client Received,表示客户端获取到服务端返回信息
- BinaryAnnotation:提供一些额外信息,一般已key-value对出现
二、安装Zipkin
Zipkin 是由推特开发的分布式链路追踪系统,用于对 Sleuth 产生的日志加以收集并采用可视化的数据对链路追踪进行分析与图表展示,Zipkin 是典型的 C/S(客户端与服务端)架构模式,需要独立部署 Zipkin 服务器,同时也需要在微服务内部持有Zipkin客户端才可以自动实现日志的推送与展示。
1. Jar包部署
下载地址
运行
java -jar zipkin-server-2.3.1-exec.jar
这样zipkin就是以内存存储的方式进行启动了
2. docker部署
docker-zipkin官方文档:docker-zipkin
- 使用以下命令从Docker Hub上拉取Zipkin的Docker镜像:
docker pull openzipkin/zipkin
- 运行Zipkin容器
docker run -d --restart always -p 9411:9411 --name zipkin openzipkin/zipkin
该命令将Zipkin以守护进程模式(-d)运行Zipkin容器,并设置容器在退出时始终重启(–restart always)。同时,将容器的9411端口映射到主机的9411端口,以便外部访问。
- 测试
Zipkin服务启动后,可以在浏览器中访问以下URL查看Zipkin的Web UI:http://localhost:9411,如果出现下面的画面,那么代表我们zipkin服务配置成功了
三、项目集成Zipkin
在 Spring Cloud 中集成 Zipkin使用起来还是很方便的,只需引入相关依赖并配置 Zipkin 的地址即可,然后就会把请求的监控数据发往 Zipkin。
- 引入依赖
安装完成后,我们需要引入 sleuth 和 zipkin的依赖。
<!-- sleuth 可以省略-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<!-- zipkin链路追踪 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
zipkin依赖同时包含了seuth,可以省略sleuth的引用
- 添加Zipkin相关配置
spring:
# sleuth 配置
sleuth:
web:
client:
enabled: true
sampler:
probability: 1.0 # 采样比例为: 0.1(即10%),设置的值介于0.0到1.0之间,1.0则表示全部采集。默认为0.1
# zipkin 配置
zipkin:
base-url: http://192.168.253.10:9411 # 指定了Zipkin服务器的地址
参数示例:
spring:
zipkin:
base-url: http://<zipkin-server-address>:<zipkin-port>/ # Zipkin服务器地址,例如:http://localhost:9411/
sender:
type: web # 发送追踪数据的方式,通常是'web'(HTTP)或'kafka'
service:
name: ${spring.application.name} # 服务名,通常会从spring.application.name属性中获取
sleuth:
sampler:
probability: 1.0 # 采样率,1.0表示捕获所有追踪信息,0.1表示10%的追踪信息
web:
client:
# 客户端相关的配置,例如是否开启日志输出等
skip-pattern: /health.*,/info.*,/metrics.* # 匹配这些URL模式的请求将不会被追踪
baggage-keys: # 行李标签(Baggage),可以在整个分布式追踪中传递的键值对
- some-key
- another-key
propagation:
type: b3 # 追踪传播的格式,如b3(Zipkin的默认格式)
log:
slf4j:
enabled: true # 是否在日志中启用Sleuth的日志输出
cloud:
stream:
default-binder: rabbit # 如果你使用RabbitMQ作为消息代理,则配置此选项(对于Kafka或其他消息代理,配置会不同)
bindings:
output: # 如果使用消息队列(如RabbitMQ或Kafka)来发送追踪数据,则配置此部分
destination: zipkin
content-type: application/x-protobuf
配置说明:
spring.application.name:应用的名称,将作为Zipkin中追踪的服务名称。
spring.sleuth.sampler.probability:采样率,决定哪些追踪数据会被发送到Zipkin服务器。设置为1.0表示捕获所有追踪信息。
spring.sleuth.web.client.skip-pattern:正则表达式模式列表,用于指定哪些URL模式的请求将不会被追踪。
spring.sleuth.baggage-keys:行李标签(Baggage)的键列表,这些键值对可以在整个分布式追踪中传递。
spring.sleuth.propagation.type:追踪传播的格式,如b3是Zipkin的默认格式。
spring.sleuth.log.slf4j.enabled:是否在日志中启用Sleuth的日志输出。
spring.zipkin.base-url:Zipkin服务器的地址,追踪数据将发送到这个地址。
spring.zipkin.sender.type:发送追踪数据的方式,如通过HTTP(‘web’)或Kafka。
spring.zipkin.compression.enabled:是否启用发送前的数据压缩。
spring.cloud.stream.bindings.output:如果使用消息队列发送追踪数据,这部分配置指定了消息队列的目的地、内容类型等。
请注意,这些配置可能会因Spring Cloud和Spring Cloud Sleuth的版本不同而有所变化。
启动项目,查看请求链路情况
四、Zipkin数据持久化
Zipkin 默认是将监控数据存储在内存的,如果Zipkin 挂掉或重启的话,那么监控数据就会丢失。所以如果想要搭建生产可用的Zipkin,就需要实现监控数据的持久化。而想要实现数据持久化,自然就是得将数据存储至数据库。
Zipkin 支持将数据存储至:
- 内存(默认)
- MySQL
- Elasticsearch
- Cassandra
1. 内存存储
默认方式
用法示例:
java -jar zipkin.jar
参数:
MEM_MAX_SPANS:保存的最大 span 的数量,超过了会把最早的 span 删除。默认为500000
将内存中保存的span加倍的示例:
MEM_MAX_SPANS=1000000 java -Xmx1G -jar zipkin.jar
2. mysql存储
首先我们需要在数据库中,执行下面的官方SQL脚本,创建对应的表:官方脚本传送门
如果上述地址过期,请执行下面的SQL脚本,在之前,需要创建一个数据库,叫zipkin
--
-- Copyright The OpenZipkin Authors
-- SPDX-License-Identifier: Apache-2.0
--
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;
执行完成后,我们将会得到下面的三个表
其中
- zipkin_spans:存放基本工作单元,也就是一次链路调用的信息
- zipkin_dependencies:存放的依赖信息
- zipkin_annotations:用来记录请求特定事件相关信息(例如时间)
然后在按照下面的方式进行启动:
jar包启动
java -jar zipkin.jar --STORAGE_TYPE=mysql --MYSQL_DB=zipkin --MYSQL_USER=root --MYSQL_PASS=root --MYSQL_HOST=localhost --MYSQL_TCP_PORT=3306
docker启动
docker run -d -p 9411:9411 \
-e STORAGE_TYPE=mysql \
-e MYSQL_HOST=mysql-host \
-e MYSQL_PORT=3306 \
-e MYSQL_USER=root \
-e MYSQL_PASS=password \
-e MYSQL_DB=zipkin \
--name zipkin \
openzipkin/zipkin
参数:
- MYSQL_DB:MySQL数据库名,默认为 zipkin
- MYSQL_USER:用户名
- MYSQL_HOST:主机地址
- MYSQL_TCP_PORT:端口
- MYSQL_MAX_CONNECTIONS:最大连接数据,默认 10
- MYSQL_USE_SSL:是否使用ssl,需要 javax.net.ssl.trustStore 和 javax.net.ssl.trustStorePassword,默认 false
- MYSQL_JDBC_URL: 自己设置 JDBC 的 url
3. elasticsearch存储
Zipkin-Elasticsearch 存储数据库的官方文档:elasticsearch-storage
首先,你需要一个运行中的Elasticsearch集群,并且可以正常连接。
环境变量参数:
在启动Zipkin容器时,需要配置一些环境变量来指定Elasticsearch集群的连接信息。这些环境变量包括:
- STORAGE_TYPE: 存储类型,设为elasticsearch
- ES_HOSTS: Elasticsearch集群的主机地址,多个地址用逗号分隔
- ES_HTTP_LOGGING: (可选) Elasticsearch/OpenSearch API 请求,可选项BASIC, HEADERS, BODY
- ES_INDEX: (可选) Elasticsearch索引名称,默认为zipkin
- ES_TIMEOUT:连接Elasticsearch的超时时间,单位是毫秒;默认10000(10秒)
- ES_USERNAME:Elasticsearch的用户名
- ES_PASSWORD:Elasticsearch的密码
- ES_PIPELINE:指定Elasticsearch的Ingest Pipeline,用于预处理文档
jar包启动示例:
$ STORAGE_TYPE=elasticsearch ES_HOSTS=http://elasticsearch-host:9200 java -jar zipkin.jar
docker启动示例:
docker run -d -p 9411:9411 \
-e STORAGE_TYPE=elasticsearch \
-e ES_HOSTS=http://elasticsearch-host:9200 \
--name zipkin \
openzipkin/zipkin
Zipkin 支持的这几种存储方式中,内存显然是不适用于生产的,这一点开始也说了。而使用MySQL 的话,当数据量大时,查询较为缓慢,也不建议使用。Twitter 官方使用的是Cassandra作为Zipkin 的存储数据库,但国内大规模用Cassandra 的公司较少,而且Cassandra 相关文档也不多。
综上,故采用Elasticsearch 是个比较好的选择。
五、原理简介
Zipkin Server主要包括四个模块:
- Collector 接收或收集各应用传输的数据
- Storage 存储接受或收集过来的数据,当前支持Memory,MySQL,Cassandra,ElasticSearch等,默认存储在内存中
- API(Query) 负责查询Storage中存储的数据,提供简单的JSON API获取数据,主要提供给web UI使用
- Web 提供简单的web界面
Zipkin 分为两端,Zipkin 服务端和Zipkin 客户端,客户端也就是微服务的应用。客户端配置服务端的 URL 地址,一旦发生服务间的调用的时候,会被配置在微服务里面的 Sleuth 的监听器监听,并生成相应的 Trace 和 Span 信息发送给服务端。发送的方式主要有两种,一种是 HTTP 报文的方式,另一种是消息总线的方式如 RabbitMQ、kafka等。
链路追踪基本原理:
一个完整请求链路的追踪ID(traceid)用于查出本次请求调用的所有服务,每一次服务调用的跨度ID(spanid)用来记录调用顺序
上游服务parenetid用来记录调用的层级关系
调用时间timestamp,把请求发出、接收、处理的时间都记录下来,计算业务处理耗时和网络耗时,然后用可视化界面展示出来每个调用链路,性能,故障
还可以记录一些其他信息,比如发起调用服务名称、被调服务名称、返回结果、IP、调用服务的名称等,最后,我们再把相同spanid的信息合成一个大的span块,就完成了一个完整的调用链
注:图片来自chenchenchen
参考文章:
- zipkin官方文档
- SpringCloud 入门实战–Zipkin
- 分布式链路追踪原理详解及SkyWalking、Zipkin区别