傳統單機系統在使用過程當中,若是某個請求響應過慢或是響應出錯,開發人員能夠清楚知道某個請求出了問題,查看日誌能夠定位到具體方法。可是在分佈式系統中,假若客戶端一個請求到達服務器後,由多個服務協做完成。好比:服務A調用服務B,服務B又調用服務C和服務D,服務D又調用服務E,那麼想要知道是哪一個服務處理時間過長或是處理異常致使這個請求響應緩慢或中斷的話,就須要開發人員一個服務接一個服務的去機器上查看日誌,先定位到出問題的服務,再定位出問題的具體地方。試想一下,隨着系統愈來愈壯大,服務愈來愈多,一個請求對應處理的服務調用鏈愈來愈長,這種排查方式何其艱難。爲了解決這種問題,便誕生了各類分佈式場景中追蹤問題的解決方案,zipkin就是其中之一。html
一個獨立的分佈式追蹤系統,客戶端存在於應用中(即各服務中),應具有追蹤信息生成、採集發送等功能,而服務端應該包含如下基本的三個功能:java
zipkin 總體結構圖以下: git
zipkin(服務端)包含四個組件,分別是collector、storage、search、web UI。github
zipkin的客戶端主要負責根據應用的調用狀況生成追蹤信息,而且將這些追蹤信息發送至zipkin由收集器接收。各語言支持均不一樣,具體能夠查看zipkin官網,java語言的支持就是brave。上面結構圖中,有追蹤器就是指集成了brave。web
在使用zipkin以前,先了解一下Trace和Span這兩個基本概念。一個請求到達應用後所調用的全部服務全部服務組成的調用鏈就像一個樹結構(以下圖),咱們追蹤這個調用鏈路獲得的這個樹結構能夠稱之爲Trace。 spring
追蹤器位於應用程序上,負責生成相關ID、記錄span須要的信息,最後經過傳輸層傳遞給服務端的收集器。咱們首先思考下面幾個問題:sql
一個 span 表示一次服務調用,那麼追蹤器一定是被服務調用發起的動做觸發,生成基本信息,同時爲了追蹤服務提供方對其餘服務的調用狀況,便須要傳遞本次追蹤鏈路的traceId和本次調用的span-id。服務提供方完成服務將結果響應給調用方時,須要根據調用發起時記錄的時間戳與當前時間戳計算本次服務的持續時間進行記錄,至此此次調用的追蹤span完成,就能夠發送給zipkin服務端了。可是須要注意的是,發送span給zipkin collector不得影響這次業務結果,其發送成功與否跟業務無關,所以這裏須要採用異步的方式發送,防止追蹤系統發送延遲與發送失敗致使用戶系統的延遲與中斷。下圖就表示了一次http請求調用的追蹤流程(基於zipkin官網提供的流程圖): apache
上文對基於zipkin實現分佈式追蹤系統的原理作了全面的說明,這裏簡單介紹一下zipkin的安裝方法,下載jar包,直接運行。簡單粗暴,但要注意必須jdk1.8及以上。其他兩種安裝方式見官方介紹。json
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/,顯示頁面以下圖。具體使用後面介紹。 後端
java版客戶端 Brave的官方文檔不多,都在github裏。小白當時找的那叫個頭疼啊,網上各路大神寫的博客中的代碼你扒下來換最新的依賴後都會顯示那些類被標記爲過期,不建議使用。
<?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>
複製代碼
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);
}
}
複製代碼
<dependency>
<groupId>com.ycg</groupId>
<artifactId>zipkin_client</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
複製代碼
@SpringBootApplication
@Import(ZipkinClientConfiguration.class)
public class Service1Application {
public static void main(String[] args) {
SpringApplication.run(Service1Application.class, args);
}
}
複製代碼
@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);
}
}
複製代碼
到這裏,就完成了一個springboot整合zipkin簡單的demo,分別啓動三個boot應用後,在瀏覽器訪問http://localhost:8881/service1,瀏覽器顯示以下圖:
打開zipkin-webUI,點擊查詢,即可以查到剛纔請求的追蹤鏈路,以下圖。
繼續點擊查到的鏈路信息,即可查看該條追蹤鏈路的詳細信息。這裏採用縮進的形式展現了整條調用鏈路,而且再每一個調用後代表了所花費時間。點擊右上角json按鈕,便能看到本次trace的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"
}
}
]
複製代碼
zipkin V1 之 Annotation V1 時Annotation 用於記錄一個事件,事件由value標識,事件發生時間則記錄對應的時間戳。一些核心註解核心註解用於定義一個請求的開始和結束。主要是以下四種註解:
zipkin V2 之 Kind V2 使用Span.Kind替代了V1的幾個表示請求開始與結束的核心註解。kind一共有四種狀態,其爲不一樣狀態時,timestamp、duration、remoteEndpoint表明的意義均不相同。
timestamp是請求被髮送的時刻,至關於v1中註解 cs。
duration表明發送請求後,接收到服務端響應前的持續時間,也就是整個請求所消耗的時間。
remoteEndpoint表示被調用方的網絡節點信息。
timestamp是服務端接到請求並準備開始處理它的時間,至關於v1中的sr。
duration表明服務端接到請求後、發送響應前的持續時間,也就是服務端的淨處理時間。
remoteEndpoint表示調用方的網絡節點信息。
timestamp是消息被髮送的時刻。
duration表明發送方發送後,消息隊列結束到消息前的延遲時間,好比批處理的場景。
remoteEndpoint表示消息隊列的網絡節點信息。
timestamp是消息被消息隊列接收到的時刻。
duration表明消息被消息隊列接收到,被消費者消費前的持續時間,好比消息積壓的場景。
remoteEndpoint表示消費者節點信息,未知則表示service name。
V1 針對消息隊列也有ms、mr等註解,這裏就再也不詳細介紹了。小白以爲kind這種替換後,整個追蹤鏈路更爲清晰直觀,或許這也是zipkin的考慮之一吧。
相信看到這裏的小夥伴回頭再看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的源碼瞭解具體的追蹤實現,若有錯誤,也請多多拍磚多多交流。另,畫圖、碼字、梳理知識不易,如要轉載,請註明出處。