微服務架構下,服務之間的關係錯綜複雜。從調用一個 HTTP API 到最終返回結果,中間可能發生了多個服務間的調用。而這些被調用的服務,可能部署在不一樣的服務器上,由不一樣的團隊開發,甚至可能使用了不一樣的編程語言。在這樣的環境中,排查性能問題或者定位故障就很麻煩。html
Zipkin 是一個分佈式鏈路追蹤系統(distributed tracing system)。它能夠收集並展現一個 HTTP 請求從開始到最終返回結果之間完整的調用鏈。java
Trace
表明一個完整的調用鏈。一個 trace 對應一個隨機生成的惟一的 traceId。例如一個 HTTP 請求到響應是一個 trace。一個 trace 內部包含多個 span。Span
Trace 中的一個基本單元。一個 span 一樣對應一個隨機生成的惟一的 spanId。例如一個 HTTP 請求到響應過程當中,內部可能會訪問型數據庫執行一條 SQL,這是一個新的 span,或者內部調用另一個服務的 HTTP API 也是一個新的 span。一個 trace 中的全部 span 是一個樹形結構,樹的根節點叫作 root span。除 root span 外,其餘 span 都會包含一個 parentId,表示父級 span 的 spanId。Annotation
每一個 span 中包含多個 annotation,用來記錄關鍵事件的時間點。例如一個對外的 HTTP 請求從開始到結束,依次有如下幾個 annotation:mysql
cs
Client Send,客戶端發起請求的,這是一個 span 的開始sr
Server Receive,服務端收到請求開始處理ss
Server Send,服務端處理請求完成並響應cr
Client Receive,客戶端收到響應,這個 span 到此結束記錄了以上的時間點,就能夠很容易分析出一個 span 每一個階段的耗時:git
cr - cs
是整個流程的耗時sr - cs
以及 cr - ss
是網絡耗時ss - sr
是被調用服務處理業務邏輯的耗時然而,sr
和 ss
兩個 annotation 依賴被調用方,若是被調用方沒有相應的記錄,例以下游服務沒有對接 instrumentation 庫,或者像執行一條 SQL 這樣的場景,被調用方是一個數據庫服務,不會記錄 sr
和 ss
,那麼這個 span 就只有 cs
和 cr
。github
相關文檔:web
當上遊服務經過 HTTP 調用下游服務,如何將兩個服務中的全部 span 串聯起來,造成一個 trace,這就須要上游服務將 traceId 等信息傳遞給下游服務,而不能讓下游從新生成一個 traceId。spring
Zipkin 經過 B3 傳播規範(B3 Propagation),將相關信息(如 traceId、spanId 等)經過 HTTP 請求 Header 傳遞給下游服務:sql
Client Tracer Server Tracer ┌───────────────────────┐ ┌───────────────────────┐ │ │ │ │ │ TraceContext │ Http Request Headers │ TraceContext │ │ ┌───────────────────┐ │ ┌───────────────────┐ │ ┌───────────────────┐ │ │ │ TraceId │ │ │ X-B3-TraceId │ │ │ TraceId │ │ │ │ │ │ │ │ │ │ │ │ │ │ ParentSpanId │ │ Inject │ X-B3-ParentSpanId │ Extract │ │ ParentSpanId │ │ │ │ ├─┼────────>│ ├─────────┼>│ │ │ │ │ SpanId │ │ │ X-B3-SpanId │ │ │ SpanId │ │ │ │ │ │ │ │ │ │ │ │ │ │ Sampling decision │ │ │ X-B3-Sampled │ │ │ Sampling decision │ │ │ └───────────────────┘ │ └───────────────────┘ │ └───────────────────┘ │ │ │ │ │ └───────────────────────┘ └───────────────────────┘
相關文檔:數據庫
GitHub 倉庫: https://github.com/openzipkin...apache
Brave is a distributed tracing instrumentation library.
翻譯: Brave 是分佈式鏈路追蹤的埋點庫。
instrumentation 這個單詞本意是"儀器、儀表、器曲譜寫",爲了更加便於理解,這裏我翻譯爲"埋點"。埋點的意思就是在程序的關鍵位置(即上面介紹的各個 annotation)作一些記錄。
在 GitHub 倉庫的 instrumentation 目錄中,能夠看到官方已經提供了很是多的 instrumentation。
另外在 https://zipkin.io/pages/trace... 文檔中,還有其餘非 Java 語言的 instrumentation 以及非官方提供的 instrumentation,能夠根據須要來選擇。其餘 instrumentation 本文不作介紹,本文重點是 Zipkin 官方提供的 Java 語言 instrumentation : Brave 。
本文以 Web 服務爲例,不涉及像 Dubbo 這樣的 RPC 服務。
假設現有一個 Spring MVC 項目想要對接 Zipkin,須要使用 Brave 埋點,並將相關數據提交到 Zipkin 服務上。
首先加入一個 dependencyManagement,這樣就不須要在各個依賴包中添加版本號了:
<dependencyManagement> <dependencies> <dependency> <groupId>io.zipkin.brave</groupId> <artifactId>brave-bom</artifactId> <version>5.11.2</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
最新版本號能夠在這裏查看:
https://mvnrepository.com/art...
須要注意的是,不一樣版本配置方法會略有差別,具體能夠參考官方文檔。本文使用的 Brave 版本號爲 5.11.2。
添加依賴:
<dependency> <groupId>io.zipkin.brave</groupId> <artifactId>brave-context-slf4j</artifactId> </dependency> <dependency> <groupId>io.zipkin.reporter2</groupId> <artifactId>zipkin-sender-okhttp3</artifactId> </dependency>
下面提供了兩種配置方式(Java 配置方式 和 XML 配置方式)建立 Tracing 對象,須要根據項目的實際狀況選擇其中一種。
若是現有的項目是 Spring Boot 項目或者非 XML 配置的 Spring 項目,能夠採用這種方式。
@Configuration public class TracingConfiguration { @Bean public Tracing tracing() { Sender sender = OkHttpSender.create("http://127.0.0.1:9411/api/v2/spans"); Reporter<Span> spanReporter = AsyncReporter.create(sender); Tracing tracing = Tracing.newBuilder() .localServiceName("my-service") .spanReporter(spanReporter) .currentTraceContext(ThreadLocalCurrentTraceContext.newBuilder() .addScopeDecorator(MDCScopeDecorator.get()).build()) .build(); return tracing; } }
若是現有項目是採用 XML 配置的 Spring 項目,能夠採用這種方式。
相對於 Java 配置方式,須要多添加一個 brave-spring-beans 依賴:
<dependency> <groupId>io.zipkin.brave</groupId> <artifactId>brave-spring-beans</artifactId> </dependency>
該模塊提供了一系列 Spring FactoryBean
,用於經過 XML 來建立對象:
<bean id="sender" class="zipkin2.reporter.beans.OkHttpSenderFactoryBean"> <property name="endpoint" value="http://localhost:9411/api/v2/spans"/> </bean> <bean id="correlationScopeDecorator" class="brave.spring.beans.CorrelationScopeDecoratorFactoryBean"> <property name="builder"> <bean class="brave.context.slf4j.MDCScopeDecorator" factory-method="newBuilder"/> </property> </bean> <bean id="tracing" class="brave.spring.beans.TracingFactoryBean"> <property name="localServiceName" value="my-service"/> <property name="spanReporter"> <bean class="zipkin2.reporter.beans.AsyncReporterFactoryBean"> <property name="sender" ref="sender"/> </bean> </property> <property name="currentTraceContext"> <bean class="brave.spring.beans.CurrentTraceContextFactoryBean"> <property name="scopeDecorators" ref="correlationScopeDecorator"/> </bean> </property> </bean>
上面兩種方式本質上是同樣的,都是建立了一個 Tracing
對象。
該對象是單實例的,若是想要在其餘地方獲取到這個對象,能夠經過靜態方法 Tracing tracing = Tracing.current()
來獲取。
Tracing
對象提供了一系列 instrumentation 所須要的工具,例如 tracing.tracer()
能夠獲取到 Tracer
對象,Tracer
對象的做用後面會有詳細介紹。
建立 Tracing
對象一些相關屬性:
localServiceName
服務的名稱spanReporter
指定一個 Reporter<zipkin2.Span>
對象做爲埋點數據的提交方式,這裏一般會使用靜態方法 AsyncReporter.create(Sender sender)
來建立一個 AsyncReporter
對象,固然若是有特殊需求也能夠本身實現 Reporter
接口來自定義提交方式。建立 AsyncReporter
對象須要提供一個 Sender
,下面列出了一些官方提供的 Sender
可供選擇:
zipkin-sender-okhttp3
使用 OkHttp3 提交,使用方法:sender = OkHttpSender.create("http://localhost:9411/api/v2/spans")
,本文中的示例使用的就是這種方式zipkin-sender-urlconnection
使用 Java 自帶的 java.net.HttpURLConnection
提交,使用方法:sender = URLConnectionSender.create("http://localhost:9411/api/v2/spans")
zipkin-sender-activemq-client
使用 ActiveMQ
消息隊列提交,使用方法:sender = ActiveMQSender.create("failover:tcp://localhost:61616")
zipkin-sender-kafka
使用 Kafka
消息隊列提交,使用方法:sender = KafkaSender.create("localhost:9092")
zipkin-sender-amqp-client
使用 RabbitMQ
消息隊列提交,使用方法:sender = RabbitMQSender.create("localhost:5672")
currentTraceContext
指定一個 CurrentTraceContext
對象來設置 TraceContext
對象的做用範圍,一般會使用 ThreadLocalCurrentTraceContext
,也就是用 ThreadLocal
來存放 TraceContext
。TraceContext
包含了一個 trace 的相關信息,例如 traceId。
因爲在 Spring MVC 應用中,一個請求的業務邏輯一般在同一個線程中(暫不考慮異步 Servlet)。一個請求內部的全部業務邏輯應該共用一個 traceId,天然是把 TraceContext
放在 ThreadLocal
中比較合理。這也意味着,默認狀況下 traceId 只在當前線程有效,跨線程會失效。固然,跨線程也有對應的方案,本文後續會有詳細介紹。
在 CurrentTraceContext
中能夠添加 ScopeDecorator
,經過 MDC (Mapped Diagnostic Contexts) 機制關聯一些日誌框架:
以 Logback 爲例(本文中案例使用的方式),能夠配置下面的 pattern 在日誌中輸出 traceId 和 spanId:
<pattern>%d [%X{traceId}/%X{spanId}] [%thread] %-5level %logger{36} - %msg%n</pattern>
添加依賴:
<dependency> <groupId>io.zipkin.brave</groupId> <artifactId>brave-instrumentation-spring-webmvc</artifactId> </dependency>
首先建立 HttpTracing
對象,用於 HTTP 協議鏈路追蹤。
Java 配置方式:
@Bean public HttpTracing httpTracing(Tracing tracing){ return HttpTracing.create(tracing); }
XML 配置方式:
<bean id="httpTracing" class="brave.spring.beans.HttpTracingFactoryBean"> <property name="tracing" ref="tracing"/> </bean>
DelegatingTracingFilter
用於處理外部調用的 HTTP 請求,記錄 sr
(Server Receive) 和 ss
(Server Send) 兩個 annotation。
非 Spring Boot 項目能夠在 web.xml 中添加 DelegatingTracingFilter
:
<filter> <filter-name>tracingFilter</filter-name> <filter-class>brave.spring.webmvc.DelegatingTracingFilter</filter-class> </filter> <filter-mapping> <filter-name>tracingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
若是是 Spring Boot 項目能夠用 FilterRegistrationBean
來添加 DelegatingTracingFilter
:
@Bean public FilterRegistrationBean delegatingTracingFilterRegistrationBean() { FilterRegistrationBean registration = new FilterRegistrationBean(); registration.setFilter(new DelegatingTracingFilter()); registration.setName("tracingFilter"); return registration; }
若是有興趣的話能夠看下 DelegatingTracingFilter
的源碼,它本質上是一個 TracingFilter
的代理。TracingFilter
來源於 brave-instrumentation-servlet 模塊。DelegatingTracingFilter
經過 Spring 容器中的 HttpTracing
對象建立了一個 TracingFilter
。相關代碼在 DelegatingTracingFilter.java
54 行。
到此,Spring MVC 項目已經完成了最基本的 Brave 埋點和提交 Zipkin 的配置。若是有現有的 Zipkin 服務,將建立 OkHttpSender
提供的接口地址換成實際地址,啓動服務後經過 HTTP 請求一下服務,就會在 Zipkin 上找到一個對應的 trace。
因爲每一個服務內部還會調用其餘服務,例如經過 HTTP 調用外部服務的 Api、鏈接遠程數據庫執行 SQL,此時還須要用到其餘 instrumentation。
因爲篇幅有限,下面僅介紹幾個經常使用的 instrumentation。
brave-instrumentation-mysql 能夠爲 MySQL 上執行的每條 SQL 語句生成一個 span,用於分析 SQL 的執行時間。
添加依賴:
<dependency> <groupId>io.zipkin.brave</groupId> <artifactId>brave-instrumentation-mysql</artifactId> </dependency>
使用方法:在 JDBC 鏈接地址末尾加上參數 ?statementInterceptors=brave.mysql.TracingStatementInterceptor
便可。
該模塊用於 mysql-connector-java 5.x 版本,另外還有 brave-instrumentation-mysql6 和 brave-instrumentation-mysql8 可分別用於 mysql-connector-java 6+ 和 mysql-connector-java 8+ 版本。
brave-instrumentation-okhttp3 用於 OkHttp 3.x,在經過 OkHttpClient
請求外部 API 時,生成 span,而且經過 B3 傳播規範將鏈路信息傳遞給被調用方。
添加依賴:
<dependency> <groupId>io.zipkin.brave</groupId> <artifactId>brave-instrumentation-okhttp3</artifactId> </dependency>
使用方法:
OkHttpClient okHttpClient = new OkHttpClient.Builder() .dispatcher(new Dispatcher( httpTracing.tracing().currentTraceContext() .executorService(new Dispatcher().executorService()) )) .addNetworkInterceptor(TracingInterceptor.create(httpTracing)) .build();
若是你使用的 HTTP 客戶端庫不是 OkHttp 而是 Apache HttpClient 的話,可使用 brave-instrumentation-httpclient。
Span currentSpan = Tracing.currentTracer().currentSpan(); // 獲取當前 span if (currentSpan != null) { String traceId = currentSpan.context().traceIdString(); String spanId = currentSpan.context().spanIdString(); }
可將業務相關的信息寫入 tag 中,方便在查看調用鏈信息時關聯查看業務相關信息。
Span currentSpan = Tracing.currentTracer().currentSpan(); // 獲取當前 span if (currentSpan != null) { currentSpan.tag("biz.k1", "v1").tag("biz.k2", "v2"); }
若是使用了某些組件訪問外部服務,找不到官方或開源的 instrumentation,或者有一個本地的耗時任務,也想經過建立一個 span 來記錄任務的運行時間和結果,能夠本身建立一個新的 span。
ScopedSpan span = Tracing.currentTracer().startScopedSpan("span name"); try { // 訪問外部服務 或 本地耗時任務 } catch (Exception e) { span.error(e); // 任務出錯 throw e; } finally { span.finish(); // 必須記得結束 span }
下面是另一種方式,這種方式提供了更多的特性:
Tracer tracer = Tracing.currentTracer(); Span span = tracer.nextSpan().name("span name").start(); try (Tracer.SpanInScope ws = tracer.withSpanInScope(span)) { // SpanInScope 對象須要關閉 // 訪問外部服務 或 本地耗時任務 } catch (Exception e) { span.error(e); // 任務出錯 throw e; } finally { span.finish(); // 必須記得結束 span }
Runnable runnable = ...; // 原始的 Runnable 對象 Runnable tracingRunnable = Tracing.current().currentTraceContext().wrap(runnable); // 包裝過的 Runnable 對象
一樣的方式也可使用於 Callable
對象。
ExecutorService service = ....; ExecutorService proxiedService = tracing.currentTraceContext().executorService(service);
除 Zipkin 以外,還有不少優秀的開源或商業的分佈式鏈路追蹤系統。其中一部分對 Zipkin 協議作了兼容,若是不想使用 Zipkin 也是能夠嘗試一下其餘的分佈式鏈路追蹤系統。