Spring Cloud Sleuth超詳細實戰

方誌朋發表於2017-08-06

轉載請標明出處:
blog.csdn.net/forezp/arti…
本文出自方誌朋的部落格

為什麼需要Spring Cloud Sleuth

微服務架構是一個分散式架構,它按業務劃分服務單元,一個分散式系統往往有很多個服務單元。由於服務單元數量眾多,業務的複雜性,如果出現了錯誤和異常,很難去定位。主要體現在,一個請求可能需要呼叫很多個服務,而內部服務的呼叫複雜性,決定了問題難以定位。所以微服務架構中,必須實現分散式鏈路追蹤,去跟進一個請求到底有哪些服務參與,參與的順序又是怎樣的,從而達到每個請求的步驟清晰可見,出了問題,很快定位。

舉個例子,在微服務系統中,一個來自使用者的請求,請求先達到前端A(如前端介面),然後通過遠端呼叫,達到系統的中介軟體B、C(如負載均衡、閘道器等),最後達到後端服務D、E,後端經過一系列的業務邏輯計算最後將資料返回給使用者。對於這樣一個請求,經歷了這麼多個服務,怎麼樣將它的請求過程的資料記錄下來呢?這就需要用到服務鏈路追蹤。

Google開源的 Dapper鏈路追蹤元件,並在2010年發表了論文《Dapper, a Large-Scale Distributed Systems Tracing Infrastructure》,這篇文章是業內實現鏈路追蹤的標杆和理論基礎,具有非常大的參考價值。
目前,鏈路追蹤元件有Google的Dapper,Twitter 的Zipkin,以及阿里的Eagleeye (鷹眼)等,它們都是非常優秀的鏈路追蹤開源元件。

本文主要講述如何在Spring Cloud Sleuth中整合Zipkin。在Spring Cloud Sleuth中整合Zipkin非常的簡單,只需要引入相應的依賴和做相關的配置即可。

基本術語

Spring Cloud Sleuth採用的是Google的開源專案Dapper的專業術語。

  • Span:基本工作單元,傳送一個遠端排程任務 就會產生一個Span,Span是一個64位ID唯一標識的,Trace是用另一個64位ID唯一標識的,Span還有其他資料資訊,比如摘要、時間戳事件、Span的ID、以及進度ID。
  • Trace:一系列Span組成的一個樹狀結構。請求一個微服務系統的API介面,這個API介面,需要呼叫多個微服務,呼叫每個微服務都會產生一個新的Span,所有由這個請求產生的Span組成了這個Trace。
  • Annotation:用來及時記錄一個事件的,一些核心註解用來定義一個請求的開始和結束 。這些註解包括以下:
    • cs - Client Sent -客戶端傳送一個請求,這個註解描述了這個Span的開始
    • sr - Server Received -服務端獲得請求並準備開始處理它,如果將其sr減去cs時間戳便可得到網路傳輸的時間。
    • ss - Server Sent (服務端傳送響應)–該註解表明請求處理的完成(當請求返回客戶端),如果ss的時間戳減去sr時間戳,就可以得到伺服器請求的時間。
    • cr - Client Received (客戶端接收響應)-此時Span的結束,如果cr的時間戳減去cs時間戳便可以得到整個請求所消耗的時間。

案例實戰

本文案例一共四個工程採用多Module形式。需要新建一個主Maven工程,主要指定了Spring Boot的版本為1.5.3,Spring Cloud版本為Dalston.RELEASE。包含了eureka-server工程,作為服務註冊中心,eureka-server的建立過程這裡不重複;zipkin-server作為鏈路追蹤服務中心,負責儲存鏈路資料;gateway-service作為服務閘道器工程,負責請求的轉發,同時它也作為鏈路追蹤客戶端,負責產生資料,並上傳給zipkin-service;user-service為一個應用服務,對外暴露API介面,同時它也作為鏈路追蹤客戶端,負責產生資料。

構建zipkin-server工程

新建一個Module工程,取名為zipkin-server,其pom檔案繼承了主Maven工程的pom檔案;作為Eureka Client,引入Eureka的起步依賴spring-cloud-starter-eureka,引入zipkin-server依賴,以及zipkin-autoconfigure-ui依賴,後兩個依賴提供了Zipkin的功能和Zipkin介面展示的功能。程式碼如下:


    <parent>
        <groupId>com.forezp</groupId>
        <artifactId>sleuth</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>



    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka</artifactId>
        </dependency>
        <dependency>
            <groupId>io.zipkin.java</groupId>
            <artifactId>zipkin-server</artifactId>
        </dependency>

        <dependency>
            <groupId>io.zipkin.java</groupId>
            <artifactId>zipkin-autoconfigure-ui</artifactId>
        </dependency>
    </dependencies>複製程式碼

在程式的啟動類ZipkinServiceApplication加上@EnableZipkinServer開啟ZipkinServer的功能,加上@EnableEurekaClient註解,啟動Eureka Client。程式碼如下:

@SpringBootApplication
@EnableEurekaClient
@EnableZipkinServer
public class ZipkinServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(ZipkinServerApplication.class, args);
    }
}複製程式碼

在配置檔案application.yml檔案,指定程式名為zipkin-server,埠為9411,服務註冊地址為http://localhost:8761/eureka/。

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
server:
  port: 9411
spring:
  application:
    name: zipkin-server複製程式碼

構建user-service

在主Maven工程下建一個Module工程,取名為user-service,作為應用服務,對外暴露API介面。pom檔案繼承了主Maven工程的pom檔案,並引入了Eureka的起步依賴spring-cloud-starter-eureka,Web起步依賴spring-boot-starter-web,Zipkin的起步依賴spring-cloud-starter-zipkin,程式碼如下:

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka</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>
            <version>RELEASE</version>
        </dependency>
    </dependencies>複製程式碼

在配置檔案applicatiom.yml,指定了程式名為user-service,埠為8762,服務註冊地址為http://localhost:8761/eureka/,Zipkin Server地址為http://localhost:9411。spring.sleuth.sampler.percentage為1.0,即100%的概率將鏈路的資料上傳給Zipkin Server,在預設的情況下,該值為0.1,程式碼如下:

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
server:
  port: 8762
spring:
  application:
    name: user-service
  zipkin:
    base-url: http://localhost:9411
  sleuth:
    sampler:
      percentage: 1.0複製程式碼

在UserController類建一個“/user/hi”的API介面,對外提供服務,程式碼如下:

@RestController
@RequestMapping("/user")
public class UserController {

    @GetMapping("/hi")
    public String hi(){
        return "I'm forezp";
    }
}複製程式碼

最後作為Eureka Client,需要在程式的啟動類UserServiceApplication加上@EnableEurekaClient註解。

構建gateway-service

新建一個名為gateway-service工程,這個工程作為服務閘道器,將請求轉發到user-service,作為Zipkin客戶端,需要將鏈路資料上傳給Zipkin Server,同時它也作為Eureka Client。它在pom檔案除了需要繼承主Maven工程的 pom,還需引入的依賴如下:

<dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-zuul</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>
            <version>RELEASE</version>
        </dependency>

</dependencies>複製程式碼

在application.yml檔案,配置程式名為gateway-service,埠為5000,服務註冊地址為http://localhost:8761/eureka/,Zipkin Server地址為http://localhost:9411,以“/user-api/**”開頭的Uri請求,轉發到服務名為 user-service的服務。配置程式碼如下:

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/

server:
  port: 5000
spring:
  application:
    name: gateway-service
  sleuth:
    sampler:
      percentage: 1.0
  zipkin:
    base-url: http://localhost:9411

zuul:
  routes:
    api-a:
      path: /user-api/**
      serviceId: user-service複製程式碼

在程式的啟動類GatewayServiceApplication,加上@EnableEurekaClient註解開啟Eureka Client,加上@EnableZuulProxy註解,開啟Zuul代理功能。程式碼如下:

@SpringBootApplication
@EnableZuulProxy
@EnableEurekaClient
public class GatewayServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(GatewayServiceApplication.class, args);
    }
}複製程式碼

專案演示

完整的專案搭建完畢,依次啟動eureka-server、zipkin-server、user-service、gateway-service。在瀏覽器上訪問http://localhost:5000/user-api/user/hi,瀏覽器顯示:

I'm forezp

訪問http://localhost:9411,即訪問Zipkin的展示介面,介面顯示如圖1所示:

image.png
image.png

這個介面主要用來查詢服務的呼叫情況,可以根據服務名、開始時間、結束時間、請求消耗的時間等條件來查詢。點選“Find Trackes”按鈕,介面如圖所示。從圖可知服務的呼叫情況,比如服務呼叫時間、服務的消耗時間,服務呼叫的鏈路情況。

image.png
image.png

點選Dependences按鈕,可以檢視服務的依賴關係,在本案例中,gateway-service將請求轉發到了user-service,它們的依賴關係如圖:

image.png
image.png

怎麼在鏈路資料中新增自定義資料

現在需要實現這樣一個功能,需要在鏈路資料中加上操作人。這需要在gateway-service上實現。建一個ZuulFilter過濾器,它的型別為“post”,order為900,開啟攔截。在攔截邏輯方法裡,通過Tracer的addTag方法加上自定義的資料,比如本案例中加入了鏈路的操作人。另外也可以在這個過濾器中獲取當前鏈路的traceId資訊,traceId作為鏈路資料的唯一標識,可以儲存在log日誌中,方便後續查詢。

@Component
public class LoggerFilter extends ZuulFilter {

    @Autowired
    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.print(tracer.getCurrentSpan().traceIdString());
        return null;
    }
}複製程式碼

使用spring-cloud-starter-stream-rabbit進行鏈路通訊

在上述的案例中,最終gateway-service收集的資料,是通過Http上傳給zip-server的,在Spring Cloud Sleuth中支援訊息元件來通訊的,在這一小節使用RabbitMQ來通訊。首先來改造zipkin-server,在pom檔案將zipkin-server的依賴去掉,加上spring-cloud-sleuth-zipkin-stream和spring-cloud-starter-stream-rabbit,程式碼如下:


    <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-sleuth-zipkin-stream</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
        </dependency>複製程式碼

在application.yml配置上RabbitMQ的配置,包括host、埠、使用者名稱、密碼,如下:

spring:
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest複製程式碼

在程式的啟動類ZipkinServerApplication上@EnableZipkinStreamServer註解,開啟ZipkinStreamServer。程式碼如下:

@SpringBootApplication
@EnableEurekaClient
@EnableZipkinStreamServer
public class ZipkinServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(ZipkinServerApplication.class, args);
    }
}複製程式碼

現在來改造下Zipkin Client(包括gateway-service、user-service),在pom檔案中將spring-cloud-starter-zipkin以來改為spring-cloud-sleuth-zipkin-stream和spring-cloud-starter-stream-rabbit,程式碼如下:


<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-sleuth-zipkin-stream</artifactId>

        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
        </dependency>複製程式碼

同時在applicayion.yml檔案加上RabbitMQ的配置,同zipkin-server工程。

這樣,就將鏈路的上傳資料從Http改了為用訊息代元件RabbitMQ。

將鏈路資料儲存在Mysql資料庫

在上述的例子中,Zipkin Server是將資料儲存在記憶體中,一旦程式重啟,之前的鏈路資料全部丟失,那麼怎麼將鏈路資料儲存起來呢?Zipkin支援Mysql、Elasticsearch、Cassandra儲存。這一小節講述用Mysql儲存,下一節講述用Elasticsearch儲存。

首先,在zipkin-server工程加上Mysql的連線依賴mysql-connector-java,JDBC的起步依賴spring-boot-starter-jdbc,程式碼如下:

<dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>複製程式碼

在配置檔案application.yml加上資料來源的配置,包括資料庫的Url、使用者名稱、密碼、連線驅動,另外需要配置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:
    type: mysql複製程式碼

另外需要在Mysql資料庫中初始化資料庫指令碼,資料庫指令碼地址:github.com/openzipkin/…

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`);複製程式碼

將鏈路資料儲存在ElasticSearch

使用Mysql儲存鏈路資料,在併發高的情況下,顯然不合理,這時可以選擇使用ElasticSearch儲存。讀者需要自行安裝ElasticSearch、Kibana(下一小節使用),下載地址為www.elastic.co/products/el…

安裝的過程可以參考我的這篇文章:blog.csdn.net/forezp/arti…

本小節的案例在上上小節的案例的基礎上進行改造。首先在pom檔案,加上zipkin的依賴和zipkin-autoconfigure-storage-elasticsearch-http的依賴,程式碼如下:


        <dependency>
            <groupId>io.zipkin.java</groupId>
            <artifactId>zipkin</artifactId>
            <version>1.28.0</version>
        </dependency>
        <dependency>
            <groupId>io.zipkin.java</groupId>
            <artifactId>zipkin-autoconfigure-storage-elasticsearch-http</artifactId>
            <version>1.28.0</version>
        </dependency>複製程式碼

在application.yml檔案加上Zipkin的配置,配置了zipkin的儲存型別為elasticsearch,使用的StorageComponent為elasticsearch。然後需要配置elasticsearch,包括hosts,可以配置多個,用“,”隔開;index為zipkin等,具體配置如下:


zipkin:
  storage:
    type: elasticsearch
    StorageComponent: elasticsearch
    elasticsearch:
      cluster: elasticsearch
      max-requests: 30
      index: zipkin
      index-shards: 3
      index-replicas: 1
      hosts: localhost:9200複製程式碼

在kibana上展示

上一小節講述瞭如何將鏈路資料儲存在ElasticSearch,ElasticSearch可以和Kibana結合,將鏈路資料展示在 Kibana上。安裝完Kibana,並啟動,它預設會向本地的9200埠的ElasticSearch讀取資料,它預設的埠為5601。訪問http://localhost:5601,顯示的介面如下:

image.png
image.png

在上述的介面點選"Management"按鈕,然後點選“Add New”,新增一個index,在上節我們在ElasticSearch中寫入鏈路資料的index配置為“zipkin”,那麼在介面填寫為“zipkin-*”,點選“Create”按鈕。

image.png
image.png

建立完index之後,點選Discover,就可以在介面上展示鏈路資料了。

image.png
image.png

原始碼下載

最原始的工程:

github.com/forezp/Spri…

採用RabbitMq通訊的工程:

github.com/forezp/Spri…

採用Mysql儲存的工程:

github.com/forezp/Spri…

採用ES儲存的工程:

github.com/forezp/Spri…

參考資料

cloud.spring.io/spring-clou…

github.com/openzipkin/…

關注我的公眾號

精彩內容不能錯過!

forezp.jpg
forezp.jpg

相關文章