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

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

0x00 摘要

SOFA是螞蟻金服自主研發的金融級分佈式中間件,包含了構建金融級雲原生架構所需的各個組件,SOFATracer 是其中用於分佈式系統調用跟蹤的組件。html

筆者以前有過zipkin的經驗,但願擴展到Opentracing,因而在學習SOFATracer官方博客結合源碼的基礎上總結出此文,與你們分享。java

0x01 原因 & 問題

1.1 選擇

爲何選擇了從SOFATracer入手來學習?理由很簡單:有大公司背書(是在金融場景裏錘鍊出來的最佳實踐),有開發者和社區整理的官方博客,有直播,示例簡便易調試,爲何不研究使用呢?git

1.2 問題

讓咱們用問題來引導閱讀。github

  • spanId是怎麼生成的,有什麼規則?
  • traceId是怎麼生成的,有什麼規則?
  • 客戶端哪裏生成的Span?
  • ParentSpan 從哪兒來?
  • ChildSpan由ParentSpan建立,何時建立?
  • Trace信息怎麼傳遞?
  • 服務器接收到請求以後作什麼?
  • SpanContext在服務器端怎麼處理?
  • 鏈路信息如何蒐集?

1.3 本文討論範圍

全鏈路跟蹤分紅三個跟蹤級別:web

  • 跨進程跟蹤 (cross-process)(調用另外一個微服務)
  • 數據庫跟蹤
  • 進程內部的跟蹤 (in-process)(在一個函數內部的跟蹤)

本文只討論 跨進程跟蹤 (cross-process),由於跨進程跟蹤是最簡單的 ,容易上手^_^。對於跨進程跟蹤,你能夠編寫攔截器或過濾器來跟蹤每一個請求,它只須要編寫極少的代碼。spring

0x02 背景知識

2.1 趨勢和挑戰

容器、Serverless 編程方式的誕生極大提高了軟件交付與部署的效率。在架構的演化過程當中,能夠看到兩個變化:數據庫

  • 應用架構開始從單體系統逐步轉變爲微服務,其中的業務邏輯隨之而來就會變成微服務之間的調用與請求。
  • 資源角度來看,傳統服務器這個物理單位也逐漸淡化,變成了看不見摸不到的虛擬資源模式。

從以上兩個變化能夠看到這種彈性、標準化的架構背後,原先運維與診斷的需求也變得愈來愈複雜。如何理清服務依賴調用關係、如何在這樣的環境下快速 debug、追蹤服務處理耗時、查找服務性能瓶頸、合理對服務的容量評估都變成一個棘手的事情。編程

2.2 可觀察性(Observability)

爲了應對這些問題,可觀察性(Observability) 這個概念被引入軟件領域。傳統的監控和報警主要關注系統的異常狀況和失敗因素,可觀察性更關注的是從系統自身出發,去展示系統的運行情況,更像是一種對系統的自我審視。一個可觀察的系統中更關注應用自己的狀態,而不是所處的機器或者網絡這樣的間接證據。咱們但願直接獲得應用當前的吞吐和延遲信息,爲了達到這個目的,咱們就須要合理主動暴露更多應用運行信息。在當前的應用開發環境下,面對複雜系統咱們的關注將逐漸由點 到 點線面體的結合,這能讓咱們更好的理解系統,不只知道What,更能回答Why。後端

可觀察性目前主要包含如下三大支柱:api

  • 日誌(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、OkHttp三、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 鏈路追蹤剖析

相關文章
相關標籤/搜索