1. 程式人生 > 實用技巧 >[業界方案] 用SOFATracer學習分散式追蹤系統Opentracing

[業界方案] 用SOFATracer學習分散式追蹤系統Opentracing

[業界方案] 用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 介於 LoggingMetric 之間, 以請求的維度來串聯服務間的呼叫關係並記錄呼叫耗時,即保留了必要的資訊,又將分散的日誌事件通過 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開關實現對特定請求的全取樣);
  • 依賴梳理——基於呼叫關係生成服務依賴圖;
  • 效能分析和優化——可以方便的記錄統計系統鏈路上不同處理單元的耗時佔用和佔比;
  • 容量規劃與評估;
  • 配合LoggingMetric強化監控和報警。

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。它有以下的職責:

  1. 建立和開啟一個span
  2. 從某種媒介中提取/注入一個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 spanparent span 建立了直接因果關係。

3.4 SpanContext

表示一個span對應的上下文,span和spanContext基本上是一一對應的關係,這個SpanContext可以通過某些媒介和方式傳遞給呼叫鏈的下游來做一些處理(例如子Span的id生成、資訊的繼承列印日誌等等)。

上下文儲存的是一些需要跨越邊界的(傳播跟蹤所需的)一些資訊,例如:

  • spanId :當前這個span的id
  • traceId :這個span所屬的traceId(也就是這次呼叫鏈的唯一id)。
    • trace_idspan_id 用以區分Trace中的Span;任何 OpenTraceing 實現相關的狀態(比如 trace 和 span id)都需要被一個跨程序的 Span 所聯絡。
  • baggage :其他的能過跨越多個呼叫單元的資訊,即跨程序的 key value 對。Baggage ItemsSpan 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可能處在下列的狀態中:

  1. 等待子span完成
  2. 等待某種阻塞方法
  3. 建立但是並未開始

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 中將所有核心的元件都宣告為介面,例如 TracerSpanSpanContextFormat(高版本中還包括 ScopeScopeManager)等。SOFATracer 使用的版本是 0.22.0 ,主要是對 TracerSpanSpanContext 三個概念模型的實現。下面就針對幾個元件結合 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 介面,並擴充套件了對 Referencetags、執行緒非同步處理以及外掛擴充套件中所必須的 logType和產生當前 spanTracer型別等處理的能力。

每個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 例項成為不可變的,以避免 Spanfinishreference 操作時會有複雜的生命週期問題。” 這裡是可以理解的,如果 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。
  • 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_idspan_id 用以區分Trace中的Span;任何 OpenTraceing 實現相關的狀態(比如 trace 和 span id)都需要被一個跨程序的 Span 所聯絡。
  • baggage :其他的能過跨越多個呼叫單元的資訊,即跨程序的 key value 對。Baggage ItemsSpan 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 程式碼功能類似。
    • 恢復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;
  • 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

開放分散式追蹤(OpenTracing)入門與 Jaeger 實現

OpenTracing 語義說明

分散式追蹤系統概述及主流開源系統對比

Skywalking分散式追蹤與監控:起始篇

分散式全鏈路監控 -- opentracing小試

opentracing實戰

Go微服務全鏈路跟蹤詳解

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 資料上報機制和原始碼剖析

螞蟻金服開源分散式鏈路跟蹤元件 SOFATracer 埋點機制剖析

分散式鏈路元件 SOFATracer 埋點機制解析

【剖析 | SOFARPC 框架】之 SOFARPC 鏈路追蹤剖析