共计 12982 个字符,预计需要花费 33 分钟才能阅读完成。
微服务架构是一个分布式架构,微服务系统按业务划分服务单元,一个微服务系统往往有很多个服务单元。由于服务单元数量众多,业务的复杂性较高,如果出现了错误和异常,很难去定位。主要体现在一个请求可能需要调用很多个服务,而内部服务的调用复杂性决定了问题难以定位。所以在微服务架构中,必须实现分布式链路追踪,去跟进一个请求到底有哪些服务参与,参与的顺序又是怎样的,从而达到每个请求的步骤清晰可见,出了问题能够快速定位的目的。
在微服务系统中,一个来自用户的请求先到达前端 A(如前端界面),然后通过远程调用,到达系统的中间件 B、C(如负载均衡、网关等),最后到达后端服务 D、E,后端经过一系列的业务逻辑计算,最后将数据返回给用户。对于这样一个请求,经历了这么多个服务,怎么样将它的请求过程用数据记录下来呢?这就需要用到服务链路追踪。
Spring Cloud Sleuth
Spring Cloud Sleuth 为服务之间调用提供链路追踪。通过 Sleuth 可以很清楚的了解到一个服务请求经过了哪些服务,每个服务处理花费了多长。从而让我们可以很方便的理清各微服务间的调用关系。此外 Sleuth 可以帮助我们:
耗时分析: 通过 Sleuth 可以很方便的了解到每个采样请求的耗时,从而分析出哪些服务调用比较耗时;
可视化错误: 对于程序未捕捉的异常,可以通过集成 Zipkin 服务界面上看到;
链路优化: 对于调用比较频繁的服务,可以针对这些服务实施一些优化措施。
Google 开源了 Dapper 链路追踪组件,并在 2010 年发表了论文《Dapper, a Large-Scale Distributed Systems Tracing Infrastructure》,这篇论文是业内实现链路追踪的标杆和理论基础,具有很高的参考价值。
Spring Cloud Sleuth 采用了 Google 的开源项目 Dapper 的专业术语。
Span:基本工作单元,发送一个远程调度任务就会产生一个 Span,Span 是用一个 64 位 ID 唯一标识的,Trace 是用另一个 64 位 ID 唯一标识的。Span 还包含了其他的信息,例如摘要、时间戳事件、Span 的 ID 以及进程 ID。
Trace:由一系列 Span 组成的,呈树状结构。请求一个微服务系统的 API 接口,这个 API 接口需要调用多个微服务单元,调用每个微服务单元都会产生一个新的 Span,所有由这个请求产生的 Span 组成了这个 Trace。
Annotation:用于记录一个事件,一些核心注解用于定义一个请求的开始和结束,这些注解如下。
cs-Client Sent:客户端发送一个请求,这个注解描述了 Span 的开始。
sr-Server Received:服务端获得请求并准备开始处理它,如果将其 sr 减去 cs 时间戳,便可得到网络传输的时间。
ss-Server Sent:服务端发送响应,该注解表明请求处理的完成(当请求返回客户端),用 ss 的时间戳减去 sr 时间戳,便可以得到服务器请求的时间。
cr-Client Received:客户端接收响应,此时 Span 结束,如果 cr 的时间戳减去 cs 时间戳,便可以得到整个请求所消耗的时间。
Spring Cloud Sleuth 也为我们提供了一套完整的链路解决方案,Spring Cloud Sleuth 可以结合 Zipkin,将信息发送到 Zipkin,利用 Zipkin 的存储来存储链路信息,利用 Zipkin UI 来展示数据。
Zipkin
Zipkin 是一种分布式链路追踪系统。它有助于收集解决微服务架构中的延迟问题所需的时序数据。它管理这些数据的收集和查找。Zipkin 的设计基于 Google Dapper 论文。
跟踪器存在于应用程序中,记录请求调用的时间和元数据。跟踪器使用库,它们的使用对用户是无感知的。例如,Web 服务器会在收到请求时和发送响应时会记录相应的时间和一些元数据。一次完整链路请求所收集的数据被称为 Span。
我们可以使用它来收集各个服务器上请求链路的跟踪数据,并通过它提供的 REST API 接口来辅助我们查询跟踪数据以实现对分布式系统的监控程序,从而及时地发现系统中出现的延迟升高问题并找出系统性能瓶颈的根源。除了面向开发的 API 接口之外,它也提供了方便的 UI 组件来帮助我们直观的搜索跟踪信息和分析请求链路明细,比如:可以查询某段时间内各用户请求的处理时间等。Zipkin 提供了可插拔数据存储方式:In-Memory、MySql、Cassandra 以及 Elasticsearch。接下来的测试为方便直接采用 In-Memory 方式进行存储,生产推荐 Elasticsearch.
上图展示了 Zipkin 的基础架构,它主要由 4 个核心组件构成:
Collector:收集器组件,它主要用于处理从外部系统发送过来的跟踪信息,将这些信息转换为 Zipkin 内部处理的 Span 格式,以支持后续的存储、分析、展示等功能。
Storage:存储组件,它主要对处理收集器接收到的跟踪信息,默认会将这些信息存储在内存中,我们也可以修改此存储策略,通过使用其他存储组件将跟踪信息存储到数据库中。
RESTful API:API 组件,它主要用来提供外部访问接口。比如给客户端展示跟踪信息,或是外接系统访问以实现监控等。
Web UI:UI 组件,基于 API 组件实现的上层应用。通过 UI 组件用户可以方便而有直观地查询和分析跟踪信息。
案例实战
在本案例一共有三个应用,分别为注册中心,eureka-server、eureka-client、eureka-client-feign,三个应用的基本信息如下:
应用名
端口
作用
eureka-server
8761
注册中心
eureka-client
8763
服务提供者
eureka-client-feign
8765
服务消费者
其中 eureka-server 应用为注册中心,其他两个应用向它注册。eureka-client 为服务提供者,提供了一个 RESTAPI,eureka-client-feign 为服务消费者,通过 Feign Client 向服务提供者消费服务。
在之前的文章已经讲述了如何如何搭建服务注册中心,在这里就省略这一部分内容。服务提供者提供一个 REST 接口,服务消费者通过 FeignClient 消费服务。
服务提供者
eureka-client 服务提供者,对外提供一个 RESTAPI,并向服务注册中心注册,这部分内容,不再讲述,见源码。需要在工程的 pom 文件加上 sleuth 的起步依赖和 zipkin 的起步依赖,代码如下:
<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>
在工程的配置文件 application.yml 需要做以下的配置:
spring:
sleuth:
web:
client:
enabled: true
sampler:
probability: 1.0 # 将采样比例设置为 1.0,也就是全部都需要。默认是 0.1
zipkin:
base-url: http://localhost:9411/ # 指定了 Zipkin 服务器的地址
其中 spring.sleuth.web.client.enable 为 true 设置的是 web 开启 sleuth 功能;spring.sleuth.sampler.probability 可以设置为小数,最大值为 1.0,当设置为 1.0 时就是链路数据 100% 收集到 zipkin-server,当设置为 0.1 时,即 10% 概率收集链路数据;spring.zipkin.base-url 设置 zipkin-server 的地址。
对外提供一个 Api,代码如下:
@RestController
public class HiController {
@Value(“${server.port}”)
String port;
@GetMapping(“/hi”)
public String home(@RequestParam String name) {
return “hi “+name+”,i am from port:” +port;
}
}
服务消费者
服务消费者通过 FeignClient 消费服务提供者提供的服务。同服务提供者一样,需要在工程的 pom 文件加上 sleuth 的起步依赖和 zipkin 的起步依赖,另外也需要在配置文件 application.yml 做相关的配置,具体同服务提供者。
服务消费者通过 feignClient 进行服务消费,feignclient 代码如下:
@FeignClient(value = “eureka-client”,configuration = FeignConfig.class)
public interface EurekaClientFeign {
@GetMapping(value = “/hi”)
String sayHiFromClientEureka(@RequestParam(value = “name”) String name);
}
servcie 层代码如下:
@Service
public class HiService {
@Autowired
EurekaClientFeign eurekaClientFeign;
public String sayHi(String name){
return eurekaClientFeign.sayHiFromClientEureka(name);
}
}
controller 代码如下:
@RestController
public class HiController {
@Autowired
HiService hiService;
@GetMapping(“/hi”)
public String sayHi(@RequestParam( defaultValue = “forezp”,required = false)String name){
return hiService.sayHi(name);
}
上面的代码对外暴露一个 API,通过 FeignClient 的方式调用 eureka-client 的服务。
zipkin-server
在 Spring Cloud D 版本,zipkin-server 通过引入依赖的方式构建工程,自从 E 版本之后,这一方式改变了,采用官方的 jar 形式启动,所以需要通过下载官方的 jar 来启动,也通过以下命令一键启动:
curl -sSL https://zipkin.io/quickstart.sh | bash -s
java -jar zipkin.jar
上面的第一行命令会从 zipkin 官网下载官方的 jar 包。如果是 window 系统,建议使用 gitbash 执行上面的命令。
如果用 Docker 的话,使用以下命令:
docker run -d -p 9411:9411 openzipkin/zipkin
通过 java -jar zipkin.jar 的方式启动之后,在浏览器上访问 lcoalhost:9411,显示的界面如下:
链路数据验证
依次启动 eureka-server,eureka-client,eureka-client-feign 的三个应用,等所有应用启动完成后,在浏览器上访问 http://localhost:8765/hi(如果报错,是服务与发现需要一定的时间,耐心等待几十秒),访问成功后,再次在浏览器上访问 zipkin-server 的页面,显示如下:
从上图可以看出每次请求所消耗的时间,以及一些 span 的信息。
从上图可以看出具体的服务依赖关系,eureka-feign-client 依赖了 eureka-client。
使用 rabbitmq 进行链路数据收集
在上面的案例中使用的 http 请求的方式将链路数据发送给 zipkin-server,其实还可以使用 rabbitmq 的方式进行服务的消费。使用 rabbitmq 需要安装 rabbitmq 程序,下载地址 http://www.rabbitmq.com/。
下载完成后,需要 eureka-client 和 eureka-client-feign 的起步依赖加上 rabbitmq 的依赖,依赖如下:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-rabbit</artifactId>
</dependency>
在配置文件上需要配置 rabbitmq 的配置,配置信息如下:
spring:
rabbitmq:
host: localhost
username: guest
password: guest
port: 5672
另外需要把 spring.zipkin.base-url 去掉。
在上面 2 个工程中,rabbitmq 通过发送链路数据,那么 zipkin-server 是怎么样知道 rabbitmq 的地址呢,怎么监听收到的链路数据呢?这需要在程序启动的时候,通过环境变量的形式到环境中,然后 zikin-server 从环境变量中读取。可配置的属性如下:
属性
环境变量
描述
zipkin.collector.rabbitmq.addresses
RABBIT_ADDRESSES
用逗号分隔的 RabbitMQ 地址列表,例如 localhost:5672,localhost:5673
zipkin.collector.rabbitmq.password
RABBIT_PASSWORD
连接到 RabbitMQ 时使用的密码,默认为 guest
zipkin.collector.rabbitmq.username
RABBIT_USER
连接到 RabbitMQ 时使用的用户名,默认为 guest
zipkin.collector.rabbitmq.virtual-host
RABBIT_VIRTUAL_HOST
使用的 RabbitMQ virtual host,默认为 /
zipkin.collector.rabbitmq.use-ssl
RABBIT_USE_SSL
设置为 true 则用 SSL 的方式与 RabbitMQ 建立链接
zipkin.collector.rabbitmq.concurrency
RABBIT_CONCURRENCY
并发消费者数量,默认为 1
zipkin.collector.rabbitmq.connection-timeout
RABBIT_CONNECTION_TIMEOUT
建立连接时的超时时间,默认为 60000 毫秒,即 1 分钟
zipkin.collector.rabbitmq.queue
RABBIT_QUEUE
从中获取 span 信息的队列,默认为 zipkin
比如,通过以下命令启动:
RABBIT_ADDRESSES=localhost java -jar zipkin.jar
上面的命令等同于一下的命令:
java -jar zipkin.jar –zipkin.collector.rabbitmq.addressed=localhost
用上面的 2 条命令中的任何一种方式重新启动 zipkin-server 程序,并重新启动 eureka-client、eureka-server、eureka-client-feign,动完成后在浏览器上访问 http://localhost:8765/hi,再访问 http://localhost:9411/zipkin/,就可以看到通过 Http 方式发送链路数据一样的接口。
自定义 Tag
在页面上可以查看每个请求的 traceId,每个 trace 又包含若干的 span,每个 span 又包含了很多的 tag,自定义 tag 可以通过 Tracer 这个类来自定义。
@Autowired
Tracer tracer;
@GetMapping(“/hi”)
public String home(@RequestParam String name) {
tracer.currentSpan().tag(“name”,”forezp”);
return “hi “+name+”,i am from port:” +port;
}
将链路数据存储在 Mysql 数据库中
上面的例子是将链路数据存在内存中,只要 zipkin-server 重启之后,之前的链路数据全部查找不到了,zipkin 是支持将链路数据存储在 mysql、cassandra、elasticsearch 中的。现在讲解如何将链路数据存储在 Mysql 数据库中。首先需要初始化 zikin 存储在 Mysql 的数据的 scheme,可以在这里查看 https://github.com/openzipkin…,具体如下:
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,
`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`) 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 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
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;
ALTER TABLE zipkin_dependencies ADD UNIQUE KEY(`day`, `parent`, `child`);
在数据库中初始化上面的脚本之后,需要做的就是 zipkin-server 如何连接数据库。zipkin 如何连数据库同连接 rabbitmq 一样。zipkin 连接数据库的属性所对应的环境变量如下:
属性
环境变量
描述
zipkin.torage.type
STORAGE_TYPE
默认的为 mem,即为内存,其他可支持的为 cassandra、cassandra3、elasticsearch、mysql
zipkin.torage.mysql.host
MYSQL_HOST
数据库的 host,默认 localhost
zipkin.torage.mysql.port
MYSQL_TCP_PORT
数据库的端口,默认 3306
zipkin.torage.mysql.username
MYSQL_USER
连接数据库的用户名,默认为空
zipkin.torage.mysql.password
MYSQL_PASS
连接数据库的密码,默认为空
zipkin.torage.mysql.db
MYSQL_DB
zipkin 使用的数据库名,默认是 zipkin
zipkin.torage.mysql.max-active
MYSQL_MAX_CONNECTIONS
最大连接数,默认是 10
STORAGE_TYPE=mysql MYSQL_HOST=localhost MYSQL_TCP_PORT=3306 MYSQL_USER=root MYSQL_PASS=123456 MYSQL_DB=zipkin java -jar zipkin.jar
等同于以下的命令
java -jar zipkin.jar –zipkin.torage.type=mysql –zipkin.torage.mysql.host=localhost –zipkin.torage.mysql.port=3306 –zipkin.torage.mysql.username=root –zipkin.torage.mysql.password=123456
使用上面的命令启动 zipkin.jar 工程,然后再浏览数上访问 http://localhost:8765/hi,再访问 http://localhost:9411/zipkin/,可以看到链路数据。这时去数据库查看数据,也是可以看到存储在数据库的链路数据,如下:
这时重启应用 zipkin.jar,再次在浏览器上访问 http://localhost:9411/zipkin/,仍然可以得到之前的结果,证明链路数据存储在数据库中,而不是内存中。
将链路数据存在在 Elasticsearch 中
zipkin-server 支持将链路数据存储在 ElasticSearch 中。读者需要自行安装 ElasticSearch 和 Kibana,下载地址为 https://www. elastic.co/products/elasticsearch。安装完成后启动,其中 ElasticSearch 的默认端口号为 9200,Kibana 的默认端口号为 5601。
同理,zipkin 连接 elasticsearch 也是从环境变量中读取的,elasticsearch 相关的环境变量和对应的属性如下:
属性
环境变量
描述
zipkin.torage.elasticsearch.hosts
ES_HOSTS
ES_HOSTS,默认为空
zipkin.torage.elasticsearch.pipeline
ES_PIPELINE
ES_PIPELINE,默认为空
zipkin.torage.elasticsearch.max-requests
ES_MAX_REQUESTS
ES_MAX_REQUESTS,默认为 64
zipkin.torage.elasticsearch.timeout
ES_TIMEOUT
ES_TIMEOUT,默认为 10s
zipkin.torage.elasticsearch.index
ES_INDEX
ES_INDEX,默认是 zipkin
zipkin.torage.elasticsearch.date-separator
ES_DATE_SEPARATOR
ES_DATE_SEPARATOR,默认为“-”
zipkin.torage.elasticsearch.index-shards
ES_INDEX_SHARDS
ES_INDEX_SHARDS,默认是 5
zipkin.torage.elasticsearch.index-replicas
ES_INDEX_REPLICAS
ES_INDEX_REPLICAS,默认是 1
zipkin.torage.elasticsearch.username
ES_USERNAME
ES 的用户名,默认为空
zipkin.torage.elasticsearch.password
ES_PASSWORD
ES 的密码,默认是为空
采用以下命令启动 zipkin-server:
STORAGE_TYPE=elasticsearch ES_HOSTS=http://localhost:9200 ES_INDEX=zipkin java -jar zipkin.jar
java -jar zipkin.jar –STORAGE_TYPE=elasticsearch –ES_HOSTS=http://localhost:9200 –ES_INDEX=zipkin
java -jar zipkin.jar –STORAGE_TYPE=elasticsearch –ES_HOSTS=http://localhost:9200 –ES_INDEX=zipkin
java -jar zipkin.jar –zipkin.torage.type=elasticsearch –zipkin.torage.elasticsearch.hosts=http://localhost:9200 –zipkin.torage.elasticsearch.index=zipkin
启动完成后,然后在浏览数上访问 http://localhost:8765/hi,再访问 http://localhost:9411/zipkin/,可以看到链路数据。这时链路数据存储在 ElasticSearch。
在 zipkin 上展示链路数据
链路数据存储在 ElasticSearch 中,ElasticSearch 可以和 Kibana 结合,将链路数据展示在 Kibana 上。安装完成 Kibana 后启动,Kibana 默认会向本地端口为 9200 的 ElasticSearch 读取数据。Kibana 默认的端口为 5601,访问 Kibana 的主页 http://localhost:5601,其界面如下图所示。
在上图的界面中,单击“Management”按钮,然后单击“Add New”,添加一个 index。我们将在上节 ElasticSearch 中写入链路数据的 index 配置为“zipkin”,那么在界面填写为“zipkin-*”,单击“Create”按钮,界面如下图所示:
创建完成 index 后,单击“Discover”,就可以在界面上展示链路数据了,展示界面如下图所示。
参考资料
https://zipkin.io/
https://github.com/spring-clo…
https://cloud.spring.io/sprin…
https://github.com/openzipkin…
https://github.com/openzipkin…
https://windmt.com/2018/04/24…
https://segmentfault.com/a/11…
elatstic 版本为 2.6.x,下载地址:https://www.elastic.co/downlo…
http://www.cnblogs.com/JreeyQ…