分布式進階-鏈路追蹤SpringCloudSleuth、Zipkin【實戰篇】
一、前言
我們在使用微服務的時候,往往涉及到各個微服務之間的調用,肯定會存在深度的調用鏈路,如果出現BUG或者異常,就會讓問題定位和處理效率非常低。有了Sleuth ,就可以幫助我們記錄、跟蹤應用程序中的請求和操作。通常與 Zipkin 配合使用,從而提供更全面的可視化應用程序跟蹤和分析功能。
就像ElasticSearch和Kibana一樣!
復雜的鏈路調用如下圖所示:
在繼續往下看的同時,需要你具備Springboot整合Nacos構建一個聚合項目的能力。
當然如果不想自己來,小編也給大家準備好了。大家可以下載運行一下,開始下面的實戰!
防止Github訪問不了,這里把代碼提交到了Gitee。
cloud-sleuth-zipkin-demo代碼下載地址:https://gitee.com/wang-zhenjun/cloud-sleuth-zipkin-demo
二、Spring Cloud Sleuth 介紹
1、簡介
Spring Cloud Sleuth 是 Spring Cloud 生態系統的一部分,它是一個分布式追蹤解決方案,用于監視微服務架構中的請求流程,并幫助開發者跟蹤請求在不同微服務之間的傳播路徑。
Sleuth主要用于解決微服務架構中的分布式系統跟蹤和調試問題。
官網文檔:https://docs.spring.io/spring-cloud-sleuth/docs/2.2.8.RELEASE/reference/html/。
2、核心概念
我們可以看一下官網的圖片:
簡單名詞介紹:
?
cs:「客戶端發送」,客戶已提出請求。該注釋指示跨度的開始。sr:「服務器已接收」,服務器端收到請求并開始處理。從此時間戳中減去cs時間戳即可得出網絡延遲。ss:「服務器發送」,在請求處理完成時(當響應發送回客戶端時)進行注釋。從這個時間戳中減去sr時間戳就可以得出服務器端處理請求所需的時間。cr:「客戶端已收到」,表示跨度的結束。客戶端已成功收到服務器端的響應。從此時間戳中減去cs時間戳即可得出客戶端從服務器接收響應所需的整個時間。
?
詳細信息可以看官網介紹,總結一下:
名詞 | 翻譯 | 解釋 |
Trace | 追蹤 | Trace 是一個請求的整體追蹤。它代表了從請求的起始點到結束點的完整路徑,經過多個微服務。每個 Trace 都有一個唯一的 Trace ID。 |
Span | 跨度 | Span 是 Trace 中的一個小段,它代表了請求在某個特定微服務上的處理過程。Spans 之間有父子關系,它們可以形成一個層次結構,以表示請求的處理路徑。 |
Trace ID | 追蹤標識 | Trace ID 是唯一標識一個 Trace 的標識符。它在整個 Trace 中保持不變,用于將不同的 Span 關聯到同一個 Trace 上。 |
Span ID | 跨度標識 | Span ID 是唯一標識一個 Span 的標識符。它用于在不同 Span 之間建立父子關系。 |
Parent Span ID | 父 Span 標識 | 父 Span ID 是標識一個 Span 的父 Span 的標識符。它用于建立 Span 之間的關系。 |
Annotations | 注解 | Annotations 是關于 Span 的額外信息,通常用于記錄 Span 的開始和結束時間、操作名稱、以及其他相關信息。Annotations 可以幫助你更好地理解請求的處理過程。 |
Binary Annotations | 二進制注解 | Binary Annotations 是鍵值對形式的信息,用于記錄與 Span 相關的自定義信息,例如請求的狀態、錯誤信息等。 |
Collector | 收集器 | Collector 是用于收集追蹤信息的組件,它將追蹤數據發送到后端存儲或可視化工具(如Zipkin或Jaeger)。Collector 可以將 Span 數據持久化,以供分析和監視使用。 |
Sampler | 采樣器 | Sampler 用于確定是否對一個請求進行追蹤。它決定是否為請求創建一個 Trace。Sampler 可以根據策略決定是否記錄某個請求的 Trace 數據,以避免記錄過多的追蹤信息,從而降低性能開銷。 |
「官網的圖,每一個代表一個組件,他們之間進行調用,畫的少了Trace Id,加上就好了。」
每個組件都會生成一個 Trace Id(全局唯一),還會有 Span Id、Parent Id 三部分組成。鏈路上的所有組件組成一個完整的 Trace。
「注意:」
「頭鏈路Parent Id = null,其余的都指向上一個組件的Span Id,從而形成鏈路。」
「一次鏈路調用所有的組件Trace Id都是一樣的。」
「這里說的組件就是一個個的微服務!」
三、 Zipkin介紹和搭建
1、定義
Zipkin 是一個分布式追蹤系統。它有助于收集解決服務架構中的延遲問題所需的計時數據。功能包括該數據的收集和查找。
Zipkin官網地址:https://zipkin.io/。
2、核心概念
名詞 | 翻譯 | 解釋 |
Trace | 追蹤 | Trace 代表整個請求的追蹤路徑,跨越不同的服務。 |
Span | 跨度 | Span 是基本工作單位,代表了請求在單個服務中的處理過程。 |
Trace ID | 追蹤標識 | Trace ID 是唯一標識一個 Trace 的標識符,用于將不同的 Span 關聯到同一個 Trace 上。 |
Annotations | 注解 | Annotations 用于記錄 Span 的關鍵事件,通常包括開始和結束時間、操作名稱等。 |
Binary Annotations | 二進制注解 | Binary Annotations 用于記錄額外的自定義信息,例如請求狀態、錯誤信息等。 |
Collector | 收集器 | Collector 負責接收和存儲從不同服務發送的 Span 數據,以便后續的檢查和分析。 |
Query and Visualization | 查詢和可視化 | 提供了查詢和可視化界面,允許用戶查看和分析跟蹤數據,以幫助故障排查和性能優化。 |
盡管Sleuth 和 Zipkin有些術語和概念中有相似之處,但它們是兩個不同的工具,各自有自己的實現和用途。
「Spring Cloud Sleuth 用于生成和傳播跟蹤信息,而 Zipkin 用于收集、存儲、查詢和可視化這些信息。它們可以協同工作,但也可以獨立使用。」
3、docker搭建
官方有三種方式搭建,推薦使用:「如果您熟悉 Docker,這是首選的啟動方法。」
Docker Zipkin項目能夠構建 docker 鏡像、提供腳本和docker-compose.yml 用于啟動預構建鏡像的腳本。
https://github.com/openzipkin/docker-zipkin/blob/master/docker-compose.yml。
最快的啟動方式是直接運行最新的鏡像:
docker run -d -p 9411:9411 openzipkin/zipkin
我們啟動成功,在Windows下訪問看是否成功!
http://192.168.239.130:9411/zipkin/
「注意:」
「Zipkin默認將追蹤數據信息保存到內存,重啟服務后追蹤數據丟失,Zipkin支持將追蹤數據持久化到MySQL或ES。」
可以直接使用docker-componse運行:
docker-componse運行腳本:https://github.com/openzipkin/zipkin/tree/master/docker/examples
可以自行試一下,這里就不帶大家演示了!
四、Springboot整合
今天我們來進行簡單的鏈路模擬:
「service-order模塊調用service-stock模塊調用service-message模塊」
「通信我們使用openFeign來進行調用,三個模塊統一使用nacos進行注冊」
大家可以下載一下項目體驗一下,可以自己搭建,就是一個聚合項目!
結構如下:
1、導入依賴
這是父依賴。
<properties>
<spring.boot.version>2.7.3</spring.boot.version>
<spring.cloud.dependencies.version>2021.0.1</spring.cloud.dependencies.version>
<spring.cloud.alibaba.version>2021.0.1.0</spring.cloud.alibaba.version>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<org.projectlombok.lombok>1.18.26</org.projectlombok.lombok>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring.cloud.dependencies.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring.boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring.cloud.alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${org.projectlombok.lombok}</version>
</dependency>
</dependencies>
</dependencyManagement>
spring-cloud-dependencies里包含了sleuth、zipkin的依賴,父不需要再定義管理版本。
子依賴要比父依賴多了sleuth、zipkin兩個,還有openFeign的包!
<!-- Sleuth依賴項 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<!--Zipkin 依賴-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-zipkin</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
2、yml配置
三份自己改一下端口和應用名稱:service-order、service-stock、service-message;端口分別為:9000、9001、9002
server:
port: 9000
spring:
application:
# 應用名稱
name: service-order
cloud:
nacos:
discovery:
# 服務注冊地址
server-addr: localhost:8848
zipkin:
base-url: http://192.168.239.130:9411
sender:
type: web # 設置使用 http 的方式傳輸數據
3、詳細代碼
記得在啟動類上添加注解:@EnableFeignClients,表示開啟feign調用。
完整的結構如下:
下面把具體代碼給大家:OrderController :
/**
* @author wangzhenjun
* @date 2023/10/31 14:25
*/
@Slf4j
@RequiredArgsConstructor
@RequestMapping("/order")
@RestController
public class OrderController {
private final RemoteStockFeignService remoteStockFeignService;
/**
* 模擬下單流程
* @param userId
* @param productId
* @return
*/
@GetMapping("/createOrder")
public String createOrder(@RequestParam("userId") Integer userId, @RequestParam("productId") Integer productId) {
log.info("====>訂單模塊<========");
// 調用庫存服務進行庫存扣減
String stockResult = remoteStockFeignService.subtractStock(userId,productId,1);
log.info("扣減庫存結果:{}", stockResult);
// 還有其他。。。。。
return "下單成功!";
}
}
RemoteStockFeignService :
/**
* @author wangzhenjun
* @date 2023/10/31 14:29
*/
@FeignClient(value = "service-stock")
public interface RemoteStockFeignService {
@GetMapping(value = "/stock/subtractStock")
String subtractStock(@RequestParam(value = "userId") Integer userId,@RequestParam(value = "productId") Integer productId,@RequestParam(value = "num") Integer num);
}
StockController :
/**
* @author wangzhenjun
* @date 2023/10/31 14:40
*/
@Slf4j
@RequiredArgsConstructor
@RequestMapping("/stock")
@RestController
public class StockController {
private final RemoteMessageFeignService remoteMessageFeignService;
@GetMapping(value = "/subtractStock")
public String subtractStock(@RequestParam(value = "userId") Integer userId,@RequestParam(value = "productId") Integer productId, @RequestParam(value = "num") Integer num) {
log.info("====>庫存模塊<========");
if (productId < 1) {
throw new RuntimeException("商品不存在,請重新請求!");
}
// 調用短信模塊給用戶發下單成功短信
String messageResult = remoteMessageFeignService.sendMessage(userId);
log.info("發送短信結果:{}", messageResult);
return "扣減庫存成功!";
}
}
RemoteMessageFeignService:
/**
* @author wangzhenjun
* @date 2023/10/31 14:29
*/
@FeignClient(value = "service-message")
public interface RemoteMessageFeignService {
@GetMapping(value = "/message/sendMessage/{userId}")
String sendMessage(@PathVariable(value = "userId") Integer userId);
}
MessageController :
/**
* @author wangzhenjun
* @date 2023/10/31 14:40
*/
@Slf4j
@RequestMapping("/message")
@RestController
public class MessageController {
@GetMapping(value = "/sendMessage/{userId}")
public String sendMessage(@PathVariable(value = "userId") Integer userId) {
log.info("====>短信模塊<========");
if (userId < 1 || userId > 999999) {
throw new RuntimeException("用戶不存在,請重新請求!");
}
return "發送短信成功!";
}
}
4、啟動nacos
Windows下啟動nacos,找到nacos下的bin目錄執行命令:
startup.cmd -m standalone
找到地址,訪問。用戶名密碼都是nacos。可以在配置文件中修改!
5、注冊成功
我們把三個模塊進行啟動!nacos上已經可以看到我們的服務注冊上了,可以通過服務名進行調用了!
五、調試
我們以訂單模塊為入口進行鏈路調用:
http://localhost:9000/order/createOrder?userId=2&productId=89。
1、查看日志
我們看一下訂單模塊的日志。
可以總結出Sleuth 日志格式:
[service-order,e36ebe859a7473e7,e36ebe859a7473e7]
[服務名稱,Trace ID,Span ID]
在最開始的鏈路上,Trace ID 和 Span ID 的值通常是相同的,這是因為它們都代表了整個請求的追蹤。
「一條鏈路使用一個相同的Trace ID。」
在日志沒有體現出Parent Span ID,不過不應該,我們可以通過Zipkin來看鏈路!
2、Zipkin查看
前面我們已經進入了Zipkin頁面了,只需要刷新一下就可以看到每次鏈路的記錄了!
點擊SHOW按鈕,可以看到詳細鏈路信息:
耗時,深度,Trance ID 還是挺好的。
3、模擬異常
我們來把商品修改一下:
http://localhost:9000/order/createOrder?userId=2&productId=-89。
此時是庫存服務出現的問題,就不會展示下一個消息模塊,自然而然的找到了出現問題的鏈路和根源!
在模擬一個三級錯誤的,就會看到鏈路的最后一級!
下面還可以查詢依賴關系:
點擊節點,可以查看匯總,調用次數和失敗次數的統計分析!
六、總結
分布式鏈路追蹤已經成為現代微服務架構中不可或缺的工具之一。
通過它,我們可以清晰地跟蹤請求的調用路徑,了解系統的性能,診斷潛在問題,并不斷優化我們的應用程序。
Spring Cloud Sleuth讓我們輕松生成和傳播跟蹤信息,使我們的微服務能夠協同工作,無縫地捕捉每個請求的處理路徑。
Zipkin作為一個流行的分布式追蹤系統,為我們提供了可視化界面,使我們能夠以圖形化的方式查看和分析跟蹤數據。
「當然簡單系統上這個大材小用,但是我們可以在項目中試試,加了也不會影響程序的正常運行,做一個簡單的知識儲備!」