SpringCloud(八)鏈路追蹤Sleuth

SpringCloud(八)鏈路追蹤Sleuth

在分佈式系統中,完成一個外部請求可能需要多個應用之間相互協作,形成複雜的調用鏈路。而一旦出現問題,更是難以定位問題,也難以直觀地獲取到各個服務之間的依賴關係。Spring Cloud Sleuth的出現正是爲了實現分佈式系統的鏈路追蹤。

Sleuth的基本概念

Sleuth借鑑了Google Dapper的術語。
有以下幾個要素:
Span:基本的工作單元。比如發送一個RPC以及返回一個RPC應答分別是一個新的Span。Span是根據一個唯一的64-bit ID和所屬Trace的64-bit ID區分的。
Trace:一系列的Span組成的樹結構的圖
Annotation:用來記錄事件存在的時間。

  • cs - Client Sent - client端發送一個請求。Span的開始。
  • sr - Server Received - server端接收到請求,並且準備處理
  • ss - Server Sent - server端請求處理完成
  • cr - Client Received - client端接收到server端的響應結果。Span的結束。
    在這裏插入圖片描述
    上圖中七種顏色分別代表了從A-G7個Span。
    將他們轉換爲父子關係圖,如下圖所示:
    在這裏插入圖片描述

引入Sleuth

我們對web-app-serviceuser-servicesms-service這三個服務增加Sleuth的支持。
pom.xml文件中引入spring-cloud-starter-sleuth依賴。

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

application.yml文件中設置

spring:
  sleuth:
    sampler:
      percentage: 1.0

表示設置Sleuth的採樣率爲100%,即全部採樣。
我們在web-app-service新建一個用戶註冊的接口,分別請求user-servicesms-service的接口,涉及到的接口增加Log輸出。

@RequestMapping("/register")
public String register() {
    logger.info("user is registering");
    userService.register();
    smsService.sendRgister();
    return "success";
}

請求web-app-service/user/register接口(http://localhost:8101/user/register),返回成功後可以在三個服務的控制檯分別看到的日誌打印。

# web-app-service
2018-10-10 16:51:11.454  INFO [web-app-service,dd3aaab11dddbca1,dd3aaab11dddbca1,true] 3760 --- [nio-8101-exec-9] c.c.s.web.controller.UserController      : user is registering
# user-service
2018-10-10 16:51:11.463  INFO [user-service,dd3aaab11dddbca1,436fe40b80a2bdb1,true] 8608 --- [nio-8002-exec-9] c.c.s.user.controller.UserController     : invoke user register endpoint
# sms-service
2018-10-10 16:51:11.983  INFO [sms-service,dd3aaab11dddbca1,ff559b20a835e679,true] 5920 --- [nio-8001-exec-3] c.c.s.controller.SMSController           : send sms to registered user

注意日誌的輸出分別代表[appname,traceId,spanId,exportable]

exportable:表示日誌是否應該導入到Zipkin中。

Sleuth集成ELK實現日誌分析

對於分佈式系統,我們希望將日誌納入統一管理。ELK(Elasticsearch+Logstash+Kibana)是日誌管理分析的一系列組件,實現了日誌收集、檢索和可視化查詢。我們通過logbacklogstash-logback-encoder將日誌按照指定格式傳輸給Logstash,經過處理後傳輸給Elasticsearch,再通過Kibana的web界面進行日誌查詢。
在Logstash上設置輸入和輸出,並重啓Logstash服務。在輸入配置裏,tcp.host就是我們的Logstash所在IP,tcp.port是我們使用TCP傳輸日誌時的指定端口。輸出配置我們的Elasticsearch IP和端口,並指定索引(不能使用大寫字母)。

input {
  tcp {
    mode => "server"
    host => "192.168.126.128"
    port => 3333
  }
}
filter {
}
output {
  elasticsearch {
    action => "index"
    hosts  => "192.168.126.128:9200"
    index  => "spring_cloud_demo_logs"
  }
}

logback日誌配置文件logback-spring.xmldestination需要配置上面的Logstash IP和TCP端口,並指定編碼器LoggingEventCompositeJsonEncoder及格式。

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/><springProperty scope="context" name="springAppName" source="spring.application.name"/>
    <!-- Example for logging into the build folder of your project -->
    <property name="LOG_FILE" value="${BUILD_FOLDER:-build}/${springAppName}"/><!-- You can override this to have a custom pattern -->
    <property name="CONSOLE_LOG_PATTERN"
              value="%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"/>
    <!--Logstash 服務器地址和TCP端口-->
    <property name="LOGSTASH_TCP_DESTINATION" value="192.168.126.128:3333" />

    <!-- Appender to log to console -->
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <!-- Minimum logging level to be presented in the console logs-->
            <level>DEBUG</level>
        </filter>
        <encoder>
            <pattern>${CONSOLE_LOG_PATTERN}</pattern>
            <charset>utf8</charset>
        </encoder>
    </appender>

    <!-- Appender to log to file --><appender name="flatfile" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_FILE}</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_FILE}.%d{yyyy-MM-dd}.gz</fileNamePattern>
            <maxHistory>7</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>${CONSOLE_LOG_PATTERN}</pattern>
            <charset>utf8</charset>
        </encoder>
    </appender><appender name="logstash"
              class="net.logstash.logback.appender.LogstashTcpSocketAppender">
        <destination>${LOGSTASH_TCP_DESTINATION}</destination>
        <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
            <providers>
                <timestamp>
                    <timeZone>UTC</timeZone>
                </timestamp>
                <pattern>
                    <pattern>
                        {
                        "severity": "%level",
                        "service": "${springAppName:-}",
                        "trace": "%X{X-B3-TraceId:-}",
                        "span": "%X{X-B3-SpanId:-}",
                        "parent": "%X{X-B3-ParentSpanId:-}",
                        "exportable": "%X{X-Span-Export:-}",
                        "pid": "${PID:-}",
                        "thread": "%thread",
                        "class": "%logger{40}",
                        "rest": "%message"
                        }
                    </pattern>
                </pattern>
            </providers>
        </encoder>
    </appender><root level="INFO">
        <appender-ref ref="console"/>
        <!-- uncomment this to have also JSON logs -->
        <appender-ref ref="logstash"/>
        <!--<appender-ref ref="flatfile"/>-->
    </root>
</configuration>

請求web-app-service/user/register接口(http://localhost:8101/user/register),自動向ELK發送日誌。
在Kibana web界面創建索引
在這裏插入圖片描述
採集到的日誌就是我們各個服務中的輸出。
在這裏插入圖片描述
根據某一個traceId,對e7df1589e5a2a73f進行搜索顯示整個調用鏈路的日誌信息。
在這裏插入圖片描述
展開某一條日誌後可以看到其詳細信息
在這裏插入圖片描述

使用Sleuth和Zipkin實現鏈路追蹤

Zipkin作爲分佈式鏈路跟蹤系統,提供了可視化頁面極大地方便了對請求的監控,更容易明確服務之間的依賴。新建一個zipkin-server服務作爲鏈路追蹤服務。我們需要將Sleuth採集的日誌信息通過Http或者Spring Cloud Stream方式傳輸給Zipkin服務端。

通過Http傳輸Sleuth信息

zipkin-serverpom.xml需要引入zipkin-serverzipkin-autoconfigure-ui依賴。

<!--註冊到Eureka,可以自動發現Zipkin服務-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>

<!--使用Http方式獲取鏈路信息-->
<dependency>
    <groupId>io.zipkin.java</groupId>
    <artifactId>zipkin-server</artifactId>
</dependency>
<!--zipkin ui頁面-->
<dependency>
    <groupId>io.zipkin.java</groupId>
    <artifactId>zipkin-autoconfigure-ui</artifactId>
</dependency>

將服務端口設置爲8400並註冊到eureka,保證其他服務能夠發現Zipkin服務。
使用@EnableZipkinServer啓用Zipkin服務。

@EnableEurekaClient
@EnableZipkinServer
@SpringBootApplication
public class ZipkinApplicationStarter {

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

啓動成功後我們訪問http://localhost:8400就能直接訪問Zipkin服務的首頁了。
接着改造我們的web-app-serviceuser-servicesms-service服務使其支持使用Http向Zipkin傳輸鏈路信息。pom.xml去掉之前的spring-cloud-starter-sleuth依賴增加spring-cloud-sleuth-zipkin(包含spring-cloud-starter-sleuth)。

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

然後需要在application.yml中配置Zipkin服務的地址和Sleuth的採樣率。我們可以使用Eureka發現Zipkin服務或者使用url配置Zipkin的路徑。Sleuth的採樣率繼續使用1,保證所有的請求都會被收集。

spring:
  zipkin:
    locator:
      discovery:
        enabled: true
    #base-url: http://localhost:8400
  sleuth:
    sampler:
      percentage: 1.0

請求web-app-service/user/register接口(http://localhost:8101/user/register
此時我們打開Zipkin首頁,查看收集到的信息。圖中藍色的都是正常的請求,紅色則是出現錯誤的請求。
在這裏插入圖片描述
打開一個正常的請求可以看到請求的服務接口及請求時間。如圖我們可以看出web-app-service請求了user-service之後再請求了sms-service
在這裏插入圖片描述
點開user-serviceSpan顯示請求狀況
在這裏插入圖片描述
關閉sms-service服務,並重新發起請求。觀察異常請求鏈路,發現sms-service關閉後,請求異常後又重試了6次,最終失敗。
在這裏插入圖片描述
點擊下面紅色的Span查看錯誤信息。從日誌中不難發現出現錯誤的原因是/sms/sendRegister超時了。
在這裏插入圖片描述
點擊Dependencies頁面顯示服務之間的依賴關係。
在這裏插入圖片描述
點開某個服務可以看到被哪些應用使用
在這裏插入圖片描述

使用SpringCloud Sleuth Stream傳輸Sleuth信息

使用Http方式可以很容易的實現Sleuth信息的上傳,但是當服務訪問量較高時Http的勢必會影響服務性能。再者,當我們的Zipkin服務端出現異常,這些Sleuth信息也不能正常上傳,影響服務的監控。這時候使用消息隊列就能輕鬆的解決高吞吐量和異步解耦這兩個問題。
Spring Cloud Stream是一個構建基於消息微服務的框架,通過簡單的註解方式就能實現消息的發送。目前已經支持Kafka和RabbitMQ。Spring Cloud Sleuth Stream正是利用Spring Cloud Stream實現使用消息隊列傳輸Sleuth信息。
下面我們演示使用RabbitMQ作爲中間件,實現Stream傳輸Sleuth信息。
zipkin-server增加Spring Cloud Sleuth Stream的依賴。

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

在啓動類上增加@EnableZipkinStreamServer註解。

@EnableEurekaClient
//@EnableZipkinServer  //Http方式傳輸sleuth信息
@EnableZipkinStreamServer  //Stream方式傳輸Sleuth信息
@SpringBootApplication
public class ZipkinApplicationStarter {

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

application.yaml文件中增加RabbitMQ的配置

spring:
  rabbitmq:
    host: 172.16.4.39
    username: rabbitmq
    password: rabbitmq

服務端完成,啓動之。
我們發現RabbitMQ的中已經有了一個叫做sleuth的exchange。
在這裏插入圖片描述
web-app-serviceuser-servicesms-service這三個服務中增加Sleuth Stream採集支持。

	<dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-sleuth-stream</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-sleuth</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-stream-binder-rabbit</artifactId>
    </dependency>

同樣地,增加RabbitMQ地址、用戶名和密碼配置。這三個應用集成配置中心的時候已經進行了配置。
啓動這三個服務,繼續請求我們的測試接口,觀察到RabbitMQ的web界面有消息的生產和消費,打開Zipkin界面也能看到調用鏈路信息。

信息存儲方式

默認地,Zipkin將鏈路信息存儲在內存中。在正式使用中這種做法是不可取的,隨着數據的堆積會大量消耗內存,一旦服務重啓所有的監控信息都將隨之消失。我們需要將鏈路信息進行持久化。
打開io.zipkin.java:zipkin-serverpom依賴,我們可以看到Zipkin支持的存儲方式有mysql,cassandra,elasticsearch。
在這裏插入圖片描述
下面演示以mysql作爲信息的存儲方式。我們在Zipkin服務的pom.xml文件中引入mysql存儲支持

  <!--使用mysql存儲Span信息-->
   <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-jdbc</artifactId>
   </dependency>
   <dependency>
       <groupId>mysql</groupId>
       <artifactId>mysql-connector-java</artifactId>
   </dependency>
   <dependency>
       <groupId>io.zipkin.java</groupId>
       <artifactId>zipkin-autoconfigure-storage-mysql</artifactId>
   </dependency>

application.yaml中設置數據源、存儲方式以及Zipkin DDL

spring:
  datasource:
    schema: classpath:ysql.sql
    # zipkin-autoconfigure-storage-mysql包中默認包含了HikariDataSource
    type: com.zaxxer.hikari.HikariDataSource
    url: jdbc:mysql://localhost:3306/test
    username: chenxyz
    password: 123
    # Switch this on to create the schema on startup:
    initialize: true
    continueOnError: true

# 設置存儲類型爲mysql
zipkin:
  storage:
    type: mysql

服務啓動的時候會自動生成三張表。
在這裏插入圖片描述
後面我們再傳輸鏈路信息,Zipkin會將Span信息持久化到mysql中。


相關代碼
SpringCloudDemo-Sleuth


參考
Spring Cloud Sleuth - Introduction

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章