服務鏈路追蹤-Sleuth :解決分佈式部署下最頭疼的溯源問題

一、服務調用鏈追蹤

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

二、核心功能和體系架構

  Sleuth的意思是大偵探,顧名思義,偵探就是查找信息蒐集線索,順着線索鏈找到所有上下游的關聯,這恰好正是Seluth在Spring Cloud中的工作

1、核心功能

藉助Sleuth的鏈路追蹤能力,我們還可以完成一些其他的任務,比如說:

  • 線上故障定位:結合Tracking ID尋找上下游鏈路中所有的日誌信息(這一步還需要藉助一些其他開源組件,後面會有這部分的Demo)
  • 依賴分析梳理:梳理上下游依賴關係,理清整個系統中所有微服務之間的依賴關係
  • 鏈路優化:比如說目前我們有三種途徑可以導流到下單接口,通過對鏈路調用情況的統計分析,我們可以識別出轉化率最高的業務場景,從而爲以後的產品設計提供指導意見。
  • 性能分析:梳理各個環節的時間消耗,找到性能瓶頸,爲性能優化、軟硬件資源調配指明方向

2、設計理念

  • 無業務侵入:如果說接入某個監控組件還需要改動業務代碼,那麼我們認爲這是一個“高侵入性”的組件。Sleuth在設計上秉承“低侵入”的理念,不需要對業務代碼做任何改動,即可靜默接入鏈路追蹤功能
  • 高性能:一般認爲在代碼里加入完善的log(10行代碼對應2條log)會對降低5%左右接口性能(針對非異步log框架),而通過鏈路追蹤技術在log裏做埋點多多少少也會影響性能。Sleuth在埋點過程中力求對性能影響降低到最小,同時還提供了“採樣率配置”來進一步降低開銷(比如說開發人員可以設置只對20%的請求進行採樣)

3、數據埋點

  每一個微服務都有自己的Log組件(slf4j,lockback等各不相同),當我們集成了Sleuth之後,它便會將鏈路信息傳遞給底層Log組件,同時Log組件會在每行Log的頭部輸出這些數據,這個埋點動作主要會記錄兩個關鍵信息:

  • 鏈路ID(Trace ID):當前調用鏈的唯一ID,在這次調用請求開始到結束的過程中,所有經過的節點都擁有一個相同的鏈路ID
  • 單元ID(Event ID): 在一次鏈路調用中會訪問不同服務器節點上的服務,每一次服務調用都相當於一個獨立單元,也就是說會有一個獨立的單元ID。同時每一個獨立單元都要知道調用請求來自哪裏(就是對當前服務發起直接調用的那一方的單元ID,我們記爲Parent ID)

4、Sleuth與Log系統集成原理

  我們需要把鏈路追蹤信息加入到業務Log中,這些業務Log是我們研發人員寫在具體服務裏的,而不是Sleuth單獨打印的log,因此Sleuth需要找到一個合適的切入點,讓底層Log組件可以獲取鏈路信息,並且我們的業務代碼還不需要做任何改動。
  如果有對Log框架做過深度定製的同學可能一下就能想到實現方式,就是使用MDC + Format Pattern的方式輸出信息,我們先來看一下Log組件打印信息到文件的過程:
在這裏插入圖片描述
  當我們使用"log.info"打印日誌的時候,Log組件會將“寫入”動作封裝成一個LogEvent事件,而這個事件的具體表現形式由Log Format和MDC共同控制,Format決定了Log的輸出格式,而MDC決定了輸出什麼內容。

1)Log Format Pattern

Log組件定義了日誌輸出格式,這和我們平時使用“String.format”的方式差不多,集成了Sleuth後的Log輸出格式是下面這個樣子:

%5p [sleuth-traceA,%X{X-B3-TraceId:-},%X{X-B3-SpanId:-},%X{X-Span-Export:-}]

同學們發現上面有幾個X開頭的佔位符,這就是我們需要寫入Log的鏈路追蹤信息

2)MDC

  MDC是通過InheritableThreadLocal來實現的,它可以攜帶當前線程的上下文信息。它的底層是一個Map結構,存儲了一系列Key-Value的值。Sleuth就是藉助Spring的AOP機制,在方法調用的時候配置了切面,將鏈路追蹤數據加入到了MDC中,這樣在打印Log的時候,就能從MDC中獲取這些值,填入到Log Format中的佔位符裏。
  由於MDC基於InheritableThreadLocal而不是ThreadLocal實現,因此假如在當前線程中又開啓了新的子線程,那麼子線程依然會保留父線程的上下文信息。

5、Sleuth數據結構

  • Trace:它就是從頭到尾貫穿整個調用鏈的ID,我們叫它Trace ID,不管調用鏈路中途訪問了多少服務節點,在每個節點的log中都會打印同一個Trace ID
  • Span:它標識了Sleuth下面一個基本的工作單元,每個單元都有一個獨一無二的ID。比如服務A發起對服務B的調用,這個事件就可以看做一個獨立單元,生成一個獨立的ID
      Span不單單只是一個ID,它還包含一些其他信息,比如時間戳,它標識了一個事件從開始到結束經過的時間,我們可以用這個信息來統計接口的執行時間。每個Span還有一系列特殊的“標記”,也就是接下來要介紹的“Annotation”,它標識了這個Span在執行過程中發起的一些特殊事件。

1)Annotation標記

一個Span可以包含多個Annotation,每個Annotation表示一個特殊事件,比如:

  • cs Client Sent:客戶端發送了一個調用請求
  • sr Server Received:服務端收到了來自客戶端的調用
  • ss Server Sent:服務端將Response發送給客戶端
  • cr Client Received:客戶端收到了服務端發來的Response

  每個Annotation同樣有一個時間戳字段,這樣我們就能分析一個Span內部每個事件的起始和結束時間。這裏我選取了Spring Cloud官網的一張圖來展示Trace、Span和Annotation的關係。

2)服務節點間的ID傳遞

  我們知道了Trace ID和Span ID,眼下的問題就是如何在不同服務節點之間傳遞這些ID。我想這一步大家很容易猜到是怎麼做的,因爲在Eureka的服務治理下所有調用請求都是基於HTTP的,那我們的鏈路追蹤ID也一定是HTTP請求中的一部分。可是把ID加在HTTP哪裏好呢?Body裏可以嗎?NoNoNo,一來GET請求壓根就沒有Body,二來加入Body還有可能影響後臺服務的反序列化。那加在URL後面呢?似乎也不妥,因爲某些服務組件對URL的長度可能做了限制(比如Nginx可以設置最大URL長度)。
  那剩下的只有Header了!Sleuth正是通過Filter向Header中添加追蹤信息,我們來看下面表格中Header Name和Trace Data的對應關係:

HTTP Header Name Trace Data
X-B3-TraceId Trace ID 鏈路全局唯一ID
X-B3-SpanId Span ID 當前Span的ID
X-B3-ParentSpanId Parent Span ID 前一個Span的ID
X-Span-Export Can be exported for sampling or not 是否可以被採樣

三、整合Sleuth追蹤調用鏈路

1、創建Sleuth項目

1)創建一個模塊命名爲sleuth-traceA,修改pom文件

<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>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <!--sleuth-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-sleuth</artifactId>
    </dependency>
</dependencies>

2)修改啓動文件

@EnableDiscoveryClient
@SpringBootApplication
public class SleuthTraceA{
    public static void main(String[] args){
        new SpringApplicationBuilder(SleuthTraceA.class)
                .web(WebApplicationType.SERVLET)
                .run(args);
    }
    
    @LoadBalanced
    @Bean
    public RestTemplate lb(){
        return new RestTemplate();
    }
}

3)創建配置文件

spring.application.name=sleuth-traceA
server.port=62000

eureka.client.serviceUrl.defaultZone=http://localhost:20000/eureka/

logging.file=${spring.application.name}.log

#日誌採樣率, 1:所有的日誌都會被採樣
spring.sleuth.sampler.probability=1

info.app.name=sleuth-traceA
info.app.description=test

management.security.enabled=false
management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always

4)在resources中添加日誌配置文件logback-spring.xml

<configuration>
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
    
    <springProperty scope="context" name="springAppName" source="spring.application.name"/>
    <!--日誌輸出位置-->
    <property name="LOG_FILE" value="${BUILD_FOLDER:-build}/${springAppName}"/>
    <!--日誌輸出格式-->
    <property name="CONSOLE_LOG_PATTERN" value="%clr(%d{HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}" />

    <appender name="consoleLog" class="ch.qos.logback.core.ConsoleAppender">
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>INFO</level>
        </filter>
        <encoder>
            <pattern>${CONSOLE_LOG_PATTERN}</pattern>
            <charset>utf8</charset>
        </encoder>
    </appender>
    <root level="DEBUG">
        <appender-ref ref = "consoleLog"/>
    </root>
</configuration>

5)編寫controller

//lombok的註解  用來打印日誌,用其他的方式打印日誌也是一樣的
@Slf4j
@RestController
public class ControllerA{
    @Autowired
    private RestTemplate restTemplate;
    
    @GetMapping(value = "/traceA")
    public String traceA(){
        log.info("--------Trace A");
        return restTemplate.getForEntity("http://sleuth-traceB/traceB",String.class).getBody();
    }
}

6)創建一個新的模塊sleuth-traceB,和之前的步驟一樣

1.修改以下配置文件中的3個參數

spring.application.name=sleuth-traceB
server.port=62001

info.app.name=sleuth-traceB

2.修改一下Controller

//lombok的註解  用來打印日誌,用其他的方式打印日誌也是一樣的
@Slf4j
@RestController
public class ControllerB{
    @Autowired
    private RestTemplate restTemplate;
    
    @GetMapping(value = "/traceB")
    public String traceB(){
        log.info("--------Trace B");
        return "traceB";
    }
}

7)測試

1.啓動服務發現中心(示例中沒有需要自己配置下)
2.啓動traceB和traceA
3.調用/traceA接口參看日誌

四、Zipkin

  Zipkin是一套分佈式實時數據追蹤系統,它主要關注的是時間維度的監控數據,比如某個調用鏈路下各個階段所花費的時間,同時還可以從可視化的角度幫我們梳理上下游系統之間的依賴關係。
  Sleuth爲什麼需要一個搭檔?大家難道沒發現Sleuth空有一身本領,可是沒個頁面可以show出來嗎?而且Sleuth似乎只是自娛自樂在log裏埋點,卻沒有一個匯聚信息的能力,不方便對整個集羣的調用鏈路進行分析。Sleuth目前的情形就像Hystrix一樣,也需要一個類似Turbine的組件做信息聚合+展示的功能。在這個背景下,Zipkin就是一個不錯的選擇。

1、Zipkin的核心功能

Zipkin的主要作用是收集Timing維度的數據,以供查找調用延遲等線上問題。所謂Timing其實就是開始時間+結束時間的標記,有了這兩個時間信息,我們就能計算得出調用鏈路每個步驟的耗時。Zipkin的核心功能有以下兩點

  • 數據收集:聚合客戶端數據
  • 數據查找:通過不同維度對調用鏈路進行查找

  Zipkin分爲服務端和客戶端,服務端是一個專門負責收集數據、查找數據的中心Portal,而每個客戶端負責把結構化的Timing數據發送到服務端,供服務端做索引和分析。這裏我們重點關注一下“Timing數據”到底用來做什麼,前面我們說過Zipkin主要解決調用延遲情況的線上排查,它通過收集一個調用鏈上下游所有工作單元的獨立用時,Zipkin就能知道每個環節在服務總用時中所佔的比重,再通過圖形化界面的形式,讓開發人員知道性能瓶頸出在哪裏。
  Zipkin提供了多種維度的查找功能用來檢索Span的耗時,最直觀的是通過Trace ID查找整個Trace鏈路上所有Span的前後調用關係和每階段的用時,還可以根據Service Name或者訪問路徑等維度進行查找

2、Zipkin的組件

  • Collector:很多人以爲Collector是一個客戶端組件,其實它是Zipkin Server的守護進程,用來驗證客戶端發送來的鏈路數據,並在存儲結構中建立索引。守護進程就是指一類用於執行特定任務的後臺進程,它獨立於Zipkin Server的控制終端,一直等待接收客戶端數據。
  • Storage:Zipkin支持ElasticSearch和MySQL等存儲介質用來保存鏈路信息,本章demo中採用默認的Cassandra作爲存儲方式
  • Search Engine:提供基於JSON API的接口來查找信息
  • Dashboard:一個大盤監控頁面,後臺調用Search Engine來獲取展示信息。大家如果本地啓動Zipkin會每次刷新主頁後系統日誌會打印Error信息,這個是Zipkin的一個小問題,直接跳過即可

3、Zipkin與Sleuth集成,日誌分析和鏈路追蹤

1)創建一個模塊,起名zipkin-server,並修改pom文件

<dependencies>
    <!--後端工具包-->
    <dependency>
        <groupId>io.zipkin.java</groupId>
        <artifactId>zipkin-server</artifactId>
        <version>2.8.4</version>
    </dependency>
    <!--用於前端展示的包-->
    <dependency>
        <groupId>io.zipkin.java</groupId>
        <artifactId>zipkin-autoconfigure-ui</artifactId>
        <version>2.8.4</version>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <!--指定啓動方法-->
                <mainClass>>com.test.spring.ZipkinApplication</mainClass>
            </configuration>
            <executions>
                <execution>
                    <goals>
                        <!-- zipkin是可以直接使用他自己的jar部署的,我們這裏需要做一些修改,然後再次打包 -->
                        <goal>repackage</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

2)修改啓動類

//選哪個沒有被標記爲Deprecated的
@EnableZipkinServer
@SpringBootApplication
public class ZipkinApplication{
    public static void main(String[] args){
        new SpringApplicationBuilder(ZipkinApplication.class)
                .web(WebApplicationType.SERVLET)
                .run(args);
    }
}

3)創建配置文件

spring.application.name=zipkin-server
server.port=62100

#允許bean重載
spring.main.allow-bean-definition-overriding=true
management.metrics.web.server.auto-time-requests=false

4)訪問zipkin地址

localhost:62100/zipkin/

5)修改之前寫的sleuth-traceA和B,修改pom文件

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

6)修改之前寫的sleuth-traceA和B,修改配置文件

#zipkin的地址
spring.zipkin.base-url=http://localhost:62100
#使用http上傳數據到zipkin
#因爲有可能後引入bus依賴導入了rabbitmq的依賴,zipkin會自動切換到RabbitMq上
spring.zipkin.sender.type=web

五、Sleuth集成ELK實現日誌搜索

1)使用docker快速搭建ELK

  • ElasticSearch:存儲Log信息,並提供搜索功能
  • Logstash:log信息收集過濾
  • Kibana:Log信息的查詢報表

1.使用docker下載elk鏡像(很慢需要耐心)

docker pull sebp/elk

2.創建Docker容器(第一次使用的時候才需要創建,啓動很慢 5-10分鐘)

        #kibana的端口   ElasticSearch端口   LogStash端口                                                                 容器名稱   啓動的鏡像
docker run -p 5601:5601 -p 9200:9200 -p5044:5044 -e ES_MIN_MEM=128m -e ES_MAX_MEM=1025m  -it --name elk sebp/elk

3.再次啓動的命令docker容器(啓動很慢 5-10分鐘)
docker start elk
4. 進入docker容器:

docker exec -it elk /bin/bash

5.修改配置文件
配置文件位置: /etc/logstash/conf.d/02-beats-input.conf
將內容全部刪除,替換成下面的配置

input{
    tcp{
        port =>5044
        codec => json_lines
    }
}
output{
    elasticsearch{
        hosts => ["localhost:9200"]
    }
}

6.重啓docker容器(啓動很慢 5-10分鐘)
docker restart elk
7.訪問Kibana
http://localhost:5601/

2)Sleuth日誌推送給ELK

1.修改之前寫的sleuth-traceA和B,修改pom文件

<!-- Logstash for ELK -->
<dependency>
    <groupId>net.logstash.logback</groupId>
    <artifactId>logstash-logback-encoder</artifactId>
    <version>5.2</version>
</dependency>

2.修改日誌配置文件logback-spring.xml,下面是完整的配置,添加logstashLog了

<configuration>
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
    
    <springProperty scope="context" name="springAppName" source="spring.application.name"/>
    <!--日誌輸出位置-->
    <property name="LOG_FILE" value="${BUILD_FOLDER:-build}/${springAppName}"/>
    <!--日誌輸出格式-->
    <property name="CONSOLE_LOG_PATTERN" value="%clr(%d{HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}" />

    <appender name="consoleLog" class="ch.qos.logback.core.ConsoleAppender">
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>INFO</level>
        </filter>
        <encoder>
            <pattern>${CONSOLE_LOG_PATTERN}</pattern>
            <charset>utf8</charset>
        </encoder>
    </appender>

    <!-- 遠程把日誌以json格式輸出給logstash -->
    <appender name="logstashLog" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
        <!-- logstash的地址 -->
        <destination>127.0.0.1:5044</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:-}",
                        "exportable":"%X{X-SpanId-Export:-}",
                        "pid":"${PID:-}",
                        "thread":"%thread",
                        "class":"%logger{40}",
                        "rest":"%message"
                        }
                    </pattern>
                </pattern>
            </providers>
        </encoder>
    </appender>
    <root level="DEBUG">
        <appender-ref ref = "consoleLog"/>
        <appender-ref ref = "logstashLog"/>
    </root>
</configuration>

3.配置完成,發起一個調用,查看Kibana

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