分布式服务跟踪: Spring Cloud Sleuth
什么是Sprng Cloud Sleuth
是对一个请求所经过的整个服务链的跟踪,帮助我们快速发现错误根源以及监控分析每条请求链路上的性能瓶颈。
随着业务不断地发展,系统拆分导致系统调用链路愈发复杂一个前端请求可能最终需要调用很多次后端服务才能完成,当整个请求变慢或不可用时,我们是无法得知该请求是由某个或某些后端服务引起的,这时就需要解决如何快读定位服务故障点,以对症下药。于是就有了分布式系统调用跟踪的诞生。
Tips:业界分布式服务跟踪的理论基础主要来自于 Google 的一篇论文《Dapper, a Large-Scale Distributed Systems Tracing Infrastructure》,使用最为广泛的开源实现是 Twitter 的 Zipkin
,为了实现平台无关、厂商无关的分布式服务跟踪,CNCF 发布了布式服务跟踪标准 Open Tracing
。国内,淘宝的“鹰眼”、京东的“Hydra
”、大众点评的“CAT
”、新浪的“Watchman
”、唯品会的“Microscope
”、窝窝网的“Tracing
”都是这样的系统。
一般的,一个分布式服务跟踪系统,主要有三部分:
数据收集、数据存储和数据展示。
根据系统大小不同,每一部分的结构又有一定变化。譬如,对于大规模分布式系统,数据存储可分为实时数据和全量数据两部分,实时数据用于故障排查(troubleshooting),全量数据用于系统优化;数据收集除了支持平台无关和开发语言无关系统的数据收集,还包括异步数据收集(需要跟踪队列中的消息,保证调用的连贯性),以及确保更小的侵入性;数据展示又涉及到数据挖掘和分析。虽然每一部分都可能变得很复杂,但基本原理都类似。
服务追踪的追踪单元是从客户发起请求(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来展示数据。
Sleuth术语
因为Sleuth是根据Google的Dapper’s论文而来的,所以在术语上也借鉴了Dapper。
- Span: 最基本的工作单元。例如: 发送一个RPC就是一个新的span,同样一次RPC的应答也是。Span通过一个唯一的,长度为64位的ID来作为标识,另外,再使用一个64位ID用于服务调用跟踪。Span也可以带有其他数据,eg:描述,时间戳,键值对标签,起始Span的ID,以及处理ID(通常使用IP地址)等等。 Span有起始和结束,它们用于跟踪时间信息。Span应该都是成对出现的,有始必有终,所以一旦创建了一个span,那就必须在未来某个时间点结束它。
提示: 起始的Span通常被称为:
root span
。它的id通常也被作为一个跟踪记录的id。
- Trace: 一个树结构的Span集合。例如:在分布式大数据存储中,可能每一次请求都是一次跟踪记录。
-
Annotation: 用于记录一个事件的时间信息。一些基础核心的Annotation用于记录请求的起始和结束时间,例如:
- cs: 客户端发送(Client Sent的缩写)。这个annotation表示一个span的起始;
-
sr: 服务端接收(Server Received的缩写)。表示服务端接收到请求,并开始处理。如果减去
cs
的时间戳,则可以计算出网络传输耗时。 -
ss: 服务端完成请求处理,应答信息被发回客户端(Server Sent的缩写)。如果减去
sr
的时间戳,则可以计算出服务端处理请求的耗时。 -
cr: 客户端接收(Client Received的缩写)。标志着Span的结束。客户端成功的接收到服务端的应答信息。如果减去
cs
的时间戳,则可以计算出请求的响应耗时。
下图,通过可视化的方式描述了Span和Trace的概念:
ZipKin
Zipkin是Twitter的一个开源项目,它基于 Google Dapper 实现。我们可以使用它来收集各个服务器上请求链路的跟踪数据,并通过它提供的REST API
接口来辅助查询跟踪数据以实现对分布式系统的监控程序,从而及时发现系统中出现的延迟升高问题并找出系统性能瓶颈的根源。除了面向开发的API接口之外,它还提供了方便的UI组件来帮助我们直观地搜索跟踪信息和分析请求链路明细,比如可以查询某段时间内各用户请求的处理时间等。
它主要由4个核心组件构成。
- Collector:收集器组件,它主要处理从外部系统发送过来的跟踪信息,将这些信息转换为Zipkin内部处理的Span格式,以支待后续的存储、分析、展示等功能。
- Storage:存储组件,它主要处理收集器接收到的跟踪信息,默认会将这些信息存储在内存中。我们也可以修改此存储策略, 通过使用其他存储组件将跟踪信息存储到数据库中。
- RESTful API: API组件,它主要用来提供外部访问接口。比如给客户端展示跟踪信息,或是外接系统访问以实现监控等。
- Web UI: UI组件,基于API组件实现的上层应用。通过UI组件,用户可以方便而又直观地查询和分析跟踪信息。
快速入门
创建 trace-1 9015
pom 依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.zk.springcloud</groupId>
<artifactId>springcloud-trace-1</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>springcloud-trace-1</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<spring-cloud.version>Finchley.SR2</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
启动类
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@SpringBootApplication
@RestController
@EnableDiscoveryClient
public class SpringcloudTrace1Application {
@Bean
@LoadBalanced
RestTemplate restTemplate(){
return new RestTemplate();
}
@GetMapping(value = "/trace-1")
public String trace(){
System.out.println("===call trace-1===");
return restTemplate().getForEntity("http://trace-2/trace-2",String.class).getBody();
}
public static void main(String[] args) {
SpringApplication.run(SpringcloudTrace1Application.class, args);
}
}
配置文件
spring.application.name=trace-1
server.port=9015
eureka.client.service-url.defaultZone=http://localhost:9001/eureka/,http://localhost:9004/eureka/
创建 trace-2 9016
pom 依赖同 trace-1 9015
启动类
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
@RestController
@EnableDiscoveryClient
public class SpringcloudTrace2Application {
@GetMapping(value = "/trace-2")
public String trace(){
System.out.println("====== <call trace-2> ======");
return "Trace";
}
public static void main(String[] args) {
SpringApplication.run(SpringcloudTrace2Application.class, args);
}
}
配置文件
spring.application.name=trace-2
server.port=9016
eureka.client.service-url.defaultZone=http://localhost:9001/eureka/,http://localhost:9004/eureka/
将 eureka 9001 和 eureka-1 9004 、 trace-1 、 trace-2 都启动
访问 http://localhost:9015/trace-1 返回 Trace
访问 http://localhost:9016/trace-2 返回 Trace
日志打印
trace-1
===call trace-1===
trace-2
====== <call trace-2> ======
实现跟踪
在 trace-1 9015 、trace-2 9016 添加如下依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
<version>1.2.5.RELEASE</version>
</dependency>
重启后,调用 访问 http://localhost:9015/trace-1 返回 Trace
跟踪原理
对 trace-2 9016 输出头信息
@GetMapping(value = "/trace-2")
public String trace(HttpServletRequest request){
System.out.println("====== <call trace-2> ====== traceid = " + request.getHeader("X-B3-TraceId")
+ " spanid = "+ request.getHeader("X-B3-SpanId"));
return "Trace";
}
重启后,调用 访问 http://localhost:9015/trace-1 返回 Trace
为应用引入和配置 Zipkin 服务
trace-1 9015 、 trace-2 9016 添加 zipkin 依赖 和 配置
<!-- 分布式链路追踪 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
<version>1.2.5.RELEASE</version>
</dependency>
配置
# zipkin 服务地址
spring.zipkin.base-url=http://localhost:9411
# #样本采集量,默认为0.1,为了测试这里修改为1,正式环境一般使用默认值。
spring.sleuth.sampler.probability=1
调用 http://localhost:9015/trace-1 可在 http://localhost:9411/zipkin 看到请求链路
消息中间件收集
修改客户端 trace-1 和 trace-2 添加 pom 依赖
<!-- 消息中间件 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-stream</artifactId>
<version>2.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
<version>2.0.1.RELEASE</version>
</dependency>
配置文件添加 rabbitmq 并将 zipkin 配置注释
# zipkin 服务地址
#spring.zipkin.base-url=http://localhost:9411
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=springcloud
spring.rabbitmq.password=123456
Less is more.