SOFA是螞蟻金服自主研發的金融級分佈式中間件,包含了構建金融級雲原生架構所需的各個組件,SOFATracer 是其中用於分佈式系統調用跟蹤的組件。html
筆者以前有過zipkin的經驗,但願擴展到Opentracing,因而在學習SOFATracer官方博客結合源碼的基礎上總結出此文,與你們分享。java
爲何選擇了從SOFATracer入手來學習?理由很簡單:有大公司背書(是在金融場景裏錘鍊出來的最佳實踐),有開發者和社區整理的官方博客,有直播,示例簡便易調試,爲何不研究使用呢?git
讓咱們用問題來引導閱讀。github
全鏈路跟蹤分紅三個跟蹤級別:web
本文只討論 跨進程跟蹤 (cross-process),由於跨進程跟蹤是最簡單的 ,容易上手^_^。對於跨進程跟蹤,你能夠編寫攔截器或過濾器來跟蹤每一個請求,它只須要編寫極少的代碼。spring
容器、Serverless 編程方式的誕生極大提高了軟件交付與部署的效率。在架構的演化過程當中,能夠看到兩個變化:數據庫
從以上兩個變化能夠看到這種彈性、標準化的架構背後,原先運維與診斷的需求也變得愈來愈複雜。如何理清服務依賴調用關係、如何在這樣的環境下快速 debug
、追蹤服務處理耗時、查找服務性能瓶頸、合理對服務的容量評估都變成一個棘手的事情。編程
爲了應對這些問題,可觀察性(Observability
) 這個概念被引入軟件領域。傳統的監控和報警主要關注系統的異常狀況和失敗因素,可觀察性更關注的是從系統自身出發,去展示系統的運行情況,更像是一種對系統的自我審視。一個可觀察的系統中更關注應用自己的狀態,而不是所處的機器或者網絡這樣的間接證據。咱們但願直接獲得應用當前的吞吐和延遲信息,爲了達到這個目的,咱們就須要合理主動暴露更多應用運行信息。在當前的應用開發環境下,面對複雜系統咱們的關注將逐漸由點 到 點線面體的結合,這能讓咱們更好的理解系統,不只知道What,更能回答Why。後端
可觀察性目前主要包含如下三大支柱:api
Logging
) : Logging
主要記錄一些離散的事件,應用每每經過將定義好格式的日誌信息輸出到文件,而後用日誌收集程序收集起來用於分析和聚合。雖然能夠用時間將全部日誌點事件串聯起來,可是卻很難展現完整的調用關係路徑;Metrics
) :Metric
每每是一些聚合的信息,相比 Logging
喪失了一些具體信息,可是佔用的空間要比完整日誌小的多,能夠用於監控和報警,在這方面 Prometheus 已經基本上成爲了事實上的標準;Tracing
) : Tracing
介於 Logging
和 Metric
之間, 以請求的維度來串聯服務間的調用關係並記錄調用耗時,即保留了必要的信息,又將分散的日誌事件經過 Span 串聯,幫助咱們更好的理解系統的行爲、輔助調試和排查性能問題。三大支柱有以下特色:
分佈式追蹤,也稱爲分佈式請求追蹤,是一種用於分析和監視應用程序的方法,特別是那些使用微服務體系結構構建的應用程序;分佈式追蹤有助於查明故障發生的位置以及致使性能低下的緣由,開發人員可使用分佈式跟蹤來幫助調試和優化他們的代碼,IT和DevOps團隊可使用分佈式追蹤來監視應用程序。
Tracing 是在90年代就已出現的技術。但真正讓該領域流行起來的仍是源於 Google 的一篇論文」Dapper, a Large-Scale Distributed Systems Tracing Infrastructure」,而另外一篇論文」Uncertainty in Aggregate Estimates from Sampled Distributed Traces」中則包含關於採樣的更詳細分析。論文發表後一批優秀的 Tracing 軟件孕育而生。
Logging
和Metric
強化監控和報警。爲了解決不一樣的分佈式追蹤系統 API 不兼容的問題,出現了OpenTracing。OpenTracing旨在標準化Trace數據結構和格式,其目的是:
OpenTracing不是一個標準,OpenTracing API提供了一個標準的、與供應商無關的框架,是對分佈式鏈路中涉及到的一些列操做的高度抽象集合。這意味着若是開發者想要嘗試一種不一樣的分佈式追蹤系統,開發者只須要簡單地修改Tracer配置便可,而不須要替換整個分佈式追蹤系統。
大多數分佈式追蹤系統的思想模型都來自Google's Dapper論文,OpenTracing也使用類似的術語。有幾個基本概念咱們須要提早了解清楚:
Trace(追蹤) :在廣義上,一個trace表明了一個事務或者流程在(分佈式)系統中的執行過程。在OpenTracing標準中,trace是多個span組成的一個有向無環圖(DAG),每個span表明trace中被命名並計時的連續性的執行片斷。
Span(跨度) :一個span表明系統中具備開始時間和執行時長的邏輯運行單元,即應用中的一個邏輯操做。span之間經過嵌套或者順序排列創建邏輯因果關係。一個span能夠被理解爲一次方法調用,一個程序塊的調用,或者一次RPC/數據庫訪問,只要是一個具備完整時間週期的程序訪問,均可以被認爲是一個span。
Logs :每一個span能夠進行屢次Logs操做,每一次Logs操做,都須要一個帶時間戳的時間名稱,以及可選的任意大小的存儲結構。
Tags :每一個span能夠有多個鍵值對(key :value)形式的Tags,Tags是沒有時間戳的,支持簡單的對span進行註解和補充。
SpanContext :SpanContext
更像是一個「概念」,而不是通用 OpenTracing 層的有用功能。在建立Span
、向傳輸協議Inject
(注入)和從傳輸協議中Extract
(提取)調用鏈信息時,SpanContext
發揮着重要做用。
表示分佈式調用鏈條中的一個調用單元,他的邊界包含一個請求進到服務內部再由某種途徑(http/dubbo等)從當前服務出去。
一個span通常會記錄這個調用單元內部的一些信息,例如每一個Span
包含的操做名稱、開始和結束時間、附加額外信息的Span Tag
、可用於記錄Span
內特殊事件Span Log
、用於傳遞Span
上下文的SpanContext
和定義Span
之間關係的References
。
Trace 描述在分佈式系統中的一次"事務"。一個trace是由若干span組成的有向無環圖。
Tracer 用於建立Span,並理解如何跨進程邊界注入(序列化)和提取(反序列化)Span。它有如下的職責:
用圖論的觀點來看的話,traces 能夠被認爲是 spans 的 DAG。也就是說,多個 spans 造成的 DAG 是一個 Traces。
舉例來講,下圖是一個由八個 Spans 造成的一個 Trace。
單個 Trace 中 Span 之間的因果關係 [Span A] ←←←(the root span) | +------+------+ | | [Span B] [Span C] ←←←(Span C is a `ChildOf` Span A) | | [Span D] +---+-------+ | | [Span E] [Span F] >>> [Span G] >>> [Span H] ↑ ↑ ↑ (Span G `FollowsFrom` Span F)
某些時候, 用時間順序來具象化更讓人理解。下面就是一個例子。
單個 Trace 中 Spans 之間的時間關係 ––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–> time [Span A···················································] [Span B··············································] [Span D··········································] [Span C········································] [Span E·······] [Span F··] [Span G··] [Span H··]
一個span能夠和一個或者多個span間存在因果關係。OpenTracing定義了兩種關係:ChildOf 和 FollowsFrom。這兩種引用類型表明了子節點和父節點間的直接因果關係。
ChildOf
將成爲當前 Span 的 child,而 FollowsFrom
則會成爲 parent。 這兩種關係爲 child span 和 parent span 創建了直接因果關係。
表示一個span對應的上下文,span和spanContext基本上是一一對應的關係,這個SpanContext能夠經過某些媒介和方式傳遞給調用鏈的下游來作一些處理(例如子Span的id生成、信息的繼承打印日誌等等)。
上下文存儲的是一些須要跨越邊界的(傳播跟蹤所需的)一些信息,例如:
trace_id
和 span_id
用以區分Trace
中的Span
;任何 OpenTraceing 實現相關的狀態(好比 trace 和 span id)都須要被一個跨進程的 Span 所聯繫。Baggage Items
和 Span Tag
結構相同,惟一的區別是:Span Tag
只在當前Span
中存在,並不在整個trace
中傳遞,而Baggage Items
會隨調用鏈傳遞。SpanContext
數據結構簡化版以下:
SpanContext: - trace_id: "abc123" - span_id: "xyz789 - Baggage Items: - special_id: "vsid1738"
在跨界(跨服務或者協議)傳輸過程當中實現調用關係的傳遞和關聯,須要可以將 SpanContext
向下遊介質注入,並在下游傳輸介質中提取 SpanContext
。
每每可使用協議自己提供的相似HTTP Headers
的機制實現這樣的信息傳遞,像Kafka
這樣的消息中間件也有提供實現這樣功能的Headers
機制。
OpenTracing
實現,可使用 api 中提供的 Tracer.Inject(...) 和 Tracer.Extract(...) 方便的實現 SpanContext
的注入和提取。
Carrier 表示的是一個承載spanContext的媒介,比方說在http調用場景中會有HttpCarrier,在dubbo調用場景中也會有對應的DubboCarrier。
這個接口負責了具體場景中序列化反序列化上下文的具體邏輯,例如在HttpCarrier使用中一般就會有一個對應的HttpFormatter。Tracer的注入和提取就是委託給了Formatter。
這個類是0.30版本以後新加入的組件,這個組件的做用是可以經過它獲取當前線程中啓用的Span信息,而且能夠啓用一些處於未啓用狀態的span。在一些場景中,咱們在一個線程中可能同時創建多個span,可是同一時間同一線程只會有一個span在啓用,其餘的span可能處在下列的狀態中:
除了上述組件以外,在實現一個分佈式全鏈路監控框架的時候,還須要有一個reporter組件,經過它來打印或者上報一些關鍵鏈路信息(例如span建立和結束),只有把這些信息進行處理以後咱們才能對全鏈路信息進行可視化和真正的監控。
SOFATracer 是一個用於分佈式系統調用跟蹤的組件,經過統一的 traceId 將調用鏈路中的各類網絡調用狀況以日誌的方式記錄下來,以達到透視化網絡調用的目的。這些日誌可用於故障的快速發現,服務治理等。
SOFATracer 團隊已經爲咱們搭建了一個完整的 Tracer 框架內核,包括數據模型、編碼器、跨進程透傳 traceId、採樣、日誌落盤與上報等核心機制,並提供了擴展 API 及基於開源組件實現的部分插件,爲咱們基於該框架打造本身的 Tracer 平臺提供了極大便利。
SOFATracer 目前並無提供數據採集器和 UI 展現的功能;主要有兩個方面的考慮:
所以在上報模型上,SOFATracer 提供了日誌輸出和外部上報的擴展,方便接入方可以足夠靈活的方式來處理上報的數據。經過SOFARPC + SOFATracer + zipKin 能夠快速搭建一套完整的鏈路追蹤系統,包括埋點、收集、分析展現等。 收集和分析主要是借用zipKin的能力。
目前 SOFATracer 已經支持了對如下開源組件的埋點支持:Spring MVC、RestTemplate、HttpClient、OkHttp三、JDBC、Dubbo(2.6⁄2.7)、SOFARPC、Redis、MongoDB、Spring Message、Spring Cloud Stream (基於 Spring Message 的埋點)、RocketMQ、Spring Cloud FeignClient、Hystrix。
Opentracing
中將全部核心的組件都聲明爲接口,例如 Tracer
、Span
、SpanContext
、Format
(高版本中還包括 Scope
和 ScopeManager
)等。SOFATracer
使用的版本是 0.22.0 ,主要是對 Tracer
、Span
、SpanContext
三個概念模型的實現。下面就針對幾個組件結合 SOFATracer
來分析。
Tracer
是一個簡單、廣義的接口,它的做用就是構建 span
和傳輸 span
。
SofaTracer
實現了 io.opentracing.Tracer
接口,並擴展了採樣、數據上報等能力。
public class SofaTracer implements Tracer { public static final String ROOT_SPAN_ID = "0"; private final String tracerType; private final Reporter clientReporter; private final Reporter serverReporter; private final Map<String, Object> tracerTags = new ConcurrentHashMap(); private final Sampler sampler; }
Span
是一個跨度單元,在實際的應用過程當中,Span
就是一個完整的數據包,其包含的就是當前節點所須要上報的數據。
SofaTracerSpan
實現了 io.opentracing.Span
接口,並擴展了對 Reference
、tags
、線程異步處理以及插件擴展中所必須的 logType
和產生當前 span
的 Tracer
類型等處理的能力。
每一個span 包含兩個重要的信息 span id(當前模塊的span id)和 span parent ID(上一個調用模塊的span id),經過這兩個信息能夠定位一個span 在調用鏈的位置。 這些屬於核心信息,存儲在SpanContext
中。
public class SofaTracerSpan implements Span { public static final char ARRAY_SEPARATOR = '|'; private final SofaTracer sofaTracer; private final List<SofaTracerSpanReferenceRelationship> spanReferences; /** tags for String */ private final Map<String, String> tagsWithStr = new LinkedHashMap<>(); /** tags for Boolean */ private final Map<String, Boolean> tagsWithBool = new LinkedHashMap<>(); /** tags for Number */ private final Map<String, Number> tagsWithNumber = new LinkedHashMap<>(); private final List<LogData> logs = new LinkedList<>(); private String operationName = StringUtils.EMPTY_STRING; private final SofaTracerSpanContext sofaTracerSpanContext; private long startTime; private long endTime = -1; }
在SOFARPC中分爲 ClientSpan 和ServerSpan。 ClientSpan記錄從客戶端發送請求給服務端,到接受到服務端響應結果的過程。ServerSpan是服務端收到客戶端時間 到 發送響應結果給客戶端的這段過程。
SpanContext
對於 OpenTracing
實現是相當重要的,經過 SpanContext
能夠實現跨進程的鏈路透傳,而且能夠經過 SpanContext
中攜帶的信息將整個鏈路串聯起來。
官方文檔中有這樣一句話:「在
OpenTracing
中,咱們強迫SpanContext
實例成爲不可變的,以免Span
在finish
和reference
操做時會有複雜的生命週期問題。」 這裏是能夠理解的,若是SpanContext
在透傳過程當中發生了變化,好比改了tracerId
,那麼就可能致使鏈路出現斷缺。
SofaTracerSpanContext
實現了 SpanContext
接口,擴展了構建 SpanContext
、序列化 baggageItems
以及SpanContext
等新的能力。
public interface SofaTraceContext { void push(SofaTracerSpan var1); SofaTracerSpan getCurrentSpan(); SofaTracerSpan pop(); int getThreadLocalSpanSize(); void clear(); boolean isEmpty(); }
本小節回答了 Trace信息怎麼傳遞?
OpenTracing之中是經過SpanContext來傳遞Trace信息。
SpanContext存儲的是一些須要跨越邊界的一些信息,好比trace Id,span id,Baggage。這些信息會不一樣組件根據本身的特色序列化進行傳遞,好比序列化到 http header 之中再進行傳遞。而後經過這個 SpanContext 所攜帶的信息將當前節點關聯到整個 Tracer 鏈路中去。
簡單來講就是使用HTTP頭做爲媒介(Carrier)來傳遞跟蹤信息(traceID)。不管微服務是gRPC仍是RESTFul,它們都使用HTTP協議。若是是消息隊列(Message Queue),則將跟蹤信息(traceID)放入消息報頭中。
SofaTracerSpanContext 類就包括而且實現了 「一些須要跨越邊界的一些信息」 。
public class SofaTracerSpanContext implements SpanContext { //spanId separator public static final String RPC_ID_SEPARATOR = "."; //======= The following is the key for serializing data ======================== private static final String TRACE_ID_KET = "tcid"; private static final String SPAN_ID_KET = "spid"; private static final String PARENT_SPAN_ID_KET = "pspid"; private static final String SAMPLE_KET = "sample"; /** * The serialization system transparently passes the prefix of the attribute key */ private static final String SYS_BAGGAGE_PREFIX_KEY = "_sys_"; private String traceId = StringUtils.EMPTY_STRING; private String spanId = StringUtils.EMPTY_STRING; private String parentId = StringUtils.EMPTY_STRING; /** * Default will not be sampled */ private boolean isSampled = false; /** * The system transparently transmits data, * mainly refers to the transparent transmission data of the system dimension. * Note that this field cannot be used for transparent transmission of business. */ private final Map<String, String> sysBaggage = new ConcurrentHashMap<String, String>(); /** * Transparent transmission of data, mainly refers to the transparent transmission data of the business */ private final Map<String, String> bizBaggage = new ConcurrentHashMap<String, String>(); /** * sub-context counter */ private AtomicInteger childContextIndex = new AtomicInteger(0); }
在鏈路環節每一個節點中,SpanContext 都是線程相關,具體都存儲在線程ThreadLocal之中。
實現是 SofaTracerThreadLocalTraceContext 函數。咱們能夠看到使用了 ThreadLocal,這是由於Context是和線程上下文相關的。
public class SofaTracerThreadLocalTraceContext implements SofaTraceContext { private final ThreadLocal<SofaTracerSpan> threadLocal = new ThreadLocal(); public void push(SofaTracerSpan span) { if (span != null) { this.threadLocal.set(span); } } public SofaTracerSpan getCurrentSpan() throws EmptyStackException { return this.isEmpty() ? null : (SofaTracerSpan)this.threadLocal.get(); } public SofaTracerSpan pop() throws EmptyStackException { if (this.isEmpty()) { return null; } else { SofaTracerSpan sofaTracerSpan = (SofaTracerSpan)this.threadLocal.get(); this.clear(); return sofaTracerSpan; } } public int getThreadLocalSpanSize() { SofaTracerSpan sofaTracerSpan = (SofaTracerSpan)this.threadLocal.get(); return sofaTracerSpan == null ? 0 : 1; } public boolean isEmpty() { SofaTracerSpan sofaTracerSpan = (SofaTracerSpan)this.threadLocal.get(); return sofaTracerSpan == null; } public void clear() { this.threadLocal.remove(); } }
日誌落盤又分爲摘要日誌落盤 和 統計日誌落盤;
數據上報是 SofaTracer 基於 OpenTracing Tracer 接口擴展實現出來的功能;Reporter 實例做爲 SofaTracer 的屬性存在,在構造 SofaTracer 實例時,會初始化 Reporter 實例。
Reporter 接口的設計中除了核心的上報功能外,還提供了獲取 Reporter 類型的能力,這個是由於 SOFATracer 目前提供的埋點機制方案須要依賴這個實現。
public interface Reporter { String REMOTE_REPORTER = "REMOTE_REPORTER"; String COMPOSITE_REPORTER = "COMPOSITE_REPORTER"; //獲取 Reporter 實例類型 String getReporterType(); //輸出 span void report(SofaTracerSpan span); //關閉輸出 span 的能力 void close(); }
Reporter 的實現類有兩個,SofaTracerCompositeDigestReporterImpl 和 DiskReporterImpl :
咱們使用的是 RestTemplate 示例
import com.sofa.alipay.tracer.plugins.rest.SofaTracerRestTemplateBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.http.ResponseEntity; import org.springframework.util.concurrent.ListenableFuture; import org.springframework.web.client.AsyncRestTemplate; import org.springframework.web.client.RestTemplate; @SpringBootApplication public class RestTemplateDemoApplication { private static Logger logger = LoggerFactory.getLogger(RestTemplateDemoApplication.class); public static void main(String[] args) throws Exception { SpringApplication.run(RestTemplateDemoApplication.class, args); RestTemplate restTemplate = SofaTracerRestTemplateBuilder.buildRestTemplate(); ResponseEntity<String> responseEntity = restTemplate.getForEntity( "http://localhost:8801/rest", String.class); logger.info("Response is {}", responseEntity.getBody()); AsyncRestTemplate asyncRestTemplate = SofaTracerRestTemplateBuilder .buildAsyncRestTemplate(); ListenableFuture<ResponseEntity<String>> forEntity = asyncRestTemplate.getForEntity( "http://localhost:8801/asyncrest", String.class); //async logger.info("Async Response is {}", forEntity.get().getBody()); logger.info("test finish ......."); } }
這裏首先要提一下SOFATracer 的埋點機制,不一樣組件有不一樣的應用場景和擴展點,所以對插件的實現也要因地制宜,SOFATracer 埋點方式通常是經過 Filter、Interceptor 機制實現的。因此下面咱們提到的Client啓動 / Server 啓動就主要是建立了 Filter、Interceptor 機制。
咱們就以 RestTemplate 爲例看看SofaTracer的啓動。
代碼中只用到 SofaTracerRestTemplateBuilder,怎麼就可以作到一個完整的鏈路跟蹤?原來機密在pom.xml文件之中。
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.alipay.sofa</groupId> <artifactId>tracer-sofa-boot-starter</artifactId> </dependency> </dependencies>
在tracer-sofa-boot-starter 的 spring.factories 文件中,定義了不少類。
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.alipay.sofa.tracer.boot.configuration.SofaTracerAutoConfiguration,\ com.alipay.sofa.tracer.boot.springmvc.configuration.OpenTracingSpringMvcAutoConfiguration,\ com.alipay.sofa.tracer.boot.zipkin.configuration.ZipkinSofaTracerAutoConfiguration,\ com.alipay.sofa.tracer.boot.datasource.configuration.SofaTracerDataSourceAutoConfiguration,\ com.alipay.sofa.tracer.boot.springcloud.configuration.SofaTracerFeignClientAutoConfiguration,\ com.alipay.sofa.tracer.boot.flexible.configuration.TracerAnnotationConfiguration,\ com.alipay.sofa.tracer.boot.resttemplate.SofaTracerRestTemplateConfiguration org.springframework.context.ApplicationListener=com.alipay.sofa.tracer.boot.listener.SofaTracerConfigurationListener
Spring Boot中有一種很是解耦的擴展機制:Spring Factories。這種擴展機制其實是仿照Java中的SPI擴展機制來實現的。
SPI的全名爲Service Provider Interface,這是一種服務發現機制,爲某個接口尋找服務實現。可讓模塊裝配時候能夠動態指明服務。有點相似IOC的思想,就是將裝配的控制權移到程序以外。
Spring Factories是在META-INF/spring.factories文件中配置接口的實現類名稱,而後在程序中讀取這些配置文件並實例化。這種自定義的SPI機制是Spring Boot Starter實現的基礎。
對於 SpringBoot 工程來講,引入 tracer-sofa-boot-starter 以後,Spring程序直接讀取了 tracer-sofa-boot-starter 的 spring.factories 文件中的類而且實例化。用戶就能夠在程序中直接使用不少SOFA的功能。
以Reporter爲例。自動配置類 SofaTracerAutoConfiguration 會將當前全部 SpanReportListener 類型的 bean 實例保存到 SpanReportListenerHolder 的 List 對象中。而SpanReportListener 類型的 Bean 會在 ZipkinSofaTracerAutoConfiguration 自動配置類中注入到當前 Ioc 容器中。這樣 invokeReportListeners 被調用時,就能夠拿到 zipkin 的上報類,從而就能夠實現上報。
對於非 SpringBoot 應用的上報支持,本質上是須要實例化 ZipkinSofaTracerSpanRemoteReporter 對象,並將此對象放在 SpanReportListenerHolder 的 List 對象中。因此 SOFATracer 在 zipkin 插件中提供了一個ZipkinReportRegisterBean,並經過實現 Spring 提供的 bean 生命週期接口 InitializingBean,在ZipkinReportRegisterBean 初始化以後構建一個 ZipkinSofaTracerSpanRemoteReporter 實例,並交給SpanReportListenerHolder 類管理。
這部分代碼是 SofaTracerRestTemplateConfiguration。主要做用是生成一個 RestTemplateInterceptor。
RestTemplateInterceptor 的做用是在請求以前能夠先一步作處理。
首先 SofaTracerRestTemplateConfiguration 的做用是生成一個 SofaTracerRestTemplateEnhance。
@Configuration @ConditionalOnWebApplication @ConditionalOnProperty(prefix = "com.alipay.sofa.tracer.resttemplate", value = "enable", matchIfMissing = true) public class SofaTracerRestTemplateConfiguration { @Bean public SofaTracerRestTemplateBeanPostProcessor sofaTracerRestTemplateBeanPostProcessor() { return new SofaTracerRestTemplateBeanPostProcessor(sofaTracerRestTemplateEnhance()); } @Bean public SofaTracerRestTemplateEnhance sofaTracerRestTemplateEnhance() { return new SofaTracerRestTemplateEnhance(); } }
其次,SofaTracerRestTemplateEnhance 會生成一個 RestTemplateInterceptor,這樣就能夠在請求以前作處理。
public class SofaTracerRestTemplateEnhance { private final RestTemplateInterceptor restTemplateInterceptor; public SofaTracerRestTemplateEnhance() { AbstractTracer restTemplateTracer = SofaTracerRestTemplateBuilder.getRestTemplateTracer(); this.restTemplateInterceptor = new RestTemplateInterceptor(restTemplateTracer); } public void enhanceRestTemplateWithSofaTracer(RestTemplate restTemplate) { // check interceptor if (checkRestTemplateInterceptor(restTemplate)) { return; } List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>( restTemplate.getInterceptors()); interceptors.add(0, this.restTemplateInterceptor); restTemplate.setInterceptors(interceptors); } private boolean checkRestTemplateInterceptor(RestTemplate restTemplate) { for (ClientHttpRequestInterceptor interceptor : restTemplate.getInterceptors()) { if (interceptor instanceof RestTemplateInterceptor) { return true; } } return false; } }
這部分代碼是 OpenTracingSpringMvcAutoConfiguration。主要做用是註冊了 SpringMvcSofaTracerFilter。Spring Filter 用來對某個 Servlet 程序進行攔截處理時,它能夠決定是否將請求繼續傳遞給 Servlet 程序,以及對請求和響應消息是否進行修改。
@Configuration @EnableConfigurationProperties({ OpenTracingSpringMvcProperties.class, SofaTracerProperties.class }) @ConditionalOnWebApplication @ConditionalOnProperty(prefix = "com.alipay.sofa.tracer.springmvc", value = "enable", matchIfMissing = true) @AutoConfigureAfter(SofaTracerAutoConfiguration.class) public class OpenTracingSpringMvcAutoConfiguration { @Autowired private OpenTracingSpringMvcProperties openTracingSpringProperties; @Configuration @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) public class SpringMvcDelegatingFilterProxyConfiguration { @Bean public FilterRegistrationBean springMvcDelegatingFilterProxy() { FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(); SpringMvcSofaTracerFilter filter = new SpringMvcSofaTracerFilter(); filterRegistrationBean.setFilter(filter); List<String> urlPatterns = openTracingSpringProperties.getUrlPatterns(); if (urlPatterns == null || urlPatterns.size() <= 0) { filterRegistrationBean.addUrlPatterns("/*"); } else { filterRegistrationBean.setUrlPatterns(urlPatterns); } filterRegistrationBean.setName(filter.getFilterName()); filterRegistrationBean.setAsyncSupported(true); filterRegistrationBean.setOrder(openTracingSpringProperties.getFilterOrder()); return filterRegistrationBean; } } @Configuration @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) public class WebfluxSofaTracerFilterConfiguration { @Bean @Order(Ordered.HIGHEST_PRECEDENCE + 10) public WebFilter webfluxSofaTracerFilter() { return new WebfluxSofaTracerFilter(); } } }
對一個應用的跟蹤要關注的無非就是 客戶端--->web 層--->rpc 服務--->dao 後端存儲、cache 緩存、消息隊列 mq 等這些基礎組件
。SOFATracer 插件的做用實際上也就是對不一樣組件進行埋點,以便基於這些組件採集應用的鏈路數據。
不一樣組件有不一樣的應用場景和擴展點,所以對插件的實現也要因地制宜,SOFATracer 埋點方式通常是經過 Filter、Interceptor 機制實現的。
SOFATracer 目前已實現的插件中,像 SpringMVC 插件是基於 Filter 進行埋點的,httpclient、resttemplate 等是基於 Interceptor 機制進行埋點的。在實現插件時,要根據不一樣插件的特性和擴展點來選擇具體的埋點方式。正所謂條條大路通羅馬,無論怎麼實現埋點,都是依賴 SOFATracer 自身 API 的擴展機制來實現。
SOFATracer 中全部的插件均須要實現本身的 Tracer 實例,如 SpringMVC 的 SpringMvcTracer 、HttpClient 的 HttpClientTracer 等。
AbstractTracer 是 SOFATracer 用於插件擴展使用的一個抽象類,根據插件類型不一樣,又能夠分爲 clientTracer 和 serverTracer,分別對應於 AbstractClientTracer 和 AbstractServerTracer;再經過 AbstractClientTracer 和 AbstractServerTracer 衍生出具體的組件 Tracer 實現,好比上圖中提到的 HttpClientTracer 、RestTemplateTracer 、SpringMvcTracer 等插件 Tracer 實現。
如何肯定一個組件是 client 端仍是 server 端呢?就是看當前組件是請求的發起方仍是請求的接受方,若是是請求發起方則通常是 client 端,若是是請求接收方則是 server 端。那麼對於 RPC 來講,便是請求的發起方也是請求的接受方,所以這裏實現了 AbstractTracer 類。
對於一個組件來講,一次處理過程通常是產生一個 Span;這個 Span 的生命週期是從接收到請求到返回響應這段過程。
可是這裏須要考慮的問題是如何與上下游鏈路關聯起來呢?在 Opentracing 規範中,能夠在 Tracer 中 extract 出一個跨進程傳遞的 SpanContext 。而後經過這個 SpanContext 所攜帶的信息將當前節點關聯到整個 Tracer 鏈路中去,固然有提取(extract)就會有對應的注入(inject)。
鏈路的構建通常是 client------server------client------server 這種模式的,那這裏就很清楚了,就是會在 client 端進行注入(inject),而後再 server 端進行提取(extract),反覆進行,而後一直傳遞下去。
在拿到 SpanContext 以後,此時當前的 Span 就能夠關聯到這條鏈路中了,那麼剩餘的事情就是收集當前組件的一些數據;整個過程大概分爲如下幾個階段:
SOFATracer 支持對標準 Servlet 規範的 Web MVC 埋點,包括普通的 Servlet 和 Spring MVC 等,基本原理就是基於 Servelt 規範所提供的 javax.servlet.Filter 過濾器接口擴展實現。
過濾器位於 Client 和 Web 應用程序之間,用於檢查和修改二者之間流過的請求和響應信息。在請求到達 Servlet 以前,過濾器截獲請求。在響應送給客戶端以前,過濾器截獲響應。多個過濾器造成一個 FilterChain,FilterChain 中不一樣過濾器的前後順序由部署文件 web.xml 中過濾器映射的順序決定。最早截獲客戶端請求的過濾器將最後截獲 Servlet 的響應信息。
Web 應用程序通常做爲請求的接收方,在 SOFATracer 中應用是做爲 Server 存在的,其在解析 SpanContext 時所對應的事件爲 sr (server receive)。
SOFATracer 在 sofa-tracer-springmvc-plugin 插件中解析及產生 Span 的過程大體以下:
HTTP 客戶端埋點包括 HttpClient、OkHttp、RestTemplate 等,此類埋點通常都是基於攔截器機制來實現的,如 HttpClient 使用的 HttpRequestInterceptor、HttpResponseInterceptor;OkHttp 使用的 okhttp3.Interceptor;RestTemplate 使用的 ClientHttpRequestInterceptor。
以 OkHttp 爲例,簡單分析下 HTTP 客戶端埋點的實現原理:
@Override public Response intercept(Chain chain) throws IOException { // 獲取請求 Request request = chain.request(); // 解析出 SpanContext ,而後構建 Span SofaTracerSpan sofaTracerSpan = okHttpTracer.clientSend(request.method()); // 發起具體的調用 Response response = chain.proceed(appendOkHttpRequestSpanTags(request, sofaTracerSpan)); // 結束 span okHttpTracer.clientReceive(String.valueOf(response.code())); return response; }
在 SOFATracer 中將請求大體分爲如下幾個過程:
不管是哪一個插件,在請求處理週期內均可以從上述幾個階段中找到對應的處理方法。所以,SOFATracer 對這幾個階段處理進行了封裝。
在SOFA這裏,四個階段實際上會產生兩個 Span,第一個 Span 的起點是 cs,到 cr 結束;第二個 Span 是從 sr 開始,到 ss 結束。
clientSend // 客戶端發送請求,也就是 cs 階段,會產生一個 Span。 serverReceive // 服務端接收請求 sr 階段,產生了一個 Span 。 ... serverSend clientReceive
從時間序列上看,以下圖所示。
Client Server +--------------+ Request +--------------+ | Client Send | +----------------> |Server Receive| +------+-------+ +------+-------+ | | | v | +------+--------+ | |Server Business| | +------+--------+ | | | | v v +------+--------+ Response +------+-------+ |Client Receive | <---------------+ |Server Send | +------+--------+ +------+-------+ | | | | v v
產生trace ID 是在 客戶端發送請求 clientSend cs 這個階段,即,此 ID 通常由集羣中第一個處理請求的系統產生,並在分佈式調用下經過網絡傳遞到下一個被請求系統。就是 AbstractTracer # clientSend 函數。
調用 buildSpan 構建一個 SofaTracerSpan clientSpan,而後調用 start 函數創建一個 Span。
若是不存在Parent context,則調用 createRootSpanContext 創建了 new root span context。
sofaTracerSpanContext = this.createRootSpanContext();
若是存在 Parent context,則調用 createChildContext 創建 span context。
對 clientSpan 設置各類 Tag。
clientSpan.setTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_CLIENT);
對 clientSpan 設置 log。
clientSpan.log(LogData.CLIENT_SEND_EVENT_VALUE);
把 clientSpan 設置進入SpanContext.
sofaTraceContext.push(clientSpan);
具體產生traceId 的代碼是在類 TraceIdGenerator 中。能夠看到,TraceId 是由 ip,時間戳,遞增序列,進程ID等構成,即traceId爲服務器 IP + 產生 ID 時候的時間 + 自增序列 + 當前進程號,以此保證全局惟一性。這就回答了咱們以前提過的問題:traceId是怎麼生成的,有什麼規則?
public class TraceIdGenerator { private static String IP_16 = "ffffffff"; private static AtomicInteger count = new AtomicInteger(1000); private static String getTraceId(String ip, long timestamp, int nextId) { StringBuilder appender = new StringBuilder(30); appender.append(ip).append(timestamp).append(nextId).append(TracerUtils.getPID()); return appender.toString(); } public static String generate() { return getTraceId(IP_16, System.currentTimeMillis(), getNextId()); } private static String getIP_16(String ip) { String[] ips = ip.split("\\."); StringBuilder sb = new StringBuilder(); String[] var3 = ips; int var4 = ips.length; for(int var5 = 0; var5 < var4; ++var5) { String column = var3[var5]; String hex = Integer.toHexString(Integer.parseInt(column)); if (hex.length() == 1) { sb.append('0').append(hex); } else { sb.append(hex); } } return sb.toString(); } private static int getNextId() { int current; int next; do { current = count.get(); next = current > 9000 ? 1000 : current + 1; } while(!count.compareAndSet(current, next)); return next; } static { try { String ipAddress = TracerUtils.getInetAddress(); if (ipAddress != null) { IP_16 = getIP_16(ipAddress); } } catch (Throwable var1) { } } }
有兩個地方會生成SpanId : CS, SR。SOFARPC 和 Dapper不一樣,spanId中已經包含了調用鏈上下文關係,包含parent spanId 的信息。好比 系統在處理一個請求的過程當中依次調用了 B,C,D 三個系統,那麼這三次調用的的 SpanId 分別是:0.1,0.2,0.3。若是 C 系統繼續調用了 E,F 兩個系統,那麼這兩次調用的 SpanId 分別是:0.2.1,0.2.2。
接上面小節,在客戶端發送請求 clientSend cs 這個階段,就會構建Span,從而生成 SpanID。
調用 buildSpan 構建一個 SofaTracerSpan clientSpan,而後調用 start 函數創建一個 Span。
若是不存在Parent context,則調用 createRootSpanContext 創建了 new root span context。
sofaTracerSpanContext = this.createRootSpanContext();
若是存在 Parent context,則調用 createChildContext 創建 span context,這裏的 preferredReference.getSpanId() 就生成了Span ID。由於此時已經有了Parent Context,因此新的Span Id是在 Parent Span id基礎上構建。
SofaTracerSpanContext sofaTracerSpanContext = new SofaTracerSpanContext( preferredReference.getTraceId(), preferredReference.nextChildContextId(), preferredReference.getSpanId(), preferredReference.isSampled());
咱們再以 Server Receive這個動做爲例,能夠看到在Server端 的 Span構建過程。
SpringMvcSofaTracerFilter # doFilter 會從 Header 中提取 SofaTracerSpanContext。
AbstractTracer # serverReceive 會根據 SofaTracerSpanContext 進行後續操做,此時 SofaTracerSpanContext 以下:
sofaTracerSpanContext = {SofaTracerSpanContext@6056} "SofaTracerSpanContext{traceId='c0a80103159927161709310013925', spanId='0', parentId='', isSampled=true, bizBaggage={}, sysBaggage={}, childContextIndex=0}" traceId = "c0a80103159927161709310013925" spanId = "0" parentId = "" isSampled = true sysBaggage = {ConcurrentHashMap@6060} size = 0 bizBaggage = {ConcurrentHashMap@6061} size = 0 childContextIndex = {AtomicInteger@6062} "0"
從當前線程取出當前的SpanContext,而後提取serverSpan,此 serverSpan 可能爲null,也可能有值。
SofaTraceContext sofaTraceContext = SofaTraceContextHolder.getSofaTraceContext(); SofaTracerSpan serverSpan = sofaTraceContext.pop();
若是serverSpan爲null,則生成一個新的 newSpan,而後調用 setSpanId 對傳入的 SofaTracerSpanContext 參數進行設置新的 SpanId
sofaTracerSpanContext.setSpanId(sofaTracerSpanContext.nextChildContextId()); 此時 sofaTracerSpanContext 內容有變化了,具體就是spanId。 sofaTracerSpanContext = {SofaTracerSpanContext@6056} traceId = "c0a80103159927161709310013925" spanId = "0.1" parentId = "" .....
若是serverSpan 不爲 null,則 newSpan = serverSpan
設置log
設置Tag
把 newSpan 設置進入本地上下文。sofaTraceContext.push(newSpan);
須要注意,在鏈路的後續環節中,traceId 和 spanId 都是存儲在本地線程的 sofaTracerSpanContext 之中,不是在 Span 之中。
具體代碼以下:
首先,SpringMvcSofaTracerFilter # doFilter 會從 Header 中提取 SofaTracerSpanContext
public class SpringMvcSofaTracerFilter implements Filter { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) { // 從header中提取Context SofaTracerSpanContext spanContext = getSpanContextFromRequest(request); // sr springMvcSpan = springMvcTracer.serverReceive(spanContext); } }
其次,AbstractTracer # serverReceive 會根據 SofaTracerSpanContext 進行後續操做
public abstract class AbstractTracer { public SofaTracerSpan serverReceive(SofaTracerSpanContext sofaTracerSpanContext) { SofaTracerSpan newSpan = null; SofaTraceContext sofaTraceContext = SofaTraceContextHolder.getSofaTraceContext(); SofaTracerSpan serverSpan = sofaTraceContext.pop(); try { if (serverSpan == null) { if (sofaTracerSpanContext == null) { sofaTracerSpanContext = SofaTracerSpanContext.rootStart(); isCalculateSampled = true; } else { sofaTracerSpanContext.setSpanId(sofaTracerSpanContext.nextChildContextId()); } newSpan = this.genSeverSpanInstance(System.currentTimeMillis(), StringUtils.EMPTY_STRING, sofaTracerSpanContext, null); } else { newSpan = serverSpan; } } } }
咱們能夠看到,SpanID的構建規則相對簡單,這就回答了咱們以前提過的問題:spanId是怎麼生成的,有什麼規則? 以及 ParentSpan 從哪兒來?
public class SofaTracerSpanContext implements SpanContext { private AtomicInteger childContextIndex = new AtomicInteger(0); public String nextChildContextId() { return this.spanId + RPC_ID_SEPARATOR + childContextIndex.incrementAndGet(); } }
本節咱們看看RestTemplate是如何發送請求的。
首先,打印出程序運行時候的Stack以下,這樣你們能夠先有一個大體的印象:
intercept:56, RestTemplateInterceptor (com.sofa.alipay.tracer.plugins.rest.interceptor) execute:92, InterceptingClientHttpRequest$InterceptingRequestExecution (org.springframework.http.client) executeInternal:76, InterceptingClientHttpRequest (org.springframework.http.client) executeInternal:48, AbstractBufferingClientHttpRequest (org.springframework.http.client) execute:53, AbstractClientHttpRequest (org.springframework.http.client) doExecute:734, RestTemplate (org.springframework.web.client) execute:669, RestTemplate (org.springframework.web.client) getForEntity:337, RestTemplate (org.springframework.web.client) main:40, RestTemplateDemoApplication (com.alipay.sofa.tracer.examples.rest)
在 InterceptingClientHttpRequest # execute 此處代碼中
class InterceptingClientHttpRequest extends AbstractBufferingClientHttpRequest { @Override public ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException { if (this.iterator.hasNext()) { ClientHttpRequestInterceptor nextInterceptor = this.iterator.next(); return nextInterceptor.intercept(request, body, this); // 這裏進行攔截處理 } } }
最後是來到了 SOFA 的攔截器中,這裏會作處理。
具體實現代碼是在 RestTemplateInterceptor # intercept函數。
咱們能夠看到,RestTemplateInterceptor這裏有一個成員變量 restTemplateTracer,具體處理就是在 restTemplateTracer 這裏實現。能夠看到這裏包含了 clientSend 和 clientReceive 兩個過程。
首先生成一個Span。SofaTracerSpan sofaTracerSpan = restTemplateTracer.clientSend(request.getMethod().name());
先從 SofaTraceContext 取出 serverSpan。若是本 client 就是 一個服務中間點(即 serverSpan 不爲空),那麼須要給新span設置父親Span。
調用 clientSpan = (SofaTracerSpan)this.sofaTracer.buildSpan(operationName).asChildOf(serverSpan).start();
獲得自己的 client Span。若是有 server Span,則本 Client Span 就是 Sever Span的 child。
public Tracer.SpanBuilder asChildOf(Span parentSpan) { if (parentSpan == null) { return this; } return addReference(References.CHILD_OF, parentSpan.context()); }
設置父親 clientSpan.setParentSofaTracerSpan(serverSpan);
而後調用 appendRestTemplateRequestSpanTags 來把Span放入Request的Header中。
,injectCarrier(request, sofaTracerSpan);
發送請求。
收到服務器返回以後進一步處理。
從ThreadLocal中獲取 sofaTraceContext
從 SofaTracerSpan 中獲取 currentSpan
調用 appendRestTemplateResponseSpanTags 設置各類 Tag
調用 restTemplateTracer.clientReceive(resultCode); 處理
clientSpan = sofaTraceContext.pop(); 把以前的Span移除
調用 clientReceiveTagFinish ,進而調用 clientSpan.finish();
SpanTracer.reportSpan
進行 Span 上報,其中Reporter 數據上報 reportSpan 或者鏈路跨度 SofaTracerSpan 啓動調用採樣器 sample 方法檢查鏈路是否須要採樣,獲取採樣狀態 SamplingStatus 是否採樣標識 isSampled。若是還有父親Span,則須要再push 父親 Span進入Context。sofaTraceContext.push(clientSpan.getParentSofaTracerSpan());
以備後續處理。
具體代碼以下:
public class RestTemplateInterceptor implements ClientHttpRequestInterceptor { protected AbstractTracer restTemplateTracer; // Sofa內部邏輯實現 @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { SofaTracerSpan sofaTracerSpan = restTemplateTracer.clientSend(request.getMethod().name()); // 生成Span appendRestTemplateRequestSpanTags(request, sofaTracerSpan); //放入Header ClientHttpResponse response = null; Throwable t = null; try { return response = execution.execute(request, body); //發送請求 } catch (IOException e) { t = e; throw e; } finally { SofaTraceContext sofaTraceContext = SofaTraceContextHolder.getSofaTraceContext(); SofaTracerSpan currentSpan = sofaTraceContext.getCurrentSpan(); String resultCode = SofaTracerConstant.RESULT_CODE_ERROR; // is get error if (t != null) { currentSpan.setTag(Tags.ERROR.getKey(), t.getMessage()); // current thread name sofaTracerSpan.setTag(CommonSpanTags.CURRENT_THREAD_NAME, Thread.currentThread() .getName()); } if (response != null) { //tag append appendRestTemplateResponseSpanTags(response, currentSpan); //finish resultCode = String.valueOf(response.getStatusCode().value()); } restTemplateTracer.clientReceive(resultCode); } } }
上文提到了發送時候會調用 AbstractTextB3Formatter.inject 設置 traceId, spanId, parentId。
Fomatter 這個接口負責了具體場景中序列化/反序列化上下文的具體邏輯,例如在HttpCarrier使用中一般就會有一個對應的HttpFormatter。Tracer的注入和提取就是委託給了Formatter。
執行時候堆棧以下:
inject:141, AbstractTextB3Formatter (com.alipay.common.tracer.core.registry) inject:26, AbstractTextB3Formatter (com.alipay.common.tracer.core.registry) inject:115, SofaTracer (com.alipay.common.tracer.core) injectCarrier:146, RestTemplateInterceptor (com.sofa.alipay.tracer.plugins.rest.interceptor) appendRestTemplateRequestSpanTags:141, RestTemplateInterceptor (com.sofa.alipay.tracer.plugins.rest.interceptor) intercept:57, RestTemplateInterceptor (com.sofa.alipay.tracer.plugins.rest.interceptor) execute:92, InterceptingClientHttpRequest$InterceptingRequestExecution (org.springframework.http.client) executeInternal:76, InterceptingClientHttpRequest (org.springframework.http.client) executeInternal:48, AbstractBufferingClientHttpRequest (org.springframework.http.client) execute:53, AbstractClientHttpRequest (org.springframework.http.client) doExecute:734, RestTemplate (org.springframework.web.client) execute:669, RestTemplate (org.springframework.web.client) getForEntity:337, RestTemplate (org.springframework.web.client) main:40, RestTemplateDemoApplication (com.alipay.sofa.tracer.examples.rest)
OpenTracing提供了兩個處理「跟蹤上下文(trace context)」的函數:
Inject 和 extract 分別對應了序列化 和 反序列化。
public abstract class AbstractTextB3Formatter implements RegistryExtractorInjector<TextMap> { public static final String TRACE_ID_KEY_HEAD = "X-B3-TraceId"; public static final String SPAN_ID_KEY_HEAD = "X-B3-SpanId"; public static final String PARENT_SPAN_ID_KEY_HEAD = "X-B3-ParentSpanId"; public static final String SAMPLED_KEY_HEAD = "X-B3-Sampled"; static final String FLAGS_KEY_HEAD = "X-B3-Flags"; static final String BAGGAGE_KEY_PREFIX = "baggage-"; static final String BAGGAGE_SYS_KEY_PREFIX = "baggage-sys-"; public SofaTracerSpanContext extract(TextMap carrier) { if (carrier == null) { return SofaTracerSpanContext.rootStart(); } else { String traceId = null; String spanId = null; String parentId = null; boolean sampled = false; boolean isGetSampled = false; Map<String, String> sysBaggage = new ConcurrentHashMap(); Map<String, String> bizBaggage = new ConcurrentHashMap(); Iterator var9 = carrier.iterator(); while(var9.hasNext()) { Entry<String, String> entry = (Entry)var9.next(); String key = (String)entry.getKey(); if (!StringUtils.isBlank(key)) { if (traceId == null && "X-B3-TraceId".equalsIgnoreCase(key)) { traceId = this.decodedValue((String)entry.getValue()); } if (spanId == null && "X-B3-SpanId".equalsIgnoreCase(key)) { spanId = this.decodedValue((String)entry.getValue()); } if (parentId == null && "X-B3-ParentSpanId".equalsIgnoreCase(key)) { parentId = this.decodedValue((String)entry.getValue()); } String keyTmp; if (!isGetSampled && "X-B3-Sampled".equalsIgnoreCase(key)) { keyTmp = this.decodedValue((String)entry.getValue()); if ("1".equals(keyTmp)) { sampled = true; } else if ("0".equals(keyTmp)) { sampled = false; } else { sampled = Boolean.parseBoolean(keyTmp); } isGetSampled = true; } String valueTmp; if (key.indexOf("baggage-sys-") == 0) { keyTmp = StringUtils.unescapeEqualAndPercent(key).substring("baggage-sys-".length()); valueTmp = StringUtils.unescapeEqualAndPercent(this.decodedValue((String)entry.getValue())); sysBaggage.put(keyTmp, valueTmp); } if (key.indexOf("baggage-") == 0) { keyTmp = StringUtils.unescapeEqualAndPercent(key).substring("baggage-".length()); valueTmp = StringUtils.unescapeEqualAndPercent(this.decodedValue((String)entry.getValue())); bizBaggage.put(keyTmp, valueTmp); } } } if (traceId == null) { return SofaTracerSpanContext.rootStart(); } else { if (spanId == null) { spanId = "0"; } if (parentId == null) { parentId = ""; } SofaTracerSpanContext sofaTracerSpanContext = new SofaTracerSpanContext(traceId, spanId, parentId, sampled); if (sysBaggage.size() > 0) { sofaTracerSpanContext.addSysBaggage(sysBaggage); } if (bizBaggage.size() > 0) { sofaTracerSpanContext.addBizBaggage(bizBaggage); } return sofaTracerSpanContext; } } } public void inject(SofaTracerSpanContext spanContext, TextMap carrier) { if (carrier != null && spanContext != null) { carrier.put("X-B3-TraceId", this.encodedValue(spanContext.getTraceId())); carrier.put("X-B3-SpanId", this.encodedValue(spanContext.getSpanId())); carrier.put("X-B3-ParentSpanId", this.encodedValue(spanContext.getParentId())); carrier.put("X-B3-SpanId", this.encodedValue(spanContext.getSpanId())); carrier.put("X-B3-Sampled", this.encodedValue(String.valueOf(spanContext.isSampled()))); Iterator var3 = spanContext.getSysBaggage().entrySet().iterator(); Entry entry; String key; String value; while(var3.hasNext()) { entry = (Entry)var3.next(); key = "baggage-sys-" + StringUtils.escapePercentEqualAnd((String)entry.getKey()); value = this.encodedValue(StringUtils.escapePercentEqualAnd((String)entry.getValue())); carrier.put(key, value); } var3 = spanContext.getBizBaggage().entrySet().iterator(); while(var3.hasNext()) { entry = (Entry)var3.next(); key = "baggage-" + StringUtils.escapePercentEqualAnd((String)entry.getKey()); value = this.encodedValue(StringUtils.escapePercentEqualAnd((String)entry.getValue())); carrier.put(key, value); } } } }
通過序列化以後,最後發送的Header以下,咱們須要回憶下 spanContext 的概念。
上下文存儲的是一些須要跨越邊界的一些信息,例如:
- spanId :當前這個span的id
- traceId :這個span所屬的traceId(也就是此次調用鏈的惟一id)。
trace_id
和span_id
用以區分Trace
中的Span
;任何 OpenTraceing 實現相關的狀態(好比 trace 和 span id)都須要被一個跨進程的 Span 所聯繫。- baggage :其餘的能過跨越多個調用單元的信息,即跨進程的 key value 對。
Baggage Items
和Span Tag
結構相同,惟一的區別是:Span Tag
只在當前Span
中存在,並不在整個trace
中傳遞,而Baggage Items
會隨調用鏈傳遞。
能夠看到,spanContext 已經被分解而且序列化到 Header 之中。
request = {InterceptingClientHttpRequest@5808} requestFactory = {SimpleClientHttpRequestFactory@5922} interceptors = {ArrayList@5923} size = 1 method = {HttpMethod@5924} "GET" uri = {URI@5925} "http://localhost:8801/rest" bufferedOutput = {ByteArrayOutputStream@5926} "" headers = {HttpHeaders@5918} size = 6 "Accept" -> {LinkedList@5938} size = 1 "Content-Length" -> {LinkedList@5940} size = 1 "X-B3-TraceId" -> {LinkedList@5942} size = 1 key = "X-B3-TraceId" value = {LinkedList@5942} size = 1 0 = "c0a800031598690915258100115720" "X-B3-SpanId" -> {LinkedList@5944} size = 2 key = "X-B3-SpanId" value = {LinkedList@5944} size = 2 0 = "0" 1 = "0" "X-B3-ParentSpanId" -> {LinkedList@5946} size = 1 "X-B3-Sampled" -> {LinkedList@5948} size = 1 executed = false body = {byte[0]@5810}
發送的最後一步是 clientSpan.finish()。
在 Opentracing 規範中提到,Span#finish 方法是 span 生命週期的最後一個執行方法,也就意味着一個 span 跨度即將結束。那麼當一個 span 即將結束時,也是當前 span 具備最完整狀態的時候。因此在 SOFATracer 中,數據上報的入口就是 Span#finish 方法,其調用堆棧以下:
doReportStat:43, RestTemplateStatJsonReporter (com.sofa.alipay.tracer.plugins.rest) reportStat:179, AbstractSofaTracerStatisticReporter (com.alipay.common.tracer.core.reporter.stat) statisticReport:143, DiskReporterImpl (com.alipay.common.tracer.core.reporter.digest) doReport:60, AbstractDiskReporter (com.alipay.common.tracer.core.reporter.digest) report:51, AbstractReporter (com.alipay.common.tracer.core.reporter.facade) reportSpan:141, SofaTracer (com.alipay.common.tracer.core) finish:165, SofaTracerSpan (com.alipay.common.tracer.core.span) finish:158, SofaTracerSpan (com.alipay.common.tracer.core.span) clientReceiveTagFinish:176, AbstractTracer (com.alipay.common.tracer.core.tracer) clientReceive:157, AbstractTracer (com.alipay.common.tracer.core.tracer) intercept:82, RestTemplateInterceptor (com.sofa.alipay.tracer.plugins.rest.interceptor) execute:92, InterceptingClientHttpRequest$InterceptingRequestExecution (org.springframework.http.client) executeInternal:76, InterceptingClientHttpRequest (org.springframework.http.client) executeInternal:48, AbstractBufferingClientHttpRequest (org.springframework.http.client) execute:53, AbstractClientHttpRequest (org.springframework.http.client) doExecute:734, RestTemplate (org.springframework.web.client) execute:669, RestTemplate (org.springframework.web.client) getForEntity:337, RestTemplate (org.springframework.web.client) main:40, RestTemplateDemoApplication (com.alipay.sofa.tracer.examples.rest)
SOFATracer 自己提供了兩種上報模式,一種是落到磁盤,另一種是上報到zipkin。在實現細節上,SOFATracer 沒有將這兩種策略分開以提供獨立的功能支持,而是將兩種上報方式組合在了一塊兒,而且在執行具體上報的流程中經過參數來調控是否執行具體的上報。
此過程當中涉及到了三個上報點,首先是上報到 zipkin
,後面是落盤;在日誌記錄方面,SOFATracer
中爲不一樣的組件均提供了獨立的日誌空間,除此以外,SOFATracer
在鏈路數據採集時提供了兩種不一樣的日誌記錄模式:摘要日誌和統計日誌,這對於後續構建一些如故障的快速發現、服務治理等管控端提供了強大的數據支撐。。
好比 zipkin 對應上報是:
public class ZipkinSofaTracerSpanRemoteReporter implements SpanReportListener, Flushable, Closeable { public void onSpanReport(SofaTracerSpan span) { //convert Span zipkinSpan = zipkinV2SpanAdapter.convertToZipkinSpan(span); this.delegate.report(zipkinSpan); } }
其會調用到 zipkin2.reporter.AsyncReporter 進行具體 report。
採樣是對於整條鏈路來講的,也就是說從 RootSpan 被建立開始,就已經決定了當前鏈路數據是否會被記錄了。在 SofaTracer 類中,Sapmler 實例做爲成員變量存在,而且被設置爲 final,也就是當構建好 SofaTracer 實例以後,採樣策略就不會被改變。當 Sampler 採樣器綁定到 SofaTracer 實例以後,SofaTracer 對於產生的 Span 數據的落盤行爲都會依賴採樣器的計算結果(針對某一條鏈路而言)。
類 SpringMvcSofaTracerFilter 完成了服務端接收相關工做。主要就是設置 SpanContext 和 Span。
public class SpringMvcSofaTracerFilter implements Filter { private SpringMvcTracer springMvcTracer; @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) { ...... } }
回憶下:在 client 端就是
server 端則是 從請求的 Header 中 extract 出 spanContext,來還本來次請求線程的上下文。由於上下文是和所處理的線程相關,放入 ThreadLocal中。
大體能夠用以下圖演示整體流程以下:
Client Span Server Span ┌──────────────────┐ ┌──────────────────┐ │ │ │ │ │ TraceContext │ Http Request Headers │ TraceContext │ │ ┌──────────────┐ │ ┌───────────────────┐ │ ┌──────────────┐ │ │ │ TraceId │ │ │ X-B3-TraceId │ │ │ TraceId │ │ │ │ │ │ │ │ │ │ │ │ │ │ ParentSpanId │ │ Inject │ X-B3-ParentSpanId │Extract │ │ ParentSpanId │ │ │ │ ├─┼─────────>│ ├────────┼>│ │ │ │ │ SpanId │ │ │ X-B3-SpanId │ │ │ SpanId │ │ │ │ │ │ │ │ │ │ │ │ │ │ Sampled │ │ │ X-B3-Sampled │ │ │ Sampled │ │ │ └──────────────┘ │ └───────────────────┘ │ └──────────────┘ │ │ │ │ │ └──────────────────┘ └──────────────────┘
這就回答了以前的問題:服務器接收到請求以後作什麼?SpanContext在服務器端怎麼處理?
SpringMvcSofaTracerFilter 這裏有一個成員變量 SpringMvcTracer, 其是 Server Tracer,這裏是邏輯所在。
public class SpringMvcTracer extends AbstractServerTracer { private static volatile SpringMvcTracer springMvcTracer = null; }
具體 SpringMvcSofaTracerFilter 的 doFilter 的大體邏輯以下:
調用 getSpanContextFromRequest 從 request 中獲取 SpanContext,其中使用了 tracer.extract函數。
SofaTracerSpanContext spanContext = (SofaTracerSpanContext)tracer.extract(Builtin.B3_HTTP_HEADERS, new SpringMvcHeadersCarrier(headers));
調用 serverReceive 獲取 Span
springMvcSpan = this.springMvcTracer.serverReceive(spanContext);
SofaTracerSpan serverSpan = sofaTraceContext.pop(); // 取出父親Span,若是不存在,則 sofaTracerSpanContext.setSpanId(sofaTracerSpanContext.nextChildContextId()); // 設定爲下一個child id
sofaTraceContext.push(newSpan); // 把Span放入 SpanContext
Span 設置各類 setTag
調用 this.springMvcTracer.serverSend(String.valueOf(httpStatus)); 來 結束Span。
結束 & report
this.clientReceiveTagFinish(clientSpan, resultCode);
恢復restore parent span
sofaTraceContext.push(clientSpan.getParentSofaTracerSpan());
函數代碼具體以下
public class SpringMvcSofaTracerFilter implements Filter { private SpringMvcTracer springMvcTracer; public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) { if (this.springMvcTracer == null) { this.springMvcTracer = SpringMvcTracer.getSpringMvcTracerSingleton(); } SofaTracerSpan springMvcSpan = null; long responseSize = -1L; int httpStatus = -1; try { HttpServletRequest request = (HttpServletRequest)servletRequest; HttpServletResponse response = (HttpServletResponse)servletResponse; SofaTracerSpanContext spanContext = this.getSpanContextFromRequest(request); springMvcSpan = this.springMvcTracer.serverReceive(spanContext); if (StringUtils.isBlank(this.appName)) { this.appName = SofaTracerConfiguration.getProperty("spring.application.name"); } springMvcSpan.setOperationName(request.getRequestURL().toString()); springMvcSpan.setTag("local.app", this.appName); springMvcSpan.setTag("request.url", request.getRequestURL().toString()); springMvcSpan.setTag("method", request.getMethod()); springMvcSpan.setTag("req.size.bytes", request.getContentLength()); SpringMvcSofaTracerFilter.ResponseWrapper responseWrapper = new SpringMvcSofaTracerFilter.ResponseWrapper(response); filterChain.doFilter(servletRequest, responseWrapper); httpStatus = responseWrapper.getStatus(); responseSize = (long)responseWrapper.getContentLength(); } catch (Throwable var15) { httpStatus = 500; throw new RuntimeException(var15); } finally { if (springMvcSpan != null) { springMvcSpan.setTag("resp.size.bytes", responseSize); this.springMvcTracer.serverSend(String.valueOf(httpStatus)); } } } }
咱們在最初提出的問題,如今都有了解答。
clientSpan = (SofaTracerSpan)this.sofaTracer.buildSpan(operationName).asChildOf(serverSpan).start();
獲得自己的 client Span。開放分佈式追蹤(OpenTracing)入門與 Jaeger 實現
OpenTracing Java Library教程(3)——跨服務傳遞SpanContext
OpenTracing Java Library教程(1)——trace和span入門
螞蟻金服分佈式鏈路跟蹤組件 SOFATracer 總覽|剖析
螞蟻金服開源分佈式鏈路跟蹤組件 SOFATracer 鏈路透傳原理與SLF4J MDC 的擴展能力剖析
螞蟻金服開源分佈式鏈路跟蹤組件 SOFATracer 採樣策略和源碼剖析
https://github.com/sofastack-guides/sofa-tracer-guides
The OpenTracing Semantic Specification
螞蟻金服分佈式鏈路跟蹤組件 SOFATracer 數據上報機制和源碼剖析