分布式服務(wù)跟蹤: Spring Cloud Sleuth
什么是Sprng Cloud Sleuth
是對一個請求所經(jīng)過的整個服務(wù)鏈的跟蹤番刊,幫助我們快速發(fā)現(xiàn)錯誤根源以及監(jiān)控分析每條請求鏈路上的性能瓶頸哮针。
隨著業(yè)務(wù)不斷地發(fā)展曾我,系統(tǒng)拆分導(dǎo)致系統(tǒng)調(diào)用鏈路愈發(fā)復(fù)雜一個前端請求可能最終需要調(diào)用很多次后端服務(wù)才能完成察净,當(dāng)整個請求變慢或不可用時疆股,我們是無法得知該請求是由某個或某些后端服務(wù)引起的根资,這時就需要解決如何快讀定位服務(wù)故障點(diǎn)架专,以對癥下藥同窘。于是就有了分布式系統(tǒng)調(diào)用跟蹤的誕生。
Tips:業(yè)界分布式服務(wù)跟蹤的理論基礎(chǔ)主要來自于 Google 的一篇論文《Dapper, a Large-Scale Distributed Systems Tracing Infrastructure》部脚,使用最為廣泛的開源實(shí)現(xiàn)是 Twitter 的 Zipkin
想邦,為了實(shí)現(xiàn)平臺無關(guān)、廠商無關(guān)的分布式服務(wù)跟蹤委刘,CNCF 發(fā)布了布式服務(wù)跟蹤標(biāo)準(zhǔn) Open Tracing
丧没。國內(nèi),淘寶的“鷹眼”锡移、京東的“Hydra
”呕童、大眾點(diǎn)評的“CAT
”、新浪的“Watchman
”淆珊、唯品會的“Microscope
”夺饲、窩窩網(wǎng)的“Tracing
”都是這樣的系統(tǒng)。
一般的施符,一個分布式服務(wù)跟蹤系統(tǒng)往声,主要有三部分:
數(shù)據(jù)收集、數(shù)據(jù)存儲和數(shù)據(jù)展示戳吝。
根據(jù)系統(tǒng)大小不同烁挟,每一部分的結(jié)構(gòu)又有一定變化。譬如骨坑,對于大規(guī)模分布式系統(tǒng)撼嗓,數(shù)據(jù)存儲可分為實(shí)時數(shù)據(jù)和全量數(shù)據(jù)兩部分,實(shí)時數(shù)據(jù)用于故障排查(troubleshooting)欢唾,全量數(shù)據(jù)用于系統(tǒng)優(yōu)化且警;數(shù)據(jù)收集除了支持平臺無關(guān)和開發(fā)語言無關(guān)系統(tǒng)的數(shù)據(jù)收集,還包括異步數(shù)據(jù)收集(需要跟蹤隊(duì)列中的消息礁遣,保證調(diào)用的連貫性)斑芜,以及確保更小的侵入性;數(shù)據(jù)展示又涉及到數(shù)據(jù)挖掘和分析祟霍。雖然每一部分都可能變得很復(fù)雜杏头,但基本原理都類似。
服務(wù)追蹤的追蹤單元是從客戶發(fā)起請求(request)抵達(dá)被追蹤系統(tǒng)的邊界開始沸呐,到被追蹤系統(tǒng)向客戶返回響應(yīng)(response)為止的過程醇王,稱為一個“trace”。
每個 trace 中會調(diào)用若干個服務(wù)崭添,為了記錄調(diào)用了哪些服務(wù)寓娩,以及每次調(diào)用的消耗時間等信息,在每次調(diào)用服務(wù)時,埋入一個調(diào)用記錄,稱為一個“span”。這樣大脉,若干個有序的 span 就組成了一個 trace。在系統(tǒng)向外界提供服務(wù)的過程中仁连,會不斷地有請求和響應(yīng)發(fā)生,也就會不斷生成 trace阱穗,把這些帶有span 的 trace 記錄下來怖糊,就可以描繪出一幅系統(tǒng)的服務(wù)拓?fù)鋱D。附帶上 span 中的響應(yīng)時間颇象,以及請求成功與否等信息伍伤,就可以在發(fā)生問題的時候,找到異常的服務(wù)遣钳;根據(jù)歷史數(shù)據(jù)扰魂,還可以從系統(tǒng)整體層面分析出哪里性能差,定位性能優(yōu)化的目標(biāo)蕴茴。
Spring Cloud Sleuth為服務(wù)之間調(diào)用提供鏈路追蹤劝评。通過Sleuth可以很清楚的了解到一個服務(wù)請求經(jīng)過了哪些服務(wù),每個服務(wù)處理花費(fèi)了多長倦淀。從而讓我們可以很方便的理清各微服務(wù)間的調(diào)用關(guān)系蒋畜。此外Sleuth可以幫助我們:
- 耗時分析: 通過Sleuth可以很方便的了解到每個采樣請求的耗時,從而分析出哪些服務(wù)調(diào)用比較耗時;
- 可視化錯誤: 對于程序未捕捉的異常撞叽,可以通過集成Zipkin服務(wù)界面上看到;
- 鏈路優(yōu)化: 對于調(diào)用比較頻繁的服務(wù)姻成,可以針對這些服務(wù)實(shí)施一些優(yōu)化措施。
spring cloud sleuth可以結(jié)合zipkin愿棋,將信息發(fā)送到zipkin科展,利用zipkin的存儲來存儲信息,利用zipkin ui來展示數(shù)據(jù)糠雨。
Sleuth術(shù)語
因?yàn)镾leuth是根據(jù)Google的Dapper’s論文而來的才睹,所以在術(shù)語上也借鑒了Dapper。
- Span: 最基本的工作單元甘邀。例如: 發(fā)送一個RPC就是一個新的span琅攘,同樣一次RPC的應(yīng)答也是。Span通過一個唯一的松邪,長度為64位的ID來作為標(biāo)識坞琴,另外,再使用一個64位ID用于服務(wù)調(diào)用跟蹤测摔。Span也可以帶有其他數(shù)據(jù)置济,eg:描述,時間戳锋八,鍵值對標(biāo)簽浙于,起始Span的ID,以及處理ID(通常使用IP地址)等等挟纱。 Span有起始和結(jié)束羞酗,它們用于跟蹤時間信息。Span應(yīng)該都是成對出現(xiàn)的紊服,有始必有終檀轨,所以一旦創(chuàng)建了一個span,那就必須在未來某個時間點(diǎn)結(jié)束它欺嗤。
提示: 起始的Span通常被稱為:
root span
参萄。它的id通常也被作為一個跟蹤記錄的id。
- Trace: 一個樹結(jié)構(gòu)的Span集合煎饼。例如:在分布式大數(shù)據(jù)存儲中讹挎,可能每一次請求都是一次跟蹤記錄。
-
Annotation: 用于記錄一個事件的時間信息吆玖。一些基礎(chǔ)核心的Annotation用于記錄請求的起始和結(jié)束時間筒溃,例如:
- cs: 客戶端發(fā)送(Client Sent的縮寫)。這個annotation表示一個span的起始;
-
sr: 服務(wù)端接收(Server Received的縮寫)沾乘。表示服務(wù)端接收到請求怜奖,并開始處理。如果減去
cs
的時間戳翅阵,則可以計算出網(wǎng)絡(luò)傳輸耗時歪玲。 -
ss: 服務(wù)端完成請求處理,應(yīng)答信息被發(fā)回客戶端(Server Sent的縮寫)掷匠。如果減去
sr
的時間戳读慎,則可以計算出服務(wù)端處理請求的耗時。 -
cr: 客戶端接收(Client Received的縮寫)槐雾。標(biāo)志著Span的結(jié)束夭委。客戶端成功的接收到服務(wù)端的應(yīng)答信息募强。如果減去
cs
的時間戳株灸,則可以計算出請求的響應(yīng)耗時。
下圖擎值,通過可視化的方式描述了Span和Trace的概念:
ZipKin
Zipkin是Twitter的一個開源項(xiàng)目慌烧,它基于 Google Dapper 實(shí)現(xiàn)。我們可以使用它來收集各個服務(wù)器上請求鏈路的跟蹤數(shù)據(jù)鸠儿,并通過它提供的REST API
接口來輔助查詢跟蹤數(shù)據(jù)以實(shí)現(xiàn)對分布式系統(tǒng)的監(jiān)控程序屹蚊,從而及時發(fā)現(xiàn)系統(tǒng)中出現(xiàn)的延遲升高問題并找出系統(tǒng)性能瓶頸的根源厕氨。除了面向開發(fā)的API接口之外,它還提供了方便的UI組件來幫助我們直觀地搜索跟蹤信息和分析請求鏈路明細(xì)汹粤,比如可以查詢某段時間內(nèi)各用戶請求的處理時間等命斧。
它主要由4個核心組件構(gòu)成。
- Collector:收集器組件嘱兼,它主要處理從外部系統(tǒng)發(fā)送過來的跟蹤信息国葬,將這些信息轉(zhuǎn)換為Zipkin內(nèi)部處理的Span格式,以支待后續(xù)的存儲芹壕、分析汇四、展示等功能。
- Storage:存儲組件踢涌,它主要處理收集器接收到的跟蹤信息通孽,默認(rèn)會將這些信息存儲在內(nèi)存中。我們也可以修改此存儲策略睁壁, 通過使用其他存儲組件將跟蹤信息存儲到數(shù)據(jù)庫中利虫。
- RESTful API: API組件,它主要用來提供外部訪問接口堡僻。比如給客戶端展示跟蹤信息糠惫,或是外接系統(tǒng)訪問以實(shí)現(xiàn)監(jiān)控等。
- Web UI: UI組件钉疫,基于API組件實(shí)現(xiàn)的上層應(yīng)用硼讽。通過UI組件,用戶可以方便而又直觀地查詢和分析跟蹤信息牲阁。
快速入門
創(chuàng)建 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/
創(chuàng)建 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> ======
實(shí)現(xiàn)跟蹤
在 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>
重啟后备燃,調(diào)用 訪問 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";
}
重啟后,調(diào)用 訪問 http://localhost:9015/trace-1 返回 Trace
為應(yīng)用引入和配置 Zipkin 服務(wù)
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 服務(wù)地址
spring.zipkin.base-url=http://localhost:9411
# #樣本采集量并齐,默認(rèn)為0.1,為了測試這里修改為1客税,正式環(huán)境一般使用默認(rèn)值况褪。
spring.sleuth.sampler.probability=1
調(diào)用 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 服務(wù)地址
#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.