一文搞懂基於zipkin的分散式追蹤系統原理與實現

Mr_小白發表於2019-03-01

前言

傳統單機系統在使用過程中,如果某個請求響應過慢或是響應出錯,開發人員可以清楚知道某個請求出了問題,檢視日誌可以定位到具體方法。但是在分散式系統中,倘若客戶端一個請求到達伺服器後,由多個服務協作完成。比如:服務A呼叫服務B,服務B又呼叫服務C和服務D,服務D又呼叫服務E,那麼想要知道是哪個服務處理時間過長或是處理異常導致這個請求響應緩慢或中斷的話,就需要開發人員一個服務接一個服務的去機器上檢視日誌,先定位到出問題的服務,再定位出問題的具體地方。試想一下,隨著系統越來越壯大,服務越來越多,一個請求對應處理的服務呼叫鏈越來越長,這種排查方式何其艱難。為了解決這種問題,便誕生了各種分散式場景中追蹤問題的解決方案,zipkin就是其中之一。

整體結構長啥樣

一個獨立的分散式追蹤系統,客戶端存在於應用中(即各服務中),應具備追蹤資訊生成、採集傳送等功能,而服務端應該包含以下基本的三個功能:

  • 資訊收集:用來收集各服務端採集的資訊,並對這些資訊進行梳理儲存、建立索引。
  • 資料儲存:儲存追蹤資料。
  • 查詢服務:提供查詢請求鏈路資訊的介面。

zipkin 整體結構圖如下:

zipkin 結構圖

zipkin(服務端)包含四個元件,分別是collector、storage、search、web UI。

  • collector 就是資訊收集器,作為一個守護程式,它會時刻等待客戶端傳遞過來的追蹤資料,對這些資料進行驗證、儲存以及建立查詢需要的索引。
  • storage 是儲存元件。zipkin 預設直接將資料存在記憶體中,此外支援使用Cassandra、ElasticSearch 和 Mysql。
  • search 是一個查詢程式,它提供了簡單的JSON API來供外部呼叫查詢。
  • web UI 是zipkin的服務端展示平臺,主要呼叫search提供的介面,用圖表將鏈路資訊清晰地展示給開發人員。

zipkin的客戶端主要負責根據應用的呼叫情況生成追蹤資訊,並且將這些追蹤資訊傳送至zipkin由收集器接收。各語言支援均不同,具體可以檢視zipkin官網,java語言的支援就是brave。上面結構圖中,有追蹤器就是指整合了brave。

基本概念瞭解下

在使用zipkin之前,先了解一下Trace和Span這兩個基本概念。一個請求到達應用後所呼叫的所有服務所有服務組成的呼叫鏈就像一個樹結構(如下圖),我們追蹤這個呼叫鏈路得到的這個樹結構可以稱之為Trace

一文搞懂基於zipkin的分散式追蹤系統原理與實現

在一次Trace中,每個服務的每一次呼叫,就是一個基本工作單元,就像上圖中的每一個樹節點,稱之為span。每一個span都有一個id作為唯一標識,同樣每一次Trace都會生成一個traceId在span中作為追蹤標識,另外再通過一個parentId標明本次呼叫的發起者(就是發起者的span-id)。當span有了上面三個標識後,就可以很清晰的將多個span進行梳理串聯,最終歸納出一條完整的跟蹤鏈路。此外,span還會有其他資料,比如:名稱、節點上下文、時間戳以及K-V結構的tag資訊等等(Zipkin v1核心註解如“cs”和“sr”已被Span.Kind取代,詳情檢視zipkin-api,本文會在入門的demo介紹完後對具體的Span資料模型進行說明)。

具體怎麼追蹤的

追蹤器位於應用程式上,負責生成相關ID、記錄span需要的資訊,最後通過傳輸層傳遞給服務端的收集器。我們首先思考下面幾個問題:

  • 每個span需要的基本資訊何時生成?
  • 哪些資訊需要隨著服務呼叫傳遞給服務提供方?
  • 什麼時候傳送span至zipkin 服務端?
  • 以何種方式傳送span?

一個 span 表示一次服務呼叫,那麼追蹤器必定是被服務呼叫發起的動作觸發,生成基本資訊,同時為了追蹤服務提供方對其他服務的呼叫情況,便需要傳遞本次追蹤鏈路的traceId和本次呼叫的span-id。服務提供方完成服務將結果響應給呼叫方時,需要根據呼叫發起時記錄的時間戳與當前時間戳計算本次服務的持續時間進行記錄,至此這次呼叫的追蹤span完成,就可以傳送給zipkin服務端了。但是需要注意的是,傳送span給zipkin collector不得影響此次業務結果,其傳送成功與否跟業務無關,因此這裡需要採用非同步的方式傳送,防止追蹤系統傳送延遲與傳送失敗導致使用者系統的延遲與中斷。下圖就表示了一次http請求呼叫的追蹤流程(基於zipkin官網提供的流程圖):

一次http請求呼叫的追蹤流程

可以看出服務A請求服務B時先被追蹤器攔截,記錄tag資訊、時間戳,同時將追蹤標識新增進http header中傳遞給服務B,在服務B響應後,記錄持續時間,最終採取非同步的方式傳送給zipkin收集器。span從被追蹤的服務傳送到Zipkin收集器有三種主要的傳送方式:http、Kafka以及Scribe(Facebook開源的日誌收集系統)。

1分鐘安裝zipkin

上文對基於zipkin實現分散式追蹤系統的原理做了全面的說明,這裡簡單介紹一下zipkin的安裝方法,下載jar包,直接執行。簡單粗暴,但要注意必須jdk1.8及以上。其餘兩種安裝方式見官方介紹。

wget -O zipkin.jar `https://search.maven.org/remote_content?g=io.zipkin.java&a=zipkin-server&v=LATEST&c=exec`
java -jar zipkin.jar
複製程式碼

啟動成功後,開啟瀏覽器訪問zipkin的webUI,輸入http://ip:9411/,顯示頁面如下圖。具體使用後面介紹。

一文搞懂基於zipkin的分散式追蹤系統原理與實現

寫個Demo用起來(Spring Boot整合zipkin)

java版客戶端 Brave的官方文件很少,都在github裡。小白當時找的那叫個頭疼啊,網上各路大神寫的部落格中的程式碼你扒下來換最新的依賴後都會顯示那些類被標記為過時,不建議使用。

  • brave 原始碼地址:github.com/openzipkin/…
  • 官方demo地址:github.com/openzipkin/…
  • 友情提示:本節程式碼較多,註釋還算詳細,介紹文字偏少。
    小白寫的demo結構如下圖,分別建立了service1、service2、service3三個boot應用,將brave整合部分單獨作為一個module,這樣可以嵌入服務中複用,避免重複編碼。
    一文搞懂基於zipkin的分散式追蹤系統原理與實現

maven 依賴(zipkin_client)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.ycg</groupId>
    <artifactId>zipkin_client</artifactId>
    <version>1.0-SNAPSHOT</version>
    <name>zipkin_client</name>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>6</source>
                    <target>6</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
    <packaging>jar</packaging>

    <properties>
        <java.version>1.8</java.version>
        <spring-boot.version>2.1.1.RELEASE</spring-boot.version>
        <brave.version>5.6.0</brave.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>io.zipkin.brave</groupId>
                <artifactId>brave-bom</artifactId>
                <version>${brave.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
        </dependency>

        <!-- zipkin客戶端依賴 -->
        <dependency>
            <groupId>io.zipkin.brave</groupId>
            <artifactId>brave</artifactId>
        </dependency>
        <dependency>
            <groupId>io.zipkin.reporter2</groupId>
            <artifactId>zipkin-sender-okhttp3</artifactId>
        </dependency>

        <!-- 新增記錄MVC的類、方法名到span的依賴 -->
        <dependency>
            <groupId>io.zipkin.brave</groupId>
            <artifactId>brave-instrumentation-spring-webmvc</artifactId>
        </dependency>
        <!-- 新增brave的httpclient依賴 -->
        <dependency>
            <groupId>io.zipkin.brave</groupId>
            <artifactId>brave-instrumentation-httpclient</artifactId>
        </dependency>
        <!-- 整合Brave上下文的log -->
        <dependency>
            <groupId>io.zipkin.brave</groupId>
            <artifactId>brave-context-slf4j</artifactId>
        </dependency>
    </dependencies>

</project>
複製程式碼

配置類編寫(zipkin_client)

package com.ycg.zipkin_client;

import brave.CurrentSpanCustomizer;
import brave.SpanCustomizer;
import brave.Tracing;
import brave.context.slf4j.MDCScopeDecorator;
import brave.http.HttpTracing;
import brave.httpclient.TracingHttpClientBuilder;
import brave.propagation.B3Propagation;
import brave.propagation.ExtraFieldPropagation;
import brave.propagation.ThreadLocalCurrentTraceContext;
import brave.servlet.TracingFilter;
import brave.spring.webmvc.SpanCustomizingAsyncHandlerInterceptor;
import org.apache.http.impl.client.CloseableHttpClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.client.RestTemplateCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import zipkin2.Span;
import zipkin2.reporter.AsyncReporter;
import zipkin2.reporter.Sender;
import zipkin2.reporter.okhttp3.OkHttpSender;

import javax.servlet.Filter;

/**
 * 針對mvc controller 和 restTemplate 的 zipkin客戶端配置
 */
@Configuration
@Import(SpanCustomizingAsyncHandlerInterceptor.class)
public class ZipkinClientConfiguration implements WebMvcConfigurer {

    /**
     * 配置如何向 zipkin 傳送 span
     */
    @Bean
    Sender sender() {
        // 注意這裡更換為自己安裝的zipkin所在的主機IP
        return OkHttpSender.create("http://10.150.27.36:9411/api/v2/spans");
    }

    /**
     * 配置如何把 span 緩衝到給 zipkin 的訊息
     */
    @Bean
    AsyncReporter<Span> spanReporter() {
        return AsyncReporter.create(sender());
    }

    /**
     * 配置跟蹤過程中的Trace資訊
     */
    @Bean
    Tracing tracing(@Value("${spring.application.name}") String serviceName) {
        return Tracing.newBuilder()
                .localServiceName(serviceName)  // 設定節點名稱
                .propagationFactory(ExtraFieldPropagation.newFactory(B3Propagation.FACTORY, "user-name"))
                .currentTraceContext(ThreadLocalCurrentTraceContext.newBuilder()
                        .addScopeDecorator(MDCScopeDecorator.create()) // puts trace IDs into logs
                        .build()
                )
                .spanReporter(spanReporter()).build();
    }

    /** 注入可定製的Span */
    @Bean
    SpanCustomizer spanCustomizer(Tracing tracing) {
        return CurrentSpanCustomizer.create(tracing);
    }

    /** 決定如何命名和標記span。 預設情況下,它們的名稱與http方法相同 */
    @Bean
    HttpTracing httpTracing(Tracing tracing) {
        return HttpTracing.create(tracing);
    }

    /** 匯入過濾器,該過濾器中會為http請求建立span */
    @Bean
    Filter tracingFilter(HttpTracing httpTracing) {
        return TracingFilter.create(httpTracing);
    }

    /**
     * 匯入 zipkin 定製的 RestTemplateCustomizer
     */
    @Bean
    RestTemplateCustomizer useTracedHttpClient(HttpTracing httpTracing) {
        final CloseableHttpClient httpClient = TracingHttpClientBuilder.create(httpTracing).build();
        return new RestTemplateCustomizer() {
            @Override public void customize(RestTemplate restTemplate) {
                restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory(httpClient));
            }
        };
    }

    @Autowired
    SpanCustomizingAsyncHandlerInterceptor webMvcTracingCustomizer;

    /** 使用應用程式定義的Web標記裝飾伺服器span */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(webMvcTracingCustomizer);
    }
}
複製程式碼

boot 服務模組

  1. maven依賴:boot+web起步依賴,另引入上面封裝的zipkin_client模組依賴。
<dependency>
    <groupId>com.ycg</groupId>
    <artifactId>zipkin_client</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>
複製程式碼
  1. 啟動類匯入 zipkin_client模組 的配置類 ZipkinClientConfiguration
@SpringBootApplication
@Import(ZipkinClientConfiguration.class)
public class Service1Application {
    public static void main(String[] args) {
        SpringApplication.run(Service1Application.class, args);
    }
}
複製程式碼
  1. 編寫Controller,service2和service3的程式碼類似。由於zipkin配置類那邊向IOC容器注入zipkin定製的RestTemplateCustomizer,注意這裡使用注入的RestTemplateBuilder建立restTemplate
@EnableAutoConfiguration
@RestController
public class Service1Controller {

    private RestTemplate restTemplate;

    @Autowired Service1Controller(RestTemplateBuilder restTemplateBuilder) {
        this.restTemplate = restTemplateBuilder.build();
    }

    @GetMapping(value = "/service1")
    public String getService() {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "service1 sleep 100ms ->" + restTemplate.getForObject("http://localhost:8882/service2",String.class);
    }
}
複製程式碼
  1. 設定三個boot服務的內建tomcat埠號分別為8881、8882、8883。

啟動驗證

到這裡,就完成了一個springboot整合zipkin簡單的demo,分別啟動三個boot應用後,在瀏覽器訪問http://localhost:8881/service1,瀏覽器顯示如下圖:

一文搞懂基於zipkin的分散式追蹤系統原理與實現

開啟zipkin-webUI,點選查詢,便可以查到剛才請求的追蹤鏈路,如下圖。

一文搞懂基於zipkin的分散式追蹤系統原理與實現

繼續點選查到的鏈路資訊,便可檢視該條追蹤鏈路的詳細資訊。這裡採用縮排的形式展示了整條呼叫鏈路,並且再每個呼叫後表明了所花費時間。點選右上角json按鈕,便能看到本次trace的json資料。

一文搞懂基於zipkin的分散式追蹤系統原理與實現

span資料結構詳解

json結構概覽與各欄位含義

一次追蹤鏈路會包含很多個span,因此一個trace便是一個陣列,其標準的json結構如下:

[
  {
    "traceId": "string",    // 追蹤鏈路ID
    "name": "string",       // span名稱,一般為方法名稱
    "parentId": "string",   // 呼叫者ID
    "id": "string",         // spanID
    "kind": "CLIENT",       // 替代zipkin v1的註解中的四個核心狀態,詳細介紹見下文
    "timestamp": 0,         // 時間戳,呼叫時間
    "duration": 0,          // 持續時間-呼叫的服務所消耗的時間
    "debug": true,          
    "shared": true,
    "localEndpoint": {      // 本地網路節點上下文
      "serviceName": "string",
      "ipv4": "string",
      "ipv6": "string",
      "port": 0
    },
    "remoteEndpoint": {    // 遠端網路節點上下文
      "serviceName": "string",
      "ipv4": "string",
      "ipv6": "string",
      "port": 0
    },
    "annotations": [      // value通常是縮寫程式碼,對應的時間戳表示程式碼標記事件的時間
      {
        "timestamp": 0,
        "value": "string"
      }
    ],
    "tags": {        // span的上下文資訊,比如:http.method、http.path
      "additionalProp1": "string",
      "additionalProp2": "string",
      "additionalProp3": "string"
    }
  }
]
複製程式碼

聊聊 annotation 和 kind 的前世姻緣

zipkin V1 之 Annotation
V1 時Annotation 用於記錄一個事件,事件由value標識,事件發生時間則記錄對應的時間戳。一些核心註解核心註解用於定義一個請求的開始和結束。主要是如下四種註解:

  • cs – Client Send,表示客戶端發起請求.
  • sr – Server Receive,表示服務端收到請求。使用sr的時間減去cs的時間便可得到網路傳輸的時間。
  • ss – Server Send,表示服務端完成處理,並將結果傳送給客戶端。使用ss的時間減去sr的時間便是服務端處理請求的時間。
  • cr – Client Received,表示客戶端獲取到服務端返回資訊。使用cr的時間減去cs的時間便是整個請求所消耗的時間。

zipkin V2 之 Kind
V2 使用Span.Kind替代了V1的幾個表示請求開始與結束的核心註解。kind一共有四種狀態,其為不同狀態時,timestamp、duration、remoteEndpoint代表的意義均不相同。

  • CLIENT:

    timestamp是請求被髮送的時刻,相當於v1中註解 cs。
    duration代表傳送請求後,接收到服務端響應前的持續時間,也就是整個請求所消耗的時間。
    remoteEndpoint表示被呼叫方的網路節點資訊。

  • SERVER:

    timestamp是服務端接到請求並準備開始處理它的時間,相當於v1中的sr。
    duration代表服務端接到請求後、傳送響應前的持續時間,也就是服務端的淨處理時間。
    remoteEndpoint表示呼叫方的網路節點資訊。

  • PRODUCER:

    timestamp是訊息被髮送的時刻。
    duration代表傳送方傳送後,訊息佇列結束到訊息前的延遲時間,比如批處理的場景。
    remoteEndpoint表示訊息佇列的網路節點資訊。

  • CONSUMER:

    timestamp是訊息被訊息佇列接收到的時刻。
    duration代表訊息被訊息佇列接收到,被消費者消費前的持續時間,比如訊息積壓的場景。
    remoteEndpoint表示消費者節點資訊,未知則表示service name。

V1 針對訊息佇列也有ms、mr等註解,這裡就不再詳細介紹了。小白覺得kind這種替換後,整個追蹤鏈路更為清晰直觀,或許這也是zipkin的考慮之一吧。

再看Demo中追蹤鏈路的JSON資料

相信看到這裡的小夥伴回頭再看demo中鏈路的json資料,應該可以明白具體的意思了。小白這裡再梳理一下。追蹤鏈路的JSON資料如下(建議直接跳過資料看下面分析):

[
  {
    "traceId": "3857b4a56c99e9f8",
    "parentId": "7dd11a047eb02622",
    "id": "e5427222edb62a7c",
    "kind": "SERVER",
    "name": "get /service3",
    "timestamp": 1547458424863333,
    "duration": 409599,
    "localEndpoint": {
      "serviceName": "server3",
      "ipv4": "172.30.22.138"
    },
    "remoteEndpoint": {
      "ipv4": "127.0.0.1",
      "port": 52845
    },
    "tags": {
      "http.method": "GET",
      "http.path": "/service3",
      "mvc.controller.class": "Service3Controller",
      "mvc.controller.method": "getService"
    },
    "shared": true
  },
  {
    "traceId": "3857b4a56c99e9f8",
    "parentId": "7dd11a047eb02622",
    "id": "e5427222edb62a7c",
    "kind": "CLIENT",
    "name": "get",
    "timestamp": 1547458424756985,
    "duration": 520649,
    "localEndpoint": {
      "serviceName": "server2",
      "ipv4": "172.30.22.138"
    },
    "tags": {
      "http.method": "GET",
      "http.path": "/service3"
    }
  },
  {
    "traceId": "3857b4a56c99e9f8",
    "parentId": "3857b4a56c99e9f8",
    "id": "7dd11a047eb02622",
    "kind": "SERVER",
    "name": "get /service2",
    "timestamp": 1547458424446556,
    "duration": 880044,
    "localEndpoint": {
      "serviceName": "server2",
      "ipv4": "172.30.22.138"
    },
    "remoteEndpoint": {
      "ipv4": "127.0.0.1",
      "port": 52844
    },
    "tags": {
      "http.method": "GET",
      "http.path": "/service2",
      "mvc.controller.class": "Service2Controller",
      "mvc.controller.method": "getService"
    },
    "shared": true
  },
  {
    "traceId": "3857b4a56c99e9f8",
    "parentId": "3857b4a56c99e9f8",
    "id": "7dd11a047eb02622",
    "kind": "CLIENT",
    "name": "get",
    "timestamp": 1547458424271786,
    "duration": 1066836,
    "localEndpoint": {
      "serviceName": "server1",
      "ipv4": "172.30.22.138"
    },
    "tags": {
      "http.method": "GET",
      "http.path": "/service2"
    }
  },
  {
    "traceId": "3857b4a56c99e9f8",
    "id": "3857b4a56c99e9f8",
    "kind": "SERVER",
    "name": "get /service1",
    "timestamp": 1547458424017344,
    "duration": 1358590,
    "localEndpoint": {
      "serviceName": "server1",
      "ipv4": "172.30.22.138"
    },
    "remoteEndpoint": {
      "ipv6": "::1",
      "port": 52841
    },
    "tags": {
      "http.method": "GET",
      "http.path": "/service1",
      "mvc.controller.class": "Service1Controller",
      "mvc.controller.method": "getService"
    }
  }
]
複製程式碼

我們從下往上看,這才是請求最開始的地方。首先看最下面的span(3857b4a56c99e9f8)。請求(http://localhost:8881)是由瀏覽器發出,那麼當請求到達服務1時,作為服務端便會生成kind為SERVER的span,其中duration便是本次請求到後端後的淨處理時間,localEndpoint是server1的節點資訊,remoteEndpoint的呼叫方也就是瀏覽器的節點資訊。

接著服務1需要呼叫服務2的服務,這時服務1是作為客戶端發出請求的。因此會記錄出從下往上第二個span(7dd11a047eb02622),一個客戶端span,也就是kind=CLIENT。localEndpoint還是自己,同時tag裡新增了發出的請求資訊,duration表示發出/service2的請求後,到接收到server2的響應所消耗的時間。再往上span(7dd11a047eb02622),就是server2接收到server1的請求後記錄的SERVER span。剩下的同理,小白就不多說了。

結束語

到這裡小白就介紹完了基於zipkin實現分散式追蹤系統的基本原理與實現,當然這只是一個入門,追蹤資訊是全量收集還是取樣收集,設定什麼樣的取樣頻率,非同步傳送span使用http還是kafka,這些問題都是需要在生產環境中根據實際場景綜合考量的。就本文而言,小白覺得只要你仔細閱讀了,認真思考了,一定還是收穫不少的,當然有深入研究的小夥伴除外。後續小白會深入Brave的原始碼瞭解具體的追蹤實現,如有錯誤,也請多多拍磚多多交流。另,畫圖、碼字、梳理知識不易,如要轉載,請註明出處

參考

zipkin官網

zipkin-api

官方示例:brave-webmvc-example

相關文章