Spring Cloud實戰:服務鏈路追蹤Spring Cloud Sleuth

  1. <strong>
  2. <strong>

前言

Spring Cloud Sleuth 的主要功能就是為 分佈式系統 提供 追蹤解決方案,並且兼容支持了 Zipkin,只需要在 pom.xml 文件中引入相應的 依賴 即可。本文主要講述 服務追蹤組件 Zipkin,Spring Cloud Sleuth 集成了 Zipkin 組件。它主要用於 聚集 來自各個 異構系統實時監控數據,用來追蹤 微服務架構 下的 系統延時問題

Spring Cloud實戰:服務鏈路追蹤Spring Cloud Sleuth

正文

1. 相關術語

1.1. Span

Span 是一個基本的 工作單元,用於描述一次 RPC 調用,Span 通過一個 64 位的 spanId 作為 唯一標識。Zipkin 中的 Span 還有其他數據信息,比如 摘要時間戳事件關鍵值註釋 (tags) 以及 進度 ID (通常是 IP 地址)。Span 在不斷的啟動和停止,同時記錄了 時間信息,一個 Span 創建後,必須在未來的某個時刻停止它。

1.2. Trace

一系列 Span 組成的一個 樹狀結構。例如,如果你正在跑一個大型

分佈式系統,可能需要創建一個 Trace。

1.3. Annotation

表示 基本標註列表,一個 Annotation 可以理解成 Span 生命週期中 重要時刻數據快照,比如一個 Annotation 中一般包含 發生時刻(timestamp)、事件類型(value)、端點(endpoint)等信息。其中 Annotation 的 事件類型 包含以下四類:

  • cs - Client Sent

客戶端 發起一個請求,這個 Annotion 描述了這個 Span 的開始。

  • sr - Server Received

服務端 獲得請求並 準備開始 處理它,如果將 sr 減去 cs 的 時間戳 便可得到 網絡延遲

  • ss - Server Sent

服務端 完成請求處理,如果將 ss 減去 sr 的 時間戳,便可得到 服務端 處理請求消耗的時間。

  • cr - Client Received

客戶端 成功接收到 服務端 的響應,如果將 cr 減去 cs 的 時間戳,便可得到 整個請求 所消耗的 總時間

2. 項目結構

本文案例主要由 四個模塊 組成:

  • eureka-server:作為 服務註冊中心
  • zipkin-server:作為 鏈路追蹤服務中心,負責存儲 鏈路數據
  • service-hi:對外暴露一個 測試接口,同時作為 鏈路追蹤服務端,負責 產生鏈路數據
  • service-zuul:作為 路由網關
    ,負責 請求轉發,同時作為 鏈路追蹤客戶端,產生 鏈路數據,並上傳至 zipkin-server。

在 8761 端口 開啟 eureka-server 服務註冊中心,參考前面的文章即可,這裡不再演示創建。

3. 構建zipkin-server

新建一個 Spring Boot 應用模塊 zipkin-server,它的 pom.xml 完整依賴如下:



4.0.0

org.springframework.boot
spring-boot-starter-parent
1.5.3.RELEASE



io.github.ostenant.springcloud
zipkin-server
0.0.1-SNAPSHOT

zipkin-server
Demo project for Spring Boot


1.8

Dalston.RELEASE




org.springframework.cloud
spring-cloud-starter-eureka


io.zipkin.java
zipkin-server


io.zipkin.java
zipkin-autoconfigure-ui



org.springframework.boot
spring-boot-starter-test
test






org.springframework.cloud
spring-cloud-dependencies
${spring-cloud.version}
pom
import







org.springframework.boot
spring-boot-maven-plugin



在應用的入口類上, 加上註解 @EnableZipkinServer,開啟 Zipkin Server 的功能。

@EnableZipkinServer 

@EnableEurekaClient
@SpringBootApplication
public class ZipkinServerApplication {

public static void main(String[] args) {
SpringApplication.run(ZipkinServerApplication.class, args);
}
}

配置文件 application.yml 中指定服務端口號為 9411,並向 Eureka 註冊中心進行 服務註冊

eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
server:
port: 9411
spring:
application:
name: zipkin-server

4. 構建service-hi

新建一個 Spring Boot 應用模塊 service-hi,在它的 pom.xml 中引入 引入起步依賴 spring-cloud-starter-zipkin,完整依賴如下:



4.0.0

org.springframework.boot
spring-boot-starter-parent
1.5.3.RELEASE


io.github.ostenant.springcloud
service-hi
0.0.1-SNAPSHOT
eureka-client
Demo project for Spring Boot



1.8
Dalston.SR1




org.springframework.cloud
spring-cloud-starter-eureka


org.springframework.cloud
spring-cloud-starter-zipkin


org.springframework.boot
spring-boot-starter-web



org.springframework.boot
spring-boot-starter-test
test






org.springframework.cloud
spring-cloud-dependencies
${spring-cloud.version}
pom
import







org.springframework.boot
spring-boot-maven-plugin



在它的

配置文件 application.yml 中通過配置項 spring.zipkin.base-url 指定 zipkin server 的地址。

eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
server:
port: 8763
spring:
application:
name: service-hi
zipkin:
# base-url: http://localhost:9411/
# 若在同一個註冊中心的話可以啟用自動發現,省略base-url
locator:
discovery:
enabled: true #自動發現
sleuth:
sampler:
percentage: 1.0

到此為止 ZipKin 客戶端 已經整合完畢,最後在 應用啟動類 上對外暴露一個 API 接口 方便測試

@SpringBootApplication
@EnableEurekaClient
@RestController
public class ServiceHiApplication {

public static void main(String[] args) {
SpringApplication.run(ServiceHiApplication.class, args);
}

@Value("${server.port}")
private String port;

@RequestMapping("/hi")

public String home(@RequestParam String name) {
return "Hi " + name + ", I am from port: " + port;
}
}

5. 構建service-zuul

新建一個 Spring Boot 應用模塊 service-zuul,在 pom.xml 中引入依賴 spring-cloud-starter-zipkin,完整依賴如下:



4.0.0


org.springframework.boot
spring-boot-starter-parent
1.5.3.RELEASE



io.github.ostenant.springcloud
service-zuul
0.0.1-SNAPSHOT

service-zuul
Demo project for Spring Boot


1.8
Dalston.SR1




org.springframework.cloud
spring-cloud-starter-eureka


org.springframework.cloud
spring-cloud-starter-zuul


org.springframework.boot
spring-boot-starter-web


org.springframework.cloud
spring-cloud-starter-zipkin



org.springframework.boot
spring-boot-starter-test
test






org.springframework.cloud
spring-cloud-dependencies
${spring-cloud.version}
pom
import







org.springframework.boot
spring-boot-maven-plugin



在它的 配置文件 application.yml 中通過配置項 spring.zipkin.base-url 指定 zipkin server 的地址。同時指定 service-hi 基於 zuul 的 服務路徑 匹配前綴。

server:
port: 8769
spring:
application:
name: service-zuul
client:
service-url:
defaultZone: http://localhost:8761/eureka/

sleuth:
sampler:
percentage: 1.0
zipkin:
# base-url: http://localhost:9411/
# 若在同一個註冊中心的話可以啟用自動發現,省略base-url
locator:
discovery:
enabled: true #自動發現
zuul:
routes:
api-hi:
path: /api-hi/**
serviceId: service-hi

到這裡可以發現,引入 ZipKin 服務只需要 導依賴配置屬性 兩步即可。在應用的 啟動類 上使用 @EnableZuulProxy 註解開啟 路由網關

@EnableZuulProxy
@EnableEurekaClient
@SpringBootApplication
public class ServiceZuulApplication {

public static void main(String[] args) {
SpringApplication.run(ServiceZuulApplication.class, args);
}
}

6. 測試追蹤過程

完整 鏈路追蹤模塊 搭建完畢,總結一下:

  1. 搭建一臺 zipkin server 作為 鏈路服務中心
  2. 給各個 服務 引入 zipkin 依賴,配置 zipkin server 地址即可。

下面按順序依次啟動 eureka-server、service-zipkin、service-hi 和 service-zuul。訪問服務網關,地址為: http://localhost:8769/api-hi/hi?name=vainlgory,服務響應內容如下:

Hi Vainlgory, I am from port: 8763

然後訪問 http://localhost:9411,即訪問 ZipKin 提供的可視化頁面。

Spring Cloud實戰:服務鏈路追蹤Spring Cloud Sleuth

這個界面用於顯示 ZipKin Server 收集的 鏈路數據,可以根據 服務名開始時間結束時間請求消耗的時間 等條件來查找。單擊 Find Tracks按鈕,可以查看請求的 調用時間消耗時間,以及請求的 鏈路情況

單擊頂部的 Dependencies 按鈕,可以查看服務的 依賴關係

Spring Cloud實戰:服務鏈路追蹤Spring Cloud Sleuth

7. 在鏈路數據中添加自定義數據

現在需要實現一個功能:在 鏈路數據 中加上請求的 操作人。本案例在 service-zuul 網關服務 中實現。

  1. 新建一個 ZuulFilter 過濾器,它的類型為 post 類型,order 為 900,開啟 攔截功能
  2. 在過濾器的 攔截邏輯方法 run() 裡面,通過 Tracer 的 addTag() 方法加上 自定義 的數據,在本案例中加上了鏈路的 操作人
  3. 也可以在這個 過濾器 中獲取 當前鏈路 的 traceld 信息,traceld 作為 鏈路數據
    唯一標識,可以存儲在 log 日誌中,方便後續查找,本案例只是將 traceld 的信息簡單地打印在控制檯上。代碼如下:
@Component
public class LoggerFileter extends ZuulFilter {
@Autowired
private Tracer tracer;

@Override
public String filterType() {
return FilterConstants.POST_TYPE;
}

@Override
public int filterOrder() {
return 900;
}

@Override
public boolean shouldFilter() {
return true;
}

@Override
public Object run() {
tracer.addTag("operator","forezp");
System.out.println(tracer.getCurrentSpan().traceIdString());
return null;
}
}

8. 使用RabbitMQ傳輸鏈路數據

首先改造 zipkin-server 項目模塊,在它的 pom.xml 文件中將 zipkin-server 的依賴去掉,加上 spring-cloud-sleuth-zipkin-stream 和 spring-cloud-starter-stream-rabbit 的依賴,配置如下:


org.springframework.cloud
spring-cloud-sleuth-zipkin-stream


org.springframework.cloud
spring-cloud-starter-stream-rabbit

在 zipkin-server 的配置文件 application.yml 中加上 RabbitMQ 的配置,包括 主機名端口用戶名密碼,代碼如下:

spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest

在應用的啟動類 ZipkinServerApplication 上把註解 @EnableZipkinServer 替換為註解 @EnableZipkinStreamServer,開啟 ZipkinStreamServer,代碼如下:

@EnableEurekaClient
@SpringBootApplication
@EnableZipkinStreamServer
public class ZipkinServerApplication {

public static void main(String[] args) {
SpringApplication.run(ZipkinServerApplication.class, args);
}
}

現在來改造 Zipkin Client(包括 service-zuul 和 service-hi 兩個模塊),分別在它們的 pom.xml 文件中將 spring-cloud-starter-zipkin 依賴改為 spring-cloud-sleuth-zipkin-stream 和 spring-cloud-starter-stream-rabbit,代碼如下:


org.springframework.cloud
spring-cloud-sleuth-zipkin-stream


org.springframework.cloud
spring-cloud-starter-stream-rabbit

和前面的 zipkin-server 模塊一樣,同時在配置文件 applicayion.yml 加上 RabbitMQ 的配置。這樣,就將鏈路的 數據上傳 從 HTTP 改為用 消息代組件 RabbitMQ 的方式。

9. 在MySQL數據庫中存儲鏈路數據

在上面的例子中,Zipkin Server 將數據存儲在 內存 中,一旦應用服務 重啟,之前的 鏈路數據全部丟失,這時候就需要引入 持久化機制。Zipkin 支持將 鏈路數據 存儲在 MySQL、Elasticsearch 和 Cassandra 數據庫中。本節講解如何使用 MySQL 存儲。

Zipkin Client 有兩種方式將 鏈路數據 傳輸到 Zipkin Server 中,一種是使用 HTTP,另一種是使用 RabbitMQ。Zipkin Server 通過這兩種方式來 收集鏈路數據,並存儲在 MySQL 中。

9.1. 使用HTTP傳輸鏈路數據

在 zipkin-server 模塊的 pom.xml 文件加上以下依賴:

  • Zipkin Server 的依賴 zipkin-server
  • Zipkin 的 MySQL 存儲依賴 zipkin-storage-mysql
  • Zipkin Server 的 UI 界面依賴 zipkin-autoconfigure-ui
  • MySQL 的連接器依賴 mysql-connector-java
  • JDBC 的 起步依賴 spring-boot-starter-jdbc

代碼如下:


io.zipkin.java
zipkin-server
1.19.0


io.zipkin.java
zipkin-storage-mysql
1.19.0


io.zipkin.java
zipkin-autoconfigure-ui


mysql
mysql-connector-java


org.springframework.boot
spring-boot-starter-jdbc

在 zipkin-server 模塊的 配置文件 application.yml 中加上 數據源 的配置,包括 數據庫 的 Url、用戶名密碼連接驅動,並且需要配置 zipkin.storage.type 為 mysql,代碼如下:

spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/spring-cloud-zipkin?useUnicode=true&characterEncoding=utf8&useSSL=false
username: root
password: 123456
zipkin:
storage:
type: mysql

另外需要在 MySQL 數據庫中放置數據庫的 初始化腳本,創建包括 zipkin_spans、zipkin_annotations 和 zipkin_dependencies 這幾張表。

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';
ALTER TABLE zipkin_annotations ADD INDEX(`a_key`) COMMENT 'for getTraces';
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`);

最後需要在應用的啟動類 ZipkinServerApplication 中注入 MySQLStorage 的 Bean,代碼如下:

@Bean
public MySQLStorage mySQLStorage(DataSource datasource) {
return MySQLStorage.builder()
.datasource(datasource)
.executor(Runnable::run)
.build();
}

只需要上述步驟,即可將使用 HTTP 傳輸的 鏈路數據

存儲在 MySQL 數據庫中。

9.2. 使用RabbitMQ傳輸鏈路數據

本節的案例是在使用 RabbitMQ 傳輸數據 基礎上進行改造的,只需要改造 zipkin-server 的工程。

在 zipkin-server 工程的 pom.xml 文件中加上 MySQL 的 連接器依賴 mysql-connector-java 和 JDBC 的 起步依賴 spring-boot-starter-jdbc,代碼如下:


mysql
mysql-connector-java


org.springframework.boot
spring-boot-starter-jdbc

在 zipkin-server 模塊的 配置文件 application.yml 中加上 數據源 的配置,包括數據庫的 Uri、用戶名密碼連接驅動,同樣的配置 zipkin.storage.type 為 mysql,代碼如下:

spring: 

datasource:
url: jdbc:mysql://localhost:3306/spring-cloud-zipkin?useUnicode=true&characterEncoding=utf8&useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
zipkin:
storage:

另外需要在 MySQL數據庫中初始化 數據庫腳本,具體同上一節。

10. 在ElasticSearch中存儲鏈路數據

併發高 的情況下,使用 MySQL 存儲 鏈路數據 顯然不合理,這時可以選擇使用 ElasticSearch 作為存儲。

下載並安裝 ElasticSearch 和 Kibana,下載地址為 www.elastic.co/products/el…。安裝完成後啟動,其中 ElasticSearch 的 默認端口號 為 9200,Kibana 的 默認端口號 為 5601。

本節是在 第八節 的基礎上進行改造。首先在 pom.xml 文件中加上 zipkin 的依賴和 zipkin-autoconfigure-storage-elasticsearch-http 的依賴,代碼如下:


io.zipkin.java
zipkin

1.28.1


io.zipkin.java
zipkin-autoconfigure-storage-elasticsearch-http
1.28.1

在應用的配置文件 application.yml 中加上 Zipkin 的配置,配置了 zipkin 的 存儲類型(type) 為 Elasticsearch,使用的 存儲組件(StorageComponent)為 Elasticsearch,然後需要配置 Elasticsearch,包括 hosts( 可以配置 多個實例,用 “,” 隔開)。具體配置代碼如下:

zipkin:
storage:
type: elasticsearch
StorageComponent: elasticsearch
elasticsearch:
cluster: elasticsearch
max-requests: 30
index: zipkin
index-shards: 3
index-replicas: 1
hosts: localhost:9200

只需要完成這些配置,Zipkin Server 的 鏈路數據 就可以存儲在 ElasticSearch 裡面。

11. 用Kibana展示鏈路數據

上一節講述瞭如何將 鏈路數據 存儲在 ElasticSearch 中,ElasticSearch 可以和 Kibana 結合,將

鏈路數據 展示在 Kibana 上。安裝完成後啟動 Kibana,Kibana 默認會向 本地端口 為 9200 的 ElasticSearch 讀取數據。Kibana 的 默認端口 為 5601,訪問 Kibana 的主頁 http://localhost:5601。

單擊 Management 按鈕,然後單擊 Add New,添加一個 index。上一節 ElasticSearch 中寫入 鏈路數據 的 index 配置的是 zipkin,在 Kibana 界面上填寫 zipkin-*,單擊 Create 按鈕,創建完成 index 後,單擊 Discover,頁面上出現 鏈路數據
原文鏈接:https://juejin.im/post/5c56ead5e51d450165279d60


分享到:


相關文章: