[業界方案] 用SOFATracer學習分布式追蹤系統Opentracing
0x00 摘要
SOFA是螞蟻金服自主研發的金融級分布式中間件,包含了構建金融級雲原生架構所需的各個組件,SOFATracer 是其中用於分布式系統調用跟蹤的組件。
筆者之前有過zipkin的經驗,希望擴展到Opentracing,於是在學習SOFATracer官方博客結合源碼的基礎上總結出此文,與大家分享。
0x01 緣由 & 問題
1.1 選擇
為什么選擇了從SOFATracer入手來學習?理由很簡單:有大公司背書(是在金融場景里錘煉出來的最佳實踐),有開發者和社區整理的官方博客,有直播,示例簡便易調試,為什么不研究使用呢?
1.2 問題
讓我們用問題來引導閱讀。
- spanId是怎么生成的,有什么規則?
- traceId是怎么生成的,有什么規則?
- 客戶端哪里生成的Span?
- ParentSpan 從哪兒來?
- ChildSpan由ParentSpan創建,什么時候創建?
- Trace信息怎么傳遞?
- 服務器接收到請求之后做什么?
- SpanContext在服務器端怎么處理?
- 鏈路信息如何搜集?
1.3 本文討論范圍
全鏈路跟蹤分成三個跟蹤級別:
- 跨進程跟蹤 (cross-process)(調用另一個微服務)
- 數據庫跟蹤
- 進程內部的跟蹤 (in-process)(在一個函數內部的跟蹤)
本文只討論 跨進程跟蹤 (cross-process),因為跨進程跟蹤是最簡單的 ,容易上手_。對於跨進程跟蹤,你可以編寫攔截器或過濾器來跟蹤每個請求,它只需要編寫極少的代碼。
0x02 背景知識
2.1 趨勢和挑戰
容器、Serverless 編程方式的誕生極大提升了軟件交付與部署的效率。在架構的演化過程中,可以看到兩個變化:
- 應用架構開始從單體系統逐步轉變為微服務,其中的業務邏輯隨之而來就會變成微服務之間的調用與請求。
- 資源角度來看,傳統服務器這個物理單位也逐漸淡化,變成了看不見摸不到的虛擬資源模式。
從以上兩個變化可以看到這種彈性、標准化的架構背后,原先運維與診斷的需求也變得越來越復雜。如何理清服務依賴調用關系、如何在這樣的環境下快速 debug
、追蹤服務處理耗時、查找服務性能瓶頸、合理對服務的容量評估都變成一個棘手的事情。
2.2 可觀察性(Observability)
為了應對這些問題,可觀察性(Observability
) 這個概念被引入軟件領域。傳統的監控和報警主要關注系統的異常情況和失敗因素,可觀察性更關注的是從系統自身出發,去展現系統的運行狀況,更像是一種對系統的自我審視。一個可觀察的系統中更關注應用本身的狀態,而不是所處的機器或者網絡這樣的間接證據。我們希望直接得到應用當前的吞吐和延遲信息,為了達到這個目的,我們就需要合理主動暴露更多應用運行信息。在當前的應用開發環境下,面對復雜系統我們的關注將逐漸由點 到 點線面體的結合,這能讓我們更好的理解系統,不僅知道What,更能回答Why。
可觀察性目前主要包含以下三大支柱:
- 日志(
Logging
) :Logging
主要記錄一些離散的事件,應用往往通過將定義好格式的日志信息輸出到文件,然后用日志收集程序收集起來用於分析和聚合。雖然可以用時間將所有日志點事件串聯起來,但是卻很難展示完整的調用關系路徑; - 度量(
Metrics
) :Metric
往往是一些聚合的信息,相比Logging
喪失了一些具體信息,但是占用的空間要比完整日志小的多,可以用於監控和報警,在這方面 Prometheus 已經基本上成為了事實上的標准; - 分布式追蹤(
Tracing
) :Tracing
介於Logging
和Metric
之間, 以請求的維度來串聯服務間的調用關系並記錄調用耗時,即保留了必要的信息,又將分散的日志事件通過 Span 串聯,幫助我們更好的理解系統的行為、輔助調試和排查性能問題。
三大支柱有如下特點:
- Metric的特點是,它是可累加的。具有原子性,每個都是一個邏輯計量單元,或者一個時間段內的柱狀圖。 例如:隊列的當前深度可以被定義為一個計量單元,在寫入或讀取時被更新統計; 輸入HTTP請求的數量可以被定義為一個計數器,用於簡單累加;請求的執行時間可以被定義為一個柱狀圖,在指定時間片上更新和統計匯總。
- Logging的特點是,它描述一些離散的(不連續的)事件。 例如:應用通過一個滾動的文件輸出debug或error信息,並通過日志收集系統,存儲到Elasticsearch中;審批明細信息通過Kafka,存儲到數據庫(BigTable)中; 又或者,特定請求的元數據信息,從服務請求中剝離出來,發送給一個異常收集服務,如NewRelic。
- Tracing的最大特點就是,它在單次請求的范圍內處理信息。 任何的數據、元數據信息都被綁定到系統中的單個事務上。 例如:一次調用遠程服務的RPC執行過程;一次實際的SQL查詢語句;一次HTTP請求的業務性ID。
2.3 Tracing
分布式追蹤,也稱為分布式請求追蹤,是一種用於分析和監視應用程序的方法,特別是那些使用微服務體系結構構建的應用程序;分布式追蹤有助於查明故障發生的位置以及導致性能低下的原因,開發人員可以使用分布式跟蹤來幫助調試和優化他們的代碼,IT和DevOps團隊可以使用分布式追蹤來監視應用程序。
2.3.1 Tracing 的誕生
Tracing 是在90年代就已出現的技術。但真正讓該領域流行起來的還是源於 Google 的一篇論文”Dapper, a Large-Scale Distributed Systems Tracing Infrastructure”,而另一篇論文”Uncertainty in Aggregate Estimates from Sampled Distributed Traces”中則包含關於采樣的更詳細分析。論文發表后一批優秀的 Tracing 軟件孕育而生。
2.3.2 Tracing的功能
- 故障定位——可以看到請求的完整路徑,相比離散的日志,更方便定位問題(由於真實線上環境會設置采樣率,可以利用debug開關實現對特定請求的全采樣);
- 依賴梳理——基於調用關系生成服務依賴圖;
- 性能分析和優化——可以方便的記錄統計系統鏈路上不同處理單元的耗時占用和占比;
- 容量規划與評估;
- 配合
Logging
和Metric
強化監控和報警。
2.4 OpenTracing
為了解決不同的分布式追蹤系統 API 不兼容的問題,出現了OpenTracing。OpenTracing旨在標准化Trace數據結構和格式,其目的是:
- 不同語言開發的Trace客戶端的互操作性。Java/.Net/PHP/Python/NodeJs等語言開發的客戶端,只要遵循OpenTracing標准,就都可以對接OpenTracing兼容的監控后端。
- Tracing監控后端的互操作性。只要遵循OpenTracing標准,企業可以根據需要替換具體的Tracing監控后端產品,比如從Zipkin替換成Jaeger/CAT/Skywalking等后端。
OpenTracing不是一個標准,OpenTracing API提供了一個標准的、與供應商無關的框架,是對分布式鏈路中涉及到的一些列操作的高度抽象集合。這意味着如果開發者想要嘗試一種不同的分布式追蹤系統,開發者只需要簡單地修改Tracer配置即可,而不需要替換整個分布式追蹤系統。
0x03 OpenTracing 數據模型
大多數分布式追蹤系統的思想模型都來自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
發揮着重要作用。
3.1 Span
表示分布式調用鏈條中的一個調用單元,他的邊界包含一個請求進到服務內部再由某種途徑(http/dubbo等)從當前服務出去。
一個span一般會記錄這個調用單元內部的一些信息,例如每個Span
包含的操作名稱、開始和結束時間、附加額外信息的Span Tag
、可用於記錄Span
內特殊事件Span Log
、用於傳遞Span
上下文的SpanContext
和定義Span
之間關系的References
。
- Operation 的 名字(An operation name)
- 開始時間 (A start timestamp)
- 結束時間 (A finish timestamp)
- 標簽信息 :0個或多個以 keys:values 為形式組成的 Span Tags。 key 必須是 string, values 則可以是 strings, bool,numeric types
- 日志信息 :0個或多個 Span logs
- 一個 SpanContext
- 通過 SpanContext 可以指向 0個 或者多個 因果相關的 Span。
3.2 Tracer
Trace 描述在分布式系統中的一次"事務"。一個trace是由若干span組成的有向無環圖。
Tracer 用於創建Span,並理解如何跨進程邊界注入(序列化)和提取(反序列化)Span。它有以下的職責:
- 建立和開啟一個span
- 從某種媒介中提取/注入一個spanContext
用圖論的觀點來看的話,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··]
3.3 References between Spans
一個span可以和一個或者多個span間存在因果關系。OpenTracing定義了兩種關系:ChildOf 和 FollowsFrom。這兩種引用類型代表了子節點和父節點間的直接因果關系。
ChildOf
將成為當前 Span 的 child,而 FollowsFrom
則會成為 parent。 這兩種關系為 child span 和 parent span 建立了直接因果關系。
3.4 SpanContext
表示一個span對應的上下文,span和spanContext基本上是一一對應的關系,這個SpanContext可以通過某些媒介和方式傳遞給調用鏈的下游來做一些處理(例如子Span的id生成、信息的繼承打印日志等等)。
上下文存儲的是一些需要跨越邊界的(傳播跟蹤所需的)一些信息,例如:
- 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
數據結構簡化版如下:
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
的注入和提取。
- “extarct()”從媒介(通常是HTTP頭)獲取跟蹤上下文。
- “inject()”將跟蹤上下文放入媒介,來保證跟蹤鏈的連續性。
3.5 Carrier
Carrier 表示的是一個承載spanContext的媒介,比方說在http調用場景中會有HttpCarrier,在dubbo調用場景中也會有對應的DubboCarrier。
3.6 Formatter
這個接口負責了具體場景中序列化反序列化上下文的具體邏輯,例如在HttpCarrier使用中通常就會有一個對應的HttpFormatter。Tracer的注入和提取就是委托給了Formatter。
3.7 ScopeManager
這個類是0.30版本之后新加入的組件,這個組件的作用是能夠通過它獲取當前線程中啟用的Span信息,並且可以啟用一些處於未啟用狀態的span。在一些場景中,我們在一個線程中可能同時建立多個span,但是同一時間同一線程只會有一個span在啟用,其他的span可能處在下列的狀態中:
- 等待子span完成
- 等待某種阻塞方法
- 創建但是並未開始
3.8 Reporter
除了上述組件之外,在實現一個分布式全鏈路監控框架的時候,還需要有一個reporter組件,通過它來打印或者上報一些關鍵鏈路信息(例如span創建和結束),只有把這些信息進行處理之后我們才能對全鏈路信息進行可視化和真正的監控。
0x04 SOFATracer
SOFATracer 是一個用於分布式系統調用跟蹤的組件,通過統一的 traceId 將調用鏈路中的各種網絡調用情況以日志的方式記錄下來,以達到透視化網絡調用的目的。這些日志可用於故障的快速發現,服務治理等。
SOFATracer 團隊已經為我們搭建了一個完整的 Tracer 框架內核,包括數據模型、編碼器、跨進程透傳 traceId、采樣、日志落盤與上報等核心機制,並提供了擴展 API 及基於開源組件實現的部分插件,為我們基於該框架打造自己的 Tracer 平台提供了極大便利。
SOFATracer 目前並沒有提供數據采集器和 UI 展示的功能;主要有兩個方面的考慮:
- SOFATracer 作為 SOFA 體系中一個非常輕量的組件,意在將 span 數據以日志的方式落到磁盤,以便於用戶能夠更加靈活的來處理這些數據
- UI 展示方面,SOFATracer 本身基於 OpenTracing 規范實現,在模型上與開源的一些產品可以實現無縫對接,在一定程度上可以彌補本身在鏈路可視化方面的不足。
因此在上報模型上,SOFATracer 提供了日志輸出和外部上報的擴展,方便接入方能夠足夠靈活的方式來處理上報的數據。通過SOFARPC + SOFATracer + zipKin 可以快速搭建一套完整的鏈路追蹤系統,包括埋點、收集、分析展示等。 收集和分析主要是借用zipKin的能力。
目前 SOFATracer 已經支持了對以下開源組件的埋點支持:Spring MVC、RestTemplate、HttpClient、OkHttp3、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
來分析。
4.1 Tracer & 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;
}
4.2 Span & SofaTracerSpan
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是服務端收到客戶端時間 到 發送響應結果給客戶端的這段過程。
4.3 SpanContext & SofaTracerSpanContext
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();
}
4.3.1 傳遞Trace信息
本小節回答了 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);
}
4.3.2 線程存儲
在鏈路環節每個節點中,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();
}
}
4.4 Reporter
日志落盤又分為摘要日志落盤 和 統計日志落盤;
- 摘要日志是每一次調用均會落地磁盤的日志;
- 統計日志是每隔一定時間間隔進行統計輸出的日志。
數據上報是 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 :
- SofaTracerCompositeDigestReporterImpl:組合摘要日志上報實現,上報時會遍歷當前 SofaTracerCompositeDigestReporterImpl 中所有的 Reporter ,逐一執行 report 操作;可供外部用戶擴展使用。
- DiskReporterImpl:數據落磁盤的核心實現類,也是目前 SOFATracer 中默認使用的上報器。
0x05 示例代碼
5.1 RestTemplate
我們使用的是 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 .......");
}
}
0x06 啟動
這里首先要提一下SOFATracer 的埋點機制,不同組件有不同的應用場景和擴展點,因此對插件的實現也要因地制宜,SOFATracer 埋點方式一般是通過 Filter、Interceptor 機制實現的。所以下面我們提到的Client啟動 / Server 啟動就主要是創建了 Filter、Interceptor 機制。
我們就以 RestTemplate 為例看看SofaTracer的啟動。
6.1 Spring SPI
代碼中只用到 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 類管理。
6.2 Client啟動
這部分代碼是 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;
}
}
6.3 服務端啟動
這部分代碼是 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();
}
}
}
0x07 SOFATracer 的插件埋點機制
對一個應用的跟蹤要關注的無非就是 客戶端--->web 層--->rpc 服務--->dao 后端存儲、cache 緩存、消息隊列 mq 等這些基礎組件
。SOFATracer 插件的作用實際上也就是對不同組件進行埋點,以便基於這些組件采集應用的鏈路數據。
不同組件有不同的應用場景和擴展點,因此對插件的實現也要因地制宜,SOFATracer 埋點方式一般是通過 Filter、Interceptor 機制實現的。
7.1 組件擴展入口之 Filter or 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 類。
7.2 插件擴展基本思路總結
對於一個組件來說,一次處理過程一般是產生一個 Span;這個 Span 的生命周期是從接收到請求到返回響應這段過程。
但是這里需要考慮的問題是如何與上下游鏈路關聯起來呢?在 Opentracing 規范中,可以在 Tracer 中 extract 出一個跨進程傳遞的 SpanContext 。然后通過這個 SpanContext 所攜帶的信息將當前節點關聯到整個 Tracer 鏈路中去,當然有提取(extract)就會有對應的注入(inject)。
鏈路的構建一般是 client------server------client------server 這種模式的,那這里就很清楚了,就是會在 client 端進行注入(inject),然后再 server 端進行提取(extract),反復進行,然后一直傳遞下去。
在拿到 SpanContext 之后,此時當前的 Span 就可以關聯到這條鏈路中了,那么剩余的事情就是收集當前組件的一些數據;整個過程大概分為以下幾個階段:
- 從請求中提取 spanContext
- 構建 Span,並將當前 Span 存入當前 tracer上下文中(SofaTraceContext.push(Span)) 。
- 設置一些信息到 Span 中
- 返回響應
- Span 結束&上報
7.3 標准 Servlet 規范埋點原理
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 的過程大致如下:
- Servlet Filter 攔截到 request 請求;
- 從請求中解析 SpanContext;
- 通過 SpanContext 構建當前 MVC 的 Span;
- 給當前 Span 設置 tag、log;
- 在 Filter 處理的最后,結束 Span;
7.4 HTTP 客戶端埋點原理
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;
}
0x08 請求總體過程
在 SOFATracer 中將請求大致分為以下幾個過程:
- 客戶端發送請求 clientSend cs
- 服務端接受請求 serverReceive sr
- 服務端返回結果 serverSend ss
- 客戶端接受結果 clientReceive cr
無論是哪個插件,在請求處理周期內都可以從上述幾個階段中找到對應的處理方法。因此,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
8.1 TraceID
產生trace ID 是在 客戶端發送請求 clientSend cs 這個階段,即,此 ID 一般由集群中第一個處理請求的系統產生,並在分布式調用下通過網絡傳遞到下一個被請求系統。就是 AbstractTracer # clientSend 函數。
-
調用 buildSpan 構建一個 SofaTracerSpan clientSpan,然后調用 start 函數建立一個 Span。
-
如果不存在Parent context,則調用 createRootSpanContext 建立了 new root span context。
-
sofaTracerSpanContext = this.createRootSpanContext();
- 調用 String traceId = TraceIdGenerator.generate(); 來構建 trace ID。
-
-
如果存在 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) {
}
}
}
8.2 SpanID
有兩個地方會生成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。
8.2.1 Client Send
接上面小節,在客戶端發送請求 clientSend cs 這個階段,就會構建Span,從而生成 SpanID。
-
調用 buildSpan 構建一個 SofaTracerSpan clientSpan,然后調用 start 函數建立一個 Span。
-
如果不存在Parent context,則調用 createRootSpanContext 建立了 new root span context。
-
sofaTracerSpanContext = this.createRootSpanContext();
- 調用 SofaTracerSpanContext 生成新的SpanContext,里面就生成了新的Span ID。
-
-
如果存在 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());
-
-
8.2.2 Server Receive
我們再以 Server Receive這個動作為例,可以看到在Server端 的 Span構建過程。
-
SpringMvcSofaTracerFilter # doFilter 會從 Header 中提取 SofaTracerSpanContext。
- 利用 SofaTracer # extract 提取SofaTracerSpanContext,這里用到了 SpringMvcHeadersCarrier。
- 利用 RegistryExtractorInjector # extract 從 SpringMvcHeadersCarrier 中提取 SpanContext。
- 利用 AbstractTextB3Formatter # extract 從 SpringMvcHeadersCarrier 中提取 SpanContext。
- 利用 RegistryExtractorInjector # extract 從 SpringMvcHeadersCarrier 中提取 SpanContext。
- 利用 SofaTracer # extract 提取SofaTracerSpanContext,這里用到了 SpringMvcHeadersCarrier。
-
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();
}
}
0x09 Client 發送
本節我們看看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 的攔截器中,這里會做處理。
9.1 生成Span
具體實現代碼是在 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中。
- 給Span加入各種Tag,比如 app, url, method...
- 進行Carrier處理
,injectCarrier(request, sofaTracerSpan);
- 調用 AbstractTextB3Formatter.inject 設置 traceId, spanId, parentId ....
-
發送請求。
-
收到服務器返回之后進一步處理。
-
從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);
}
}
}
9.2 Fomatter
上文提到了發送時候會調用 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)”的函數:
- “extract(format,carrier)”從媒介(通常是HTTP頭)獲取跟蹤上下文。
- “inject(SpanContext,format,carrier)” 將跟蹤上下文放入媒介,來保證跟蹤鏈的連續性。
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}
9.3 Report
發送的最后一步是 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。
9.4 采樣計算
采樣是對於整條鏈路來說的,也就是說從 RootSpan 被創建開始,就已經決定了當前鏈路數據是否會被記錄了。在 SofaTracer 類中,Sapmler 實例作為成員變量存在,並且被設置為 final,也就是當構建好 SofaTracer 實例之后,采樣策略就不會被改變。當 Sampler 采樣器綁定到 SofaTracer 實例之后,SofaTracer 對於產生的 Span 數據的落盤行為都會依賴采樣器的計算結果(針對某一條鏈路而言)。
0x10 服務端接收
類 SpringMvcSofaTracerFilter 完成了服務端接收相關工作。主要就是設置 SpanContext 和 Span。
public class SpringMvcSofaTracerFilter implements Filter {
private SpringMvcTracer springMvcTracer;
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
FilterChain filterChain) {
......
}
}
回憶下:在 client 端就是
- 將當前請求線程的產生的 traceId 相關信息 Inject 到 SpanContext。
- 然后通過 Fomatter 將 SpanContext序列化到Header之中。
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);
- 設置log,resultCode,結束Client Span :clientSpan.finish();
- 調用 SofaTracer # reportSpan 來 report。這部分和 Client 代碼功能類似。
- 設置log,resultCode,結束Client Span :clientSpan.finish();
-
-
恢復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));
}
}
}
}
0x11 問題解答
我們在最初提出的問題,現在都有了解答。
- traceId是怎么生成的,有什么規則?答案如下:
- 在clientSend cs 這個階段,建立Span時候,如果不存在 Parent context,則調用 createRootSpanContext 建立了 new root span context。此時會生成一個 traceId
- TraceId 是由 ip,時間戳,遞增序列,進程ID等構成,具體可以參見 TraceIdGenerator 類。
- spanId是怎么生成的,有什么規則?答案如下:
- 在 Server Receive 這個階段,如果當前線程SpanContext中沒有Span,則生成一個新的 newSpan,然后調用 setSpanId 對傳入的 SofaTracerSpanContext 參數進行設置新的 SpanId。
- 規則很簡單,就是在之前Span ID基礎上單調遞增,參見 SofaTracerSpanContext #nextChildContextId。
- 客戶端哪里生成的Span?答案如下:
- 在 客戶端發送請求 clientSend cs 這個階段,就是 AbstractTracer # clientSend 函數,調用 buildSpan 構建一個 SofaTracerSpan clientSpan,然后調用 start 函數建立一個 Span。
- ParentSpan 從哪兒來?答案如下:
- 在 clientSend 階段,先從 SofaTraceContext 取出 serverSpan。如果本 client 就是 一個服務中間點(即 serverSpan 不為空),則 serverSpan 就是 parentSpan,那么需要給新span設置父親Span。
- ChildSpan由ParentSpan創建,那么什么時候創建?答案如下:
- 接上面回答,如果存在 ParentSpan,則調用
clientSpan = (SofaTracerSpan)this.sofaTracer.buildSpan(operationName).asChildOf(serverSpan).start();
得到本身的 client Span。 - 即如果存在active span ,若存在則生成CHILD_OF關系的上下文, 如果不存在則createNewContext;
- 接上面回答,如果存在 ParentSpan,則調用
- Trace信息怎么傳遞?答案如下:
- OpenTracing之中是通過SpanContext來傳遞Trace信息。
- SpanContext存儲的是一些需要跨越邊界的一些信息,比如trace Id,span id,Baggage。這些信息會不同組件根據自己的特點序列化進行傳遞,比如序列化到 http header 之中再進行傳遞。
- 然后通過這個 SpanContext 所攜帶的信息將當前節點關聯到整個 Tracer 鏈路中去
- 服務器接收到請求之后做什么?答案如下:
- server 端則是 從請求的 Header 中 extract 出 spanContext,來還原本次請求線程的上下文。因為上下文是和所處理的線程相關,放入 ThreadLocal中。
- SpanContext在服務器端怎么處理?答案見上面回答。
- 鏈路信息如何搜集?答案如下:
- 采樣是對於整條鏈路來說的,也就是說從 RootSpan 被創建開始,就已經決定了當前鏈路數據是否會被記錄了。
- 在 SofaTracer 類中,Sapmler 實例作為成員變量存在,並且被設置為 final,也就是當構建好 SofaTracer 實例之后,采樣策略就不會被改變。當 Sampler 采樣器綁定到 SofaTracer 實例之后,SofaTracer 對於產生的 Span 數據的落盤行為都會依賴采樣器的計算結果(針對某一條鏈路而言)。
0xFF 參考
開放分布式追蹤(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 數據上報機制和源碼剖析