微服务架构是一个分布式架构,微服务系统按业务划分服务单元,一个微服务系统往往有很多个服务单元。由于服务单元数量众多,业务的复杂性较高,如果出现了错误和异常,很难去定位。主要体现在一个请求可能需要调用很多个服务,而内部服务的调用复杂性决定了问题难以定位。所以在微服务架构中,必须实现分布式链路追踪,去跟进一个请求到底有哪些服务参与,参与的顺序又是怎样的,从而达到每个请求的步骤清晰可见,出了问题能够快速定位的目的。
在微服务系统中,一个来自用户的请求先到达前端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的起步依赖,代码如下:
org.springframework.cloud
spring-cloud-starter-sleuth
org.springframework.cloud
spring-cloud-starter-zipkin
在工程的配置文件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的依赖,依赖如下:
org.springframework.cloud
spring-cloud-stream-binder-rabbit
在配置文件上需要配置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...