一、Spring Cloud Sleuth 是Spring Cloud 的一個組件,它的主要功能是在分佈式系統中提供服務鏈路追蹤的解決方案。
二、爲什麼需要Spring Cloud Sleuth?
微服務架構是一個分佈式架構,微服務系統按業務劃分服務單元,一個微服務系統往往有很多個服務單元。由於服務單元數量衆多 ,業務的複雜性較高,如果出現了錯誤和異常,很難去定位。主要體現在一個請求可能需要調用很多個服務,而內部服務的調用複雜性決定了問題難以定位。所以在微服務架構中,必須實現分佈式鏈路追蹤去跟進一個請求到底有哪些服務參與,參與的順序又是怎樣的 ,從而達到每個請求的步驟清晰可見,出了問題能夠快速定位的目的。
目前,常見的鏈路追蹤組件有 Google Dapper、Twitter、Zipkin以及阿里的 Eagleeye(鷹眼)等,它們都是非常優秀的鏈路追蹤開源組件。
三、Spring Cloud Sleuth採用了 Google 開源項目 Dapper 專業術語。
(1)Span基本工作單元,發送一個遠程調度任務就會產生一個Span, Span 是用 64ID唯一標識的,Trace是用另一個64ID唯一標識的。Span還包含了其他的信息,例如摘要、時間戳事件、Span的ID以及進程 ID。
(2)Trace :由一系列 Span組成的,呈樹狀結構。請求一個微服務系統的 API接口,這個API 接口需要調用多個微服務單元,調用 個微服務單元都會產生一個新的Span,所有由這個請求產生 Span組成了這個Trace。
(3)Annotation:用於記錄一個事件, 一些核心註解用於定義一個請求的開始和結束,這些註解如下。
a、cs:客戶端已發送。客戶提出了要求。此註釋指示跨度的開始。
b、sr:接收到服務器:服務器端收到了請求並開始處理它。cs
從該時間戳中減去該時間戳可揭示網絡延遲。
c、ss:服務器已發送。在請求處理完成時進行註釋(當響應被髮送回客戶端時)。sr
從該時間戳中減去該時間戳將顯示服務器端處理該請求所需的時間。
d、cr:收到客戶。表示跨度結束。客戶端已成功收到服務器端的響應。cs
從該時間戳中減去該時間戳將顯示客戶端從服務器接收響應所需的整個時間
下圖顯示了Span和Trace以及Zipkin批註在系統中的親子關係:
四、Zipkin實際案例:
1)首先構建鏈路追蹤sluth-server(zipkin):
a、第一種構建方式:編程式構建。
(1)加入依賴
<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>io.zipkin.java</groupId> <artifactId>zipkin-server</artifactId> <version>2.11.8</version> </dependency> <dependency> <groupId>io.zipkin.java</groupId> <artifactId>zipkin-autoconfigure-ui</artifactId> <version>2.11.8</version> </dependency> </dependencies>
注意:這裏的zipkin版本要慎重選擇,我在這裏消磨了很長時間,主要就是參考官方文檔,查看合適的版本。結果官方也沒有一個好點說明,這裏最好選擇2.11.8的版本。
(2)編寫啓動類
package com.cetc; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; import zipkin2.server.internal.EnableZipkinServer; @SpringBootApplication @EnableEurekaClient @EnableZipkinServer public class ZipkinApplication { public static void main(String[] args) { SpringApplication.run(ZipkinApplication.class, args); } }
(3)編寫配置文件application.yaml
server: port: 8687 spring: application: name: sleuth-zipkin eureka: client: service-url: defaultZone: http://127.0.0.1:8670/eureka/ # 實際開發中建議使用域名的方式 management: metrics: web: server: auto-time-requests: false # 關閉自動配置檢測,不然會報錯
說明:關閉自動檢測的目的是避免報錯。
b、第二種方式直接使用zipkin-server-exec.jar的執行包。
通過java -jar zipkin-server-exec.jar的方式執行
默認端口:9411。
下載地址:https://maven.aliyun.com/mvn/search 搜索zipkin-server,選擇最新版的zip-server-<version>-exec.jar下載。
2)構建聲明式調用服務(sleuth-feign):
a、加入依賴
<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-openfeign</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zipkin</artifactId> </dependency>
b、編寫啓動類
package com.cetc; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; import org.springframework.cloud.openfeign.EnableFeignClients; @SpringBootApplication @EnableEurekaClient @EnableFeignClients public class SleuthFeignApplication { public static void main(String[] args) { SpringApplication.run(SleuthFeignApplication.class, args); } }
c、其他的feign相關類
package com.cetc.config; import feign.Retryer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class FeignConfiguration { @Bean public Retryer retryer() { return new Retryer.Default(); } }
package com.cetc.feign.client; import com.cetc.config.FeignConfiguration; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.stereotype.Component; import org.springframework.web.bind.annotation.GetMapping; @Component @FeignClient(value = "client", configuration = {FeignConfiguration.class}) public interface TestFeign { @GetMapping("/api/test/getPort") Integer getPort(); }
package com.cetc.service.impl; import com.cetc.feign.client.TestFeign; import com.cetc.service.ISleuthFeignService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class SleuthFeignServiceImpl implements ISleuthFeignService { @Autowired private TestFeign testFeign; @Override public Integer getPort() { return testFeign.getPort(); } }
基本上按照Spring-Cloud之Feign聲明式調用-4的方式進行編寫的,只是做了一下名稱修改。
package com.cetc.web.rest; import com.cetc.service.ISleuthFeignService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/sleuthFeign") public class SleuthFeignResource { @Autowired private ISleuthFeignService feignService; @GetMapping("/getPort") public Integer getPort() { return feignService.getPort(); } }
d、配置文件application.yaml
server: port: 8688 spring: application: name: sleuth-feign zipkin: base-url: http://127.0.0.1:8687/ sleuth: sampler: probability: 1.0 eureka: client: service-url: defaultZone: http://127.0.0.1:8670/eureka/ # 實際開發中建議使用域名的方式
說明:這裏我是使用的自己編寫zipkin-server所以配置也是自己的。
3)編寫網關服務sleuth-zuul:
a、加入依賴
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-zuul</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zipkin</artifactId> </dependency>
b、編寫啓動類
package com.cetc; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; import org.springframework.cloud.netflix.zuul.EnableZuulProxy; @SpringBootApplication @EnableEurekaClient @EnableZuulProxy public class SleuthZuulApplication { public static void main(String[] args) { SpringApplication.run(SleuthZuulApplication.class, args); } }
c、編寫配置文件application.yaml
server: port: 8689 spring: application: name: sleuth-zuul zipkin: base-url: http://127.0.0.1:8687/ sleuth: sampler: probability: 1.0 zuul: routes: sleuth-zuul: path: /sleuthZuul/** serviceId: sleuth-feign eureka: client: service-url: defaultZone: http://127.0.0.1:8670/eureka/ # 實際開發中建議使用域名的方式
4)測試:啓動Eureka-Server、2個Eureka-Client、Sleuth-Server(Zipkin)、Sleuth-Feign、Sleuth-Zuul。端口分別爲8670、8673/8674、8687、8688、8689。
先訪問一下網關暴露的服務:
訪問:http://127.0.0.1:8687/zipkin/點擊Find Traces
五、在鏈路中添加自定義數據
在任何可以過濾的地方加入一下代碼即可。這裏是直接在Rest接口中添加。
@Autowired private Tracer tracer; tracer.currentSpan().tag("operator", "feign"); System.out.println(tracer.currentSpan().context().traceId()); ...
實例:
package com.cetc.web.rest; import brave.Tracer; import com.cetc.service.ISleuthFeignService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/sleuthFeign") public class SleuthFeignResource { @Autowired private ISleuthFeignService feignService; @Autowired private Tracer tracer; @GetMapping("/getPort") public Integer getPort() { tracer.currentSpan().tag("operator", "feign"); System.out.println(tracer.currentSpan().context().traceId()); return feignService.getPort(); } }
說明:這裏主要是測試,實際引用可以直接做請求攔截。這裏只是展示一個訪問的過程。
注意:這裏的節點是可以添加新的數據,或者獲取其他節點數據,用於日誌記錄等。這樣方便查看!
測試結果:
六、使用RabbitMQ傳輸鏈路數據:
1)說明:Spring Cloud 2.0以後就廢棄了RabbitMQ在Sleuth中的使用了,官方說明如何需要使用,請添加如下依賴。
大概意思就是spring-cloud-sleuth-stream被廢棄,如果想在Sleuth中使用RabbitMQ,請添加spring-cloud-starter-zipkin和spring-rabbit。實測這種方式只針對於Sleuth-Client有用,代碼形式的Sleuth-Server沒啥用。
2)爲了更好的展示代碼和jar包2種效果,我們需要做一些修改這裏做了一些修改。
a、修改Sleuth-Server(zipkin)代碼的方式
(1)加入rabbitmq的依賴
<dependency> <groupId>io.zipkin.java</groupId> <artifactId>zipkin-autoconfigure-collector-rabbitmq</artifactId> <version>2.11.8</version> </dependency>
(2)加入報錯的配置
package com.cetc.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import zipkin2.collector.CollectorMetrics; import zipkin2.collector.CollectorSampler; import zipkin2.collector.InMemoryCollectorMetrics; import zipkin2.storage.InMemoryStorage; import zipkin2.storage.StorageComponent; @Configuration public class ZipkinConfiguration { @Bean public CollectorSampler collectorSampler() { return CollectorSampler.ALWAYS_SAMPLE; } @Bean public CollectorMetrics collectorMetrics() { return InMemoryCollectorMetrics.NOOP_METRICS; } @Bean public StorageComponent storageComponent() { InMemoryStorage.Builder builder = new InMemoryStorage.Builder(); return builder.build(); } }
(3)添加rabbitmq的配置(application.yaml)
zipkin: collector: rabbitmq: addresses: 127.0.0.1:5672 username: guest password: guest
b、通過jar包啓動
java -jar zipkin-server-2.19.2-exec.jar --zipkin.collector.rabbitmq.addresses=127.0.0.1:5672 --zipkin.collector.rabbitmq.username=guest --zipkin.collector.rabbitmq.password=guest
3)Sleuth-Client的修改
(1)在Sleuth-Zuul和Sleuth-Feign中加入rabbitmq依賴
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-stream-rabbit</artifactId> </dependency>
(2)修改application.yaml配置
spring: application: name: sleuth-zuul zipkin: # base-url: http://127.0.0.1:8687/ sender: type: rabbit rabbitmq: host: 127.0.0.1 port: 5672 username: guest password: guest
spring: application: name: sleuth-feign zipkin: # base-url: http://127.0.0.1:8687/ sender: type: rabbit rabbitmq: host: 127.0.0.1 port: 5672 username: guest password: guest
4)到此上面基本上配置完成。下面進行代碼的Sleuth-Server測試。啓動項目包含:Eureka-Server、2個Eureka-Client、Sleuth-Server(zipkin)、Sleuth-Feign、Sleuth-Zuul端口分別爲8670、8673/8674、8687、8688、8689
上面是通過代碼去啓動的,下面進行jar測試:
七、將鏈路數據存入數據庫,這裏只講服務端的,客戶端的大同小異。
1)加入依賴
<dependency> <groupId>io.zipkin.java</groupId> <artifactId>zipkin-autoconfigure-storage-mysql</artifactId> <version>2.11.8</version> </dependency> <!--解決spring自帶的版本不一致問題--> <dependency> <groupId>org.jooq</groupId> <artifactId>jooq</artifactId> <version>3.12.3</version> </dependency> <!--解決spring自帶的版本不一致問題-->
說明:jooq在springboot2.0中是3.10.5的版本,此版本與zipkin的版本調用不一致,需要使用3.11.x以上版本
2)修改ZipkinConfiguration配置,主要是屏蔽內存的配置。
package com.cetc.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import zipkin2.collector.CollectorMetrics; import zipkin2.collector.CollectorSampler; import zipkin2.collector.InMemoryCollectorMetrics; import zipkin2.storage.InMemoryStorage; import zipkin2.storage.StorageComponent; @Configuration public class ZipkinConfiguration { @Bean public CollectorSampler collectorSampler() { return CollectorSampler.ALWAYS_SAMPLE; } // @Bean // public CollectorMetrics collectorMetrics() { // return InMemoryCollectorMetrics.NOOP_METRICS; // } // // @Bean // public StorageComponent storageComponent() { // InMemoryStorage.Builder builder = new InMemoryStorage.Builder(); // return builder.build(); // } }
3)在application.yaml加入以下配置
zipkin:
storage:
type: mysql
mysql:
host: 127.0.0.1
port: 3306
username: root
password: root
db: spring-cloud-sleuth
說明:這裏不採用spring的jdbc配置,目的是zipkin本身提供了自動注入功能,所以只需要加入即可。數據庫的配置可以在ZipkinMySQLStorageProperties找到,這裏不過解釋
4)在指定的數據庫中加入對應表,mysql.sql在zipkin-storage-mysql-v1-2.11.8.jar中,此依賴在zipkin-autoconfigure-storage-mysql中加入
在對應數據庫中執行mysql.sql。
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`);
5)測試:
八、在elasticsearch中存儲鏈路數據。爲什麼不是用msyql存放鏈路數據嗎?因爲在高併發的情況下mysql的存儲肯定會存在問題的,雖然使用了rabbitmq來減輕壓力,但是應用效果不理想。
1)elasticsearch下載安裝地址,因爲目前使用的springcloud2.0建議使用6.8.5版本:
elasticsearch:https://www.elastic.co/cn/downloads/elasticsearch
kibana(elasticsearch的視圖展示)下載地址:https://www.elastic.co/cn/downloads/kibana
中文下載網:https://elasticsearch.cn/download/
百度網盤6.8.5:https://pan.baidu.com/s/1aNTF9ELva4GJ91Zm4jZ-9A
2)運行elasticsearch和kibana
.\bin\elasticsearch.bat
.\bin\kibana.bat
elasticsearch:默認端口9200
kibana:默認端口5601
elasticsearch成功標誌:
kibana成功標誌:
3)在Sleuth-Server中配置elasticsearch:
a、加入依賴
<dependency> <groupId>io.zipkin.java</groupId> <artifactId>zipkin-autoconfigure-storage-elasticsearch-http</artifactId> <version>2.8.4</version> </dependency>
b、ZipkinConfiguration的配置和mysql配置一樣
package com.cetc.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import zipkin2.collector.CollectorSampler; @Configuration public class ZipkinConfiguration { @Bean public CollectorSampler collectorSampler() { return CollectorSampler.ALWAYS_SAMPLE; } // @Bean // public CollectorMetrics collectorMetrics() { // return InMemoryCollectorMetrics.NOOP_METRICS; // } // // @Bean // public StorageComponent storageComponent() { // InMemoryStorage.Builder builder = new InMemoryStorage.Builder(); // return builder.build(); // } }
c、修改配置文件application.yaml
zipkin:
storage:
type: elasticsearch
elasticsearch:
hosts: ["127.0.0.1:9200"]
具體可配置的如下:
4)測試。啓動Eureka-Server、2個Eureka-Client、Sleuth-Server(elasticSearch)、Sleuth-Feign、Sleuth-Zuul端口分別爲8670、8673/8674、8690、8688、8689
通過kibana查看
九、源碼地址:https://github.com/lilin409546297/spring-cloud/tree/master/sleuth