1、快速入门
1)、准备工作
在引入Sleuth之前,先构建一些基础的设施和应用
- 服务注册中心:eureka-server
- 微服务应用:trace-1,实现一个REST接口/trace-1,调用该接口后将触发对trace-2应用的调用
trace-1:
@RestController
@EnableDiscoveryClient
@SpringBootApplication
public class Trace1Application {
private final Logger logger = LoggerFactory.getLogger(Trace1Application.class);
@Bean
@LoadBalanced
RestTemplate restTemplate() {
return new RestTemplate();
}
@GetMapping("/trace-1")
public String trace() {
logger.info("==call trace-1==");
return restTemplate().getForEntity("http://trace-2/trace-2", String.class).getBody();
}
public static void main(String[] args) {
SpringApplication.run(Trace1Application.class, args);
}
}
trace-2:
@RestController
@EnableDiscoveryClient
@SpringBootApplication
public class Trace2Application {
private final Logger logger = LoggerFactory.getLogger(Trace2Application.class);
@GetMapping("/trace-2")
public String trace() {
logger.info("==call trace-2==");
return "Trace";
}
public static void main(String[] args) {
SpringApplication.run(Trace2Application.class, args);
}
}
访问trace-1的接口,控制台分别获得下面的输出:
trace-1:
2019-07-21 09:31:11.499 INFO 308 --- [nio-9101-exec-1] com.hand.trace1.Trace1Application : ==call trace-1==
trace-2:
2019-07-21 09:31:12.501 INFO 5608 --- [nio-9102-exec-2] com.hand.trace2.Trace2Application : ==call trace-2==
2)、实现跟踪
通过SpringCloud Sleuth的封装,为应用增加服务跟踪能力只需在trace-1和trace-2的pom.xml依赖管理中增加spring-cloud-starter-sleuth的依赖即可
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
重启trace-1和trace-2,再对trace-1的接口发送请求,控制台分别获得下面的输出:
trace-1:
2019-07-21 09:41:46.824 INFO [trace-1,b5f20eebce715f69,b5f20eebce715f69,false] 16804 --- [nio-9101-exec-1] com.hand.trace1.Trace1Application : ==call trace-1==
trace-2:
2019-07-21 09:41:47.625 INFO [trace-2,b5f20eebce715f69,2c0ca9cbd7ca5843,false] 4088 --- [nio-9102-exec-1] com.hand.trace2.Trace2Application : ==call trace-2==
从上面的控制台输出内容中,可以看到多了一些例如[trace-1,b5f20eebce715f69,b5f20eebce715f69,false]
的日志信息,而这些元素正是实现分布式服务跟踪的重要组成部分,每个值的含义如下:
- 第一个值:它记录了应用的名称,也就是
spring.application.name
参数配置的值 - 第二个值:SpringCloud Sleuth生成的一个ID,称为Trace ID,它用来标记一条请求链路。一条请求链路中包含一个Trace ID,多个Span ID
- 第三个值:SpringCloud Sleuth生成的另外一个ID,称为Span ID,它表示一个基本的工作单元,比如发送一个HTTP请求
- 第四个值:表示是否要将该信息输出到Zipkin等服务中来收集和展示
Trace ID和Span ID是SpringCloud Sleuth实现分布式服务跟踪的核心。在一次服务请求链路的调用过程中,会保持并传递同一个Trace ID,从而将整个分布于不同微服务进程中的请求跟踪信息串联起来
2、跟踪原理
为了实现请求跟踪,当请求发送到分布式系统的入口端点时,只需要服务跟踪框架为该请求创建一个唯一的跟踪标识,同时在分布式系统内部流转的时候,框架始终保持该唯一标识,直到返回给请求方位置,这个唯一标识就是Trace ID。通过Trace ID的记录,就能将所有请求过程的日志关联起来
为了统计各处理单元的时间延迟,当请求到达各个服务组件时,或是处理逻辑到达某个状态时,也通过一个唯一标识来标记它的开始、具体过程以及结束,该标识就是Span ID。对于每个Span来说,它必须有开始和结束两个节点,通过记录开始Span和结束Span的时间戳,就能统计出该Span的时间延迟,除了时间戳之外,它还可以包含一些其他元数据,比如事件名称、请求信息等
通过在工程中引入spring-cloud-starter-sleuth的依赖之后,它会自动为当前应用构建起各通信通道的跟踪机制,比如:
- 通过诸如RabbitMQ、Kafka(或者其他任何SpringCloud Stream绑定器实现的消息中间件)传递的请求
- 通过Zuul代理传递的请求
- 通过RestTemplate发起的请求
在快速入门示例中,由于trace-1对trace-2发送的请求是通过RestTemplate实现的,所以spring-cloud-starter-sleuth组件会对该请求进行处理。在发送到trace-2之前,Sleuth会在该请求的Header中增加实现跟踪需要的重要信息,主要有下面几个(关于头信息的定义可以通过查看brave.propagation.B3Propagation的源码获取):
- X-B3-TraceId:一条请求链路(Trace)的唯一标识
- X-B3-SpanId:一条工作单元(Span)的唯一标识
- X-B3-ParentSpanId:标识当前工作单元所属的上一个工作单元,第一个工作单元的该值为空
- X-B3-Sampled:是否被抽样输出的标志,1表示需要被输出,0表示不需要被输出
3、抽样收集
分析系统在收集跟踪信息的时候,需要收集多少跟踪信息才合适?
理论上来说,我们收集的跟踪信息越多就可以越好地反映出系统的实际运行情况,并给出更精准的预警和分析。但是在高并发的分布式系统运行时,大量的请求调用会产生海量的跟踪日志信息,如果收集过多的跟踪信息将会对整个分布式系统的性能造成一定的影响,同时保存大量的日志信息也需要不少的存储开销。所以,在Sleuth中采用了抽样收集的方式来为跟踪信息打上收集标记,也就是X-B3-Sampled的值,它代表了该信息是否要被后续的跟踪信息收集器获取和存储
Sleuth中的抽样收集策略是通过Sampler抽象类实现的
public abstract class Sampler {
public abstract boolean isSampled(long traceId);
通过实现isSampled方法,SpringCloud Sleuth会在产生跟踪信息的时候调用它来为跟踪信息生成是否要被收集的标志。即使isSampled返回了false,它仅代表该跟踪信息不被输出到后续对接的远程分析系统(比如Zipkin),对于请求的跟踪活动依然会进行,所以在日志中还是能看到收集标识为false的记录
默认情况下,Sleuth会使用ProbabilityBasedSampler实现的抽样策略,以请求百分比的方式配置和收集跟踪信息。可以通过application.properties中配置下面的参数对其百分比值进行设置,它的默认值为0.1,代表收集10%的请求跟踪信息
spring.sleuth.sampler.probability=0.1
在开发调试期间,通常会收集全部跟踪信息并输出到远程仓库,可以将其值设置为1,或者也可以通过创建一个isSampled方法始终返回true的Sampler来覆盖默认的ProbabilityBasedSampler策略,比如:
@Bean
public Sampler defaultSampler() {
return new Sampler() {
@Override
public boolean isSampled(long traceId) {
return true;
}
};
}
4、与Zipkin整合
Zipkin是Twitter的一个开源项目,它基于Google Dapper实现,我们可以使用它来收集各个服务器上请求链路的跟踪数据,并通过它提供的REST API接口来辅助查询跟踪数据以实现对分布式系统的监控程序,从而及时发现系统中出现的延迟升高问题并找出系统性能瓶颈的根源。除了面向开发的API接口之外,它还提供了方便的UI组件来帮助我们直观地搜索跟踪信息和分析请求链路明细,比如可以查询某段时间内各用户请求的处理时间等
1)、架构概述
Zipkin主要由4个核心组件构成:
- Collector:收集器组件,它主要处理从外部系统发送过来的跟踪信息,将这些信息转换为Zipkin内部处理的Span格式,以支持后续的存储、分析、展示等功能
- Storage:存储组件,它主要处理收集器接收到的跟踪信息,默认会将这些信息存储在内存中。也可以修改此存储策略,通过使用其他存储组件将跟踪信息存储到数据库中
- RESTful API:API组件,它主要用来提供外部访问接口。比如给客户端展示跟踪信息,或是外接系统访问以实现监控等
- Web UI:UI组件,基于API组件实现的上层应用。通过UI组件,用户可以方便而又直观地查询和分析跟踪信息
2)、示例流程
这是一个示例序列的HTTP跟踪,其中用户代码调用/foo接口。这导致单个跨度,在用户代码收到HTTP响应后异步发送到Zipkin
┌─────────────┐ ┌───────────────────────┐ ┌─────────────┐ ┌──────────────────┐
│ User Code │ │ Trace Instrumentation │ │ Http Client │ │ Zipkin Collector │
└─────────────┘ └───────────────────────┘ └─────────────┘ └──────────────────┘
│ │ │ │
┌─────────┐
│ ──┤GET /foo ├─▶ │ ────┐ │ │
└─────────┘ │ record tags
│ │ ◀───┘ │ │
────┐
│ │ │ add trace headers │ │
◀───┘
│ │ ────┐ │ │
│ record timestamp
│ │ ◀───┘ │ │
┌─────────────────┐
│ │ ──┤GET /foo ├─▶ │ │
│X-B3-TraceId: aa │ ────┐
│ │ │X-B3-SpanId: 6b │ │ │ │
└─────────────────┘ │ invoke
│ │ │ │ request │
│
│ │ │ │ │
┌────────┐ ◀───┘
│ │ ◀─────┤200 OK ├─────── │ │
────┐ └────────┘
│ │ │ record duration │ │
┌────────┐ ◀───┘
│ ◀──┤200 OK ├── │ │ │
└────────┘ ┌────────────────────────────────┐
│ │ ──┤ asynchronously report span ├────▶ │
│ │
│{ │
│ "traceId": "aa", │
│ "id": "6b", │
│ "name": "get", │
│ "timestamp": 1483945573944000,│
│ "duration": 386000, │
│ "annotations": [ │
│--snip-- │
└────────────────────────────────┘
跟踪检测报告以异步方式发送,以防止与跟踪系统相关的延迟或故障延迟中断用户代码
3)、HTTP收集
1)Zipkin的服务端
在使用Spring Boot 2.x版本后,官方就不推荐自行定制编译Zipkin的服务端了,反而是直接提供了编译好的jar包来给我们使用,详细请看https://github.com/openzipkin/zipkin/issues/1962
官方提供了一键脚本:
[root@aliyun zipkin]# curl -sSL https://zipkin.io/quickstart.sh | bash -s
[root@aliyun zipkin]# nohup java -jar zipkin.jar &
访问http://IP:9411/,看到如下界面:
2)为应用引入和配置Zipkin服务
在完成了Zipkin的服务端搭建之后,需要对应用做一些配置,以实现将跟踪信息输出到Zipkin Server。修改之前实现的trace-1和trace-2
在pom.xml中引入spring-cloud-starter-zipkin的依赖,由于其中已经包含了spring-cloud-starter-sleuth的依赖,所以将之前引入的spring-cloud-starter-sleuth依赖去掉
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
在application.properties中增加Zipkin Server的配置信息
#收集全部的请求跟踪信息
spring.sleuth.sampler.probability=1
spring.zipkin.base-url=http://IP:9411/
3)测试
向trace-1的接口发送几次请求,在Zipkin Server的管理页面中选择合适的条件,单击Find Traces按钮,就可以查询出刚才在日志中出现的跟踪信息了
单击下方trace-1端点的跟踪信息,还可以得到Sleuth跟踪到的详细信息
点击导航栏中的依赖菜单,还可以查看Zipkin Server根据跟踪信息分析生成的系统请求链路依赖关系图,如下图所示
4)、消息中间件收集
SpringCloud Sleuth在整合Zipkin时,不仅实现了以HTTP的方式收集跟踪信息,还实现了通过消息中间件来对跟踪信息进行异步收集的封装
1)Zipkin的服务端
指定RabbitMQ相关参数启动Zipkin的服务端
[root@aliyun zipkin]# nohup java -jar zipkin.jar --zipkin.collector.rabbitmq.addresses=IP:5672 --zipkin.collector.rabbitmq.virtual-host=/ --zipkin.collector.rabbitmq.queue=zipkin --zipkin.collector.rabbitmq.username=guest --zipkin.collector.rabbitmq.password=guest &
或者通过shell脚本
#!/bin/bash
export RABBIT_ADDRESSES=IP:5672
export RABBIT_VIRTUAL_HOST=/
export RABBIT_QUEUE=zipkin
export RABBIT_USER=guest
export RABBIT_PASSWORD=guest
java -jar zipkin.jar
可配置的环境变量如下表所示:
属性 | 环境变量 | 描述 |
---|---|---|
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 |
zipkin.collector.rabbitmq.uri | RABBIT_URI | 符合RabbitMQ URI规范的 URI,例如amqp://user:pass@host:10000/vhost |
如果设置了URI,则以下属性将被忽略
属性 | 环境变量 | 描述 |
---|---|---|
zipkin.collector.rabbitmq.addresses | RABBIT_ADDRESSES | 用逗号分隔的RabbitMQ地址列表,例如localhost:5672,localhost:5673 |
zipkin.collector.rabbitmq.username | RABBIT_USER | 连接到RabbitMQ时使用的用户名,默认为guest |
zipkin.collector.rabbitmq.password | RABBIT_PASSWORD | 连接到RabbitMQ时使用的密码,默认为guest |
zipkin.collector.rabbitmq.virtual-host | RABBIT_VIRTUAL_HOST | 使用的RabbitMQ virtual host,默认为/ |
zipkin.collector.rabbitmq.use-ssl | RABBIT_USE_SSL | 设置为true则用SSL的方式与RabbitMQ建立链接 |
启动成功后,RabbitMQ Admin中多了一个名为zipkin的队列
zipkin.jar的yml配置文件内容可以从GitHub上查看:https://github.com/openzipkin/zipkin/blob/master/zipkin-server/src/main/resources/zipkin-server-shared.yml
2)修改客户端trace-1和trace-2
引入spring-cloud-stream-binder-rabbit依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-rabbit</artifactId>
</dependency>
application.properties中增加消息中间件相关配置:
spring.sleuth.sampler.probability=1
spring.zipkin.sender.type=rabbit
spring.rabbitmq.addresses=IP:5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.virtual-host=/
spring.zipkin.rabbitmq.queue=zipkin
3)测试
向trace-1的接口发送几次请求,在Zipkin Server的管理页面查询出刚才在日志中出现的跟踪信息了
5)、Zipkin中关于跟踪信息的基础概念
1)Span:它代表了一个基础的工作单元,一次完整的HTTP请求过程在客户端和服务端都会产生多个不同的事件状态,对于同一个请求来说,它们属于同一个工作单元,所以同一个HTTP请求过程中的4个Annotation同属于一个Span。每一个不同的工作单元都通过一个64位的ID来做唯一标识,称为Span ID。另外,在工作单元中还存储了一个用来串联其他工作单元的ID,它也通过一个64位的ID来做唯一标识,称为Trace ID。在同一条请求链路中的不同工作单元都会有不同的Span ID,但是它们的Trace ID是相同的,所以通过Trace ID可以将一次请求中依赖的所有依赖请求串联起来形成请求链路。除了这两个核心的ID之外,Span中还存储了一些其他信息,比如,描述信息、事件时间戳、Annotation的键值对属性、上一级工作单元的Span ID等
2)Trace:它是由一系列具有相同Trace ID的Span串联形成的一个树状结构。在复杂的分布式系统中,每一个外部请求通常都会产生一个复杂的树状结构的Tree
3)Annotation:它用来及时地记录一个事件的存在。对于一个HTTP请求来说,在Sleuth中定义了下面4个核心Annotation来标识一个请求的开始和结束
- cs(Client Send):该Annotation用来记录客户端发起了一个请求,同时它也标识了这个HTTP请求的开始
- sr(Server Received):该Annotation用来记录服务端接收到了请求,并准备开始处理它。通过计算sr与cs两个Annotation的时间戳之差,可以得到HTTP请求的网络
- ss(Server Send):该Annotation用来记录服务端处理完请求后准备发送请求响应信息。通过计算ss与sr两个Annotation的之间戳之差,可以得到当前服务端处理请求的时间消耗
- cr(Client Received):该Annotation用来记录客户端收到服务端的回复,同时它也标识了这个HTTP请求的结束。通过计算cr与cs两个Annotation的时间戳之差,可以得到该HTTP请求从客户端发起到接收服务端响应的总时间消耗
4)Binary Annotation:它用来对跟踪信息添加一些额外的补充说明,一般以键值对的方式出现
6)、数据存储
默认情况下,Zipkin Server会将跟踪信息存储在内存中,每次重启Zipkin Server都会使之前收集的跟踪信息丢失,并且当有大量跟踪信息时,内存存储也会造成性能瓶颈,所以通常我们都需要将跟踪信息存储到外部组件中,如MySQL
1)创建数据库、表信息
建表的脚本文件mysql.sql链接如下:
https://github.com/openzipkin/zipkin/blob/master/zipkin-storage/mysql-v1/src/main/resources/mysql.sql
先创建一个名为zipkin的库,然后执行SQL语句,这里会生成三张表
2)Zipkin的服务端
指定MySQL相关参数启动Zipkin的服务端,不需要对客户端进行修改
[root@aliyun zipkin]# nohup java -jar zipkin.jar --zipkin.collector.rabbitmq.addresses=IP:5672 --zipkin.collector.rabbitmq.virtual-host=/ --zipkin.collector.rabbitmq.queue=zipkin --zipkin.collector.rabbitmq.username=guest --zipkin.collector.rabbitmq.password=guest --zipkin.storage.type=mysql --zipkin.storage.mysql.host=IP --zipkin.storage.mysql.port=3306 --zipkin.storage.mysql.username=root --zipkin.storage.mysql.password=123456 --zipkin.storage.mysql.db=zipkin &
3)测试
向trace-1的接口发送几次请求,查看MySQL中的两张表,可以得到类似下面的数据信息
- zipkin_spans表:
- zipkin_annotations表:
在zipkin_spans表中可以看到一次请求调用链路的跟踪信息产生了2条span数据,也就是说,在入zipkin_spans表的时候,已经对收集的span信息进行了合并,所以在查询详细信息时,不需要每次都来合并计算span。而在zipkin_annotations表中,通过trace_id字段可以关联到每个具体工作单元的详细信息,同时根据endpoint_service_name和span_id字段还可以计算出一次请求调用链路中总共接收到的span数量