歡迎光臨
每天分享高質量文章

分散式鏈路追蹤 SkyWalking 原始碼分析 —— Agent 收集 Trace 資料

本文主要基於 SkyWalking 3.2.6 正式版

  • 1. 概述
  • 2. Trace
    • 2.1 ID
    • 2.2 AbstractSpan
    • 2.3 TraceSegmentRef
  • 2.4 TraceSegment
  • 3. Context
    • 3.1 ContextManager
    • 3.2 AbstractTracerContext
    • 3.3 SamplingService

1. 概述

分散式鏈路追蹤系統,鏈路的追蹤大體流程如下:

  1. Agent 收集 Trace 資料
  2. Agent 傳送 Trace 資料給 Collector 。
  3. Collector 接收 Trace 資料。
  4. Collector 儲存 Trace 資料到儲存器,例如,資料庫。

本文主要分享【第一部分】 SkyWalking Agent 收集 Trace 資料。文章的內容順序如下:

  • Trace 的資料結構
  • Context 收集 Trace 的方法

不包括外掛對 Context 收集的方法的呼叫,後續單獨文章專門分享,胖友也可以閱讀完本文後,自己去看 apm-sdk-plugin 的實現程式碼。

本文涉及到的程式碼如下圖:

  • 紅框部分:Trace 的資料結構,在 「2. Trace」 分享。
  • 黃框部分:Context 收集 Trace 的方法,在 「3. Context」 分享。

2. Trace

友情提示:胖友,請先行閱讀 《OpenTracing語意標準》 。

本小節,筆者認為胖友已經對 OpenTracing 有一定的理解。

org.skywalking.apm.agent.core.context.trace.TraceSegment ,是一次分散式鏈路追蹤( Distributed Trace ) 的一段

  • 一條 TraceSegment ,用於記錄所在執行緒( Thread )的鏈路。
  • 一次分散式鏈路追蹤,可以包含多條 TraceSegment ,因為存在跨行程( 例如,RPC 、MQ 等等),或者垮執行緒( 例如,併發執行、非同步回呼等等 )。

TraceSegment 屬性,如下:

  • traceSegmentId 屬性,TraceSegment 的編號,全域性唯一。在 「2.1 ID」 詳細解析。
  • refs 屬性,TraceSegmentRef 陣列,指向的 TraceSegment 陣列。
    • 為什麼會有多個爸爸?下麵統一講。
    • TraceSegmentRef ,在 「2.3 TraceSegmentRef」 詳細解析。
  • relatedGlobalTraces 屬性,關聯的 DistributedTraceId 陣列
    • 為什麼會有多個爸爸?下麵統一講。
    • DistributedTraceId ,在 「2.1.2 DistributedTraceId」 詳細解析。
  • spans 屬性,包含的 Span 陣列。在 「2.2 AbstractSpan」 詳細解析。這是 TraceSegment 的主體,總的來說,TraceSegment 是 Span 陣列的封裝。
  • ignore 屬性,是否忽略該條 TraceSegment 。在一些情況下,我們會忽略 TraceSegment ,即不收集鏈路追蹤,在下麵 「3. Context」 部分內容,我們將會看到這些情況。
  • isSizeLimited 屬性,Span 是否超過上限( `Config.Agent.SPAN_LIMIT_PER_SEGMENT` )。超過上限,不在記錄 Span 。

為什麼會有多個爸爸

  • 我們先來看看一個爸爸的情況,常見於 RPC 呼叫。例如,【服務 A】呼叫【服務 B】時,【服務 B】新建一個 TraceSegment 物件:
    • 將自己的 `refs` 指向【服務 A】的 TraceSegment 。
    • 將自己的 `relatedGlobalTraces` 設定為 【服務 A】的 DistributedTraceId 物件。
  • 我們再來看看多個爸爸的情況,常見於 MQ / Batch 呼叫。例如,MQ 批次消費訊息時,訊息來自【多個服務】。每次批次消費時,【消費者】新建一個 TraceSegment 物件:
    • 將自己的 `refs` 指向【多個服務】的多個 TraceSegment 。
    • 將自己的 `relatedGlobalTraces` 設定為【多個服務】的多個 DistributedTraceId 。

友情提示:多個爸爸的故事,可能比較難懂,等胖友讀完全文,在回過頭想想。或者拿起來程式碼除錯除錯。

下麵,我們來具體看看 TraceSegment 的每個元素,最後,我們會回過頭,在 「2.4 TraceSegment」 詳細解析它。

2.1 ID

org.skywalking.apm.agent.core.context.ids.ID ,編號。從類的定義上,這是一個通用的編號,由三段整陣列成。

目前使用 GlobalIdGenerator 生成,作為全域性唯一編號。屬性如下:

  • part1 屬性,應用實體編號。
  • part2 屬性,執行緒編號。
  • part3 屬性,時間戳串,生成方式為 ${時間戳} * 10000 + 執行緒自增序列([0, 9999]) 。例如:15127007074950012 。具體生成方法的程式碼,在 GlobalIdGenerator 中詳細解析。
  • encoding 屬性,編碼後的字串。格式為 "${part1}.${part2}.${part3}" 。例如,"12.35.15127007074950000" 。
    • 使用 `#encode()` 方法,編碼編號。
  • isValid 屬性,編號是否合法。
    • 使用 `ID(encodingString)` 構造方法,解析字串,生成 ID 。

2.1.1 GlobalIdGenerator

org.skywalking.apm.agent.core.context.ids.GlobalIdGenerator ,全域性編號生成器。

#generate() 方法,生成 ID 物件。程式碼如下:

  • 第 67 行:獲得執行緒對應的 IDContext 物件。
  • 第 69 至 73 行:生成 ID 物件。
    • 第 70 行:`ID.part1` 屬性,應用編號實體。
    • 第 71 行:`ID.part2` 屬性,執行緒編號。
    • 第 72 行:`ID.part3` 屬性,呼叫 `IDContext#nextSeq()` 方法,生成帶有時間戳的序列號。
  • ps :程式碼比較易懂,已經新增完成註釋。

2.1.2 DistributedTraceId

org.skywalking.apm.agent.core.context.ids.DistributedTraceId ,分散式鏈路追蹤編號抽象類

  • id 屬性,全域性編號。

DistributedTraceId 有兩個實現類:

  • org.skywalking.apm.agent.core.context.ids.NewDistributedTraceId ,新建的分散式鏈路追蹤編號。當全域性鏈路追蹤開始,建立 TraceSegment 物件的過程中,會呼叫 `DistributedTraceId()` 構造方法,建立 DistributedTraceId 物件。該構造方法內部會呼叫 GlobalIdGenerator#generate() 方法,建立 ID 物件。
  • org.skywalking.apm.agent.core.context.ids.PropagatedTraceId ,傳播的分散式鏈路追蹤編號。例如,A 服務呼叫 B 服務時,A 服務會將 DistributedTraceId 物件帶給 B 服務,B 服務會呼叫 `PropagatedTraceId(String id)` 構造方法 ,建立 PropagatedTraceId 物件。該構造方法內部會解析 id ,生成 ID 物件。

2.1.3 DistributedTraceIds

org.skywalking.apm.agent.core.context.ids.DistributedTraceIds ,DistributedTraceId 陣列的封裝。

  • relatedGlobalTraces 屬性,關聯的 DistributedTraceId 鏈式陣列。

#append(DistributedTraceId) 方法,新增分散式鏈路追蹤編號( DistributedTraceId )。程式碼如下:

  • 第 51 至 54 行:移除首個 NewDistributedTraceId 物件。為什麼呢?在 「2.4 TraceSegment」 的構造方法中,會預設建立 NewDistributedTraceId 物件。在跨執行緒、或者跨行程的情況下時,建立的 TraceSegment 物件,需要指向父 Segment 的 DistributedTraceId ,所以需要移除預設建立的。
  • 第 56 至 58 行:新增 DistributedTraceId 物件到陣列。

2.2 AbstractSpan

org.skywalking.apm.agent.core.context.trace.AbstractSpan ,Span 介面( 不是抽象類 ),定義了 Span 通用屬性的介面方法:

  • #getSpanId() 方法,獲得 Span 編號。一個整數,在 TraceSegment 內唯一,從 0 開始自增,在建立 Span 物件時生成。

  • #setOperationName(operationName) 方法,設定操作名。

    • 操作名,定義如下:
    • #setOperationId(operationId) 方法,設定操作編號。考慮到操作名是字串,Agent 傳送給 Collector 佔用流量較大。因此,Agent 會將操作註冊到 Collector ,生成操作編號。在 《SkyWalking 原始碼分析 —— Agent DictionaryManager 字典管理》 有詳細解析。
  • #setComponent(Component) 方法,設定 org.skywalking.apm.network.trace.component.Component ,例如:MongoDB / SpringMVC / Tomcat 等等。目前,官方在 org.skywalking.apm.network.trace.component.ComponentsDefine 定義了目前已經支援的 Component 。

    • #setComponent(componentName) 方法,直接設定 Component 名字。大多數情況下,我們不使用該方法。

      Only use this method in explicit instrumentation, like opentracing-skywalking-bridge.
      It it higher recommend don’t use this for performance consideration.

  • #setLayer(SpanLayer) 方法,設定 org.skywalking.apm.agent.core.context.trace.SpanLayer 。目前有,DB 、RPC_FRAMEWORK 、HTTP 、MQ ,未來會增加 CACHE 。

  • #tag(key, value) 方法,設定鍵值對的標簽。可以呼叫多次,構成 Span 的標簽集合。在 「2.2.1 Tag」 詳細解析。

  • 日誌相關

    • #log(timestampMicroseconds, fields) 方法,記錄一條通用日誌,包含 fields 鍵值對集合。
    • #log(Throwable) 方法,記錄一條異常日誌,包含異常資訊。
  • #errorOccurred() 方法,標記發生異常。大多數情況下,配置 #log(Throwable) 方法一起使用。

  • #start() 方法,開始 Span 。一般情況的實現,設定開始時間。

  • #isEntry() 方法,是否是入口 Span ,在 「2.2.2.1 EntrySpan」 詳細解析。

  • #isExit() 方法,是否是出口 Span ,在 「2.2.2.2 ExitSpan」 詳細解析。

2.2.1 Tag

2.2.1.1 AbstractTag

org.skywalking.apm.agent.core.context.tag.AbstractTag ,標簽抽象類。註意,這個類的用途是將標簽屬性設定到 Span 上,或者說,它是設定 Span 的標簽的工具類。程式碼如下:

  • key 屬性,標簽的鍵。
  • #set(AbstractSpan span, T tagValue) 抽象方法,設定 Span 的標簽鍵 key 的值為 tagValue

2.2.1.2 StringTag

org.skywalking.apm.agent.core.context.tag.StringTag ,值型別為 String 的標簽實現類

  • #set(AbstractSpan span, String tagValue) 實現方法,設定 Span 的標簽鍵 key 的值為 tagValue 。

2.2.1.3 Tags

org.skywalking.apm.agent.core.context.tag.Tags ,常用 Tag 列舉類,內部定義了多個 HTTP 、DB 相關的 StringTag 的靜態變數。

在 《opentracing-specification-zh —— 語意慣例》 裡,定義了標準的 Span Tag 。

2.2.2 AbstractSpan 實現類

AbstractSpan 實現類如下圖:

  • 左半邊的 Span 實現類:具體操作的 Span 。
  • 右半邊的 Span 實現類:具體操作的 Span ,和左半邊的 Span 實現類相對,用於不需要收集 Span 的場景。

拋開右半邊的 Span 實現類的特殊處理,Span 只有三種實現類:

  • EntrySpan :入口 Span
  • LocalSpan :本地 Span
  • ExitSpan :出口 Span

下麵,我們分小節逐步分享。

2.2.2.1 AbstractTracingSpan

org.skywalking.apm.agent.core.context.trace.AbstractTracingSpan ,實現 AbstractSpan 介面,鏈路追蹤 Span 抽象類

在建立 AbstractTracingSpan 時,會傳入 spanId , parentSpanId , operationName / operationId 引數。參見構造方法:

  • #AbstractTracingSpan(spanId, parentSpanId, operationName)
  • #AbstractTracingSpan(spanId, parentSpanId, operationId)

大部分是 setting / getting 方法,或者類似方法,已經新增註釋,胖友自己閱讀。

#finish(TraceSegment) 方法,完成( 結束 ) Span ,將當前 Span ( 自己 )新增到 TraceSegment 。為什麼會呼叫該方法,在 「3. Context」 詳細解析。

2.2.2.2 StackBasedTracingSpan

org.skywalking.apm.agent.core.context.trace.StackBasedTracingSpan ,實現 AbstractTracingSpan 抽象類,基於的鏈路追蹤 Span 抽象類。這種 Span 能夠被多次呼叫 #start(...) 和 #finish(...) 方法,在類似堆疊的呼叫中。在 「2.2.2.2.1 EntrySpan」 中詳細舉例子。程式碼如下:

  • stackDepth 屬,深度。
  • `#finish(TraceSegment)` 實現方法,完成( 結束 ) Span ,將當前 Span ( 自己 )新增到 TraceSegment 。當且僅當 `stackDepth == 0` 時,新增成功。程式碼如下:
    • 第 55 至 72 行:當操作編號為空時,嘗試使用操作名獲得操作編號並設定。用於減少 Agent 傳送 Collector 資料的網路流量。
    • 第 53 至 73 行:棧深度為零,出棧成功。呼叫 `super#finish(TraceSegment)` 方法,完成( 結束 ) Span ,將當前 Span ( 自己 )新增到 TraceSegment 。
    • 第 74 至 76 行:棧深度非零,出棧失敗。
2.2.2.2.1 EntrySpan

重點

org.skywalking.apm.agent.core.context.trace.EntrySpan ,實現 StackBasedTracingSpan 抽象類,入口 Span ,用於服務提供者( Service Provider ) ,例如 Tomcat 。

EntrySpan 是 TraceSegment 的第一個 Span ,這也是為什麼稱為”入口” Span 的原因。

那麼為什麼 EntrySpan 繼承 StackBasedTracingSpan ?

例如,我們常用的 SprintBoot 場景下,Agent 會在 SkyWalking 外掛在 Tomcat 定義的方法切麵,建立 EntrySpan 物件,也會在 SkyWalking 外掛在 SpringMVC 定義的方法切麵,建立 EntrySpan 物件。那豈不是出現兩個 EntrySpan ,一個 TraceSegment 出現了兩個入口 Span ?

答案是當然不會!Agent 只會在第一個方法切麵,生成 EntrySpan 物件,第二個方法切麵,棧深度 + 1。這也是上面我們看到的 #finish(TraceSegment) 方法,只在棧深度為零時,出棧成功。透過這樣的方式,保持一個 TraceSegment 有且僅有一個 EntrySpan 物件。

當然,多個 TraceSegment 會有多個 EntrySpan 物件 ,例如【服務 A】遠端呼叫【服務 B】。

另外,雖然 EntrySpan 在第一個服務提供者建立,EntrySpan 代表的是最後一個服務提供者,例如,上面的例子,EntrySpan 代表的是 Spring MVC 的方法切麵。所以,startTime 和 endTime 以第一個為準,componentId 、componentName 、layer 、logs 、tags 、operationName 、operationId 等等以最後一個為準。並且,一般情況下,最後一個服務提供者的資訊也會更加詳細

ps:如上內容資訊量較大,胖友可以對照著實現方法,在理解理解。HOHO ,良心筆者當然也是加了註釋的。

如下是一個 EntrySpan 在 SkyWalking 展示的例子:

2.2.2.2.2 ExitSpan

重點

org.skywalking.apm.agent.core.context.trace.ExitSpan ,繼承 StackBasedTracingSpan 抽象類,出口 Span ,用於服務消費者( Service Consumer ) ,例如 HttpClient 、MongoDBClient 。


ExitSpan 實現 org.skywalking.apm.agent.core.context.trace.WithPeerInfo 介面,程式碼如下:

  • peer 屬性,節點地址。
  • peerId 屬性,節點編號。

如下是一個 ExitSpan 在 SkyWalking 展示的例子:


那麼為什麼 ExitSpan 繼承 StackBasedTracingSpan ?

例如,我們可能在使用的 Dubbox 場景下,【Dubbox 服務 A】使用 HTTP 呼叫【Dubbox 服務 B】時,實際過程是,【Dubbox 服務 A】=》【HttpClient】=》【Dubbox 服務 B】。Agent 會在【Dubbox 服務 A】建立 ExitSpan 物件,也會在 【HttpClient】建立 ExitSpan 物件。那豈不是一次出口,出現兩個 ExitSpan ?

答案是當然不會!Agent 只會在【Dubbox 服務 A】,生成 EntrySpan 物件,第二個方法切麵,棧深度 + 1。這也是上面我們看到的 #finish(TraceSegment) 方法,只在棧深度為零時,出棧成功。透過這樣的方式,保持一次出口有且僅有一個 ExitSpan 物件。

當然,一個 TraceSegment 會有多個 ExitSpan 物件 ,例如【服務 A】遠端呼叫【服務 B】,然後【服務 A】再次遠端呼叫【服務 B】,或者然後【服務 A】遠端呼叫【服務 C】。

另外,雖然 ExitSpan 在第一個消費者建立,ExitSpan 代表的也是第一個服務提消費者,例如,上面的例子,ExitSpan 代表的是【Dubbox 服務 A】。

ps:如上內容資訊量較大,胖友可以對照著實現方法,在理解理解。HOHO ,良心筆者當然也是加了註釋的。

2.2.2.3 LocalSpan

org.skywalking.apm.agent.core.context.trace.LocalSpan ,繼承 AbstractTracingSpan 抽象類,本地 Span ,用於一個普通方法的鏈路追蹤,例如本地方法。

如下是一個 EntrySpan 在 SkyWalking 展示的例子:

2.2.2.4 NoopSpan

org.skywalking.apm.agent.core.context.trace.NoopSpan ,實現 AbstractSpan 介面,無操作的 Span 。配置 IgnoredTracerContext 一起使用,在 IgnoredTracerContext 宣告單例 ,以減少不收集 Span 時的物件建立,達到減少記憶體使用和 GC 時間。

2.2.2.3.1 NoopExitSpan

org.skywalking.apm.agent.core.context.trace.NoopExitSpan ,實現 org.skywalking.apm.agent.core.context.trace.WithPeerInfo 介面,繼承 StackBasedTracingSpan 抽象類,出口 Span ,無操作的出口 Span 。和 ExitSpan 相對,不記錄服務消費者的出口 Span 。

2.3 TraceSegmentRef

org.skywalking.apm.agent.core.context.trace.TraceSegmentRef ,TraceSegment 指向,透過 traceSegmentId 和 spanId 屬性,指向父級 TraceSegment 的指定 Span 。

  • type 屬性,指向型別( SegmentRefType ) 。不同的指向型別,使用不同的構造方法。

    • CROSS_PROCESS ,跨行程,例如遠端呼叫,對應構造方法 #TraceSegmentRef(ContextCarrier)。
    • CROSS_THREAD ,跨執行緒,例如非同步執行緒任務,對應構造方法 #TraceSegmentRef(ContextSnapshot) 。
    • 構造方法的程式碼,在 「3. Context」 中,伴隨著呼叫過程,一起解析。
  • traceSegmentId 屬性, TraceSegment 編號。重要

  • spanId 屬性, Span 編號。重要

  • peerId 屬性,節點編號。註意,此處的節點編號就是應用( Application )編號

  • peerHost 屬性,節點地址。

  • entryApplicationInstanceId 屬性,入口應用實體編號。例如,在一個分散式鏈路 A->B->C 中,此欄位為 A 應用的實體編號。

  • parentApplicationInstanceId 屬性,應用實體編號。

  • entryOperationName 屬性,入口操作名。

  • entryOperationId 屬性,入口操作編號。

  • parentOperationName 屬性,操作名。

  • parentOperationId 屬性,操作編號。

2.4 TraceSegment

在看完了 TraceSegment 的各個元素,我們來看看 TraceSegment 內部實現的方法。

TraceSegment 構造方法,程式碼如下:

  • 第 80 行:呼叫 GlobalIdGenerator#generate() 方法,生成 ID 物件,賦值給 traceSegmentId
  • 第 81 行:建立 spans 陣列。
    • `#archive(AbstractTracingSpan)` 方法,被 `AbstractSpan#finish(TraceSegment)` 方法呼叫,新增到 `spans` 陣列。
  • 第 83 至 84 行:建立 DistributedTraceIds 物件,並新增 NewDistributedTraceId 到它。
    • 註意,當 TraceSegment 是一次分散式鏈路追蹤的首條記錄,建立的 NewDistributedTraceId 物件,即為分散式鏈路追蹤的全域性編號
    • `#relatedGlobalTraces(DistributedTraceId)` 方法,新增 DistributedTraceId 物件。被 `TracingContext#continued(ContextSnapshot)` 或者 `TracingContext#extract(ContextCarrier)` 方法呼叫,在 「3. Context」 詳細解析。

#ref(TraceSegmentRef) 方法,新增 TraceSegmentRef 物件,到 refs 屬性,即指向父 Segment 。

3. Context

在 「2. Trace」 中,我們看了 Trace 的資料結構,本小節,我們一起來看看 Context 是怎麼收集 Trace 資料的。

3.1 ContextManager

org.skywalking.apm.agent.core.context.ContextManager ,實現了 BootService 、TracingContextListener 、IgnoreTracerContextListener 介面,鏈路追蹤背景關係管理器。


CONTEXT 靜態屬性,執行緒變數,儲存 AbstractTracerContext 物件。為什麼是執行緒變數呢?

一個 TraceSegment 物件,關聯到一個執行緒,負責收集該執行緒的鏈路追蹤資料,因此使用執行緒變數。

一個 AbstractTracerContext 會關聯一個 TraceSegment 物件,ContextManager 負責獲取、建立、銷毀 AbstractTracerContext 物件。

#getOrCreate(operationName, forceSampling) 靜態方法,獲取 AbstractTracerContext 物件。若不存在,進行建立

  • 需要收集 Trace 資料的情況下,建立 TracingContext 物件。
  • 需要收集 Trace 資料的情況下,建立 IgnoredTracerContext 物件。

在下麵的 #createEntrySpan(...) 、#createLocalSpan(...) 、#createExitSpan(...) 等等方法中,都會呼叫 AbstractTracerContext 提供的方法。這些方法的程式碼,我們放在 「3.2 AbstractTracerContext」 一起解析,保證流程的整體性。

另外,ContextManager 封裝了所有 AbstractTracerContext 提供的方法,從而實現,外部呼叫者,例如 SkyWalking 的外掛,只呼叫 ContextManager 的方法,而不呼叫 AbstractTracerContext 的方法。


#boot() 實現方法,啟動時,將自己註冊到 [TracingContext.ListenerManager]() 和 [IgnoredTracerContext.ListenerManager]() 中,這樣一次鏈路追蹤背景關係( Context )完成時,從而被回呼如下方法,清理背景關係:

  • `#afterFinished(TraceSegment)`
  • `#afterFinished(IgnoredTracerContext)`

3.2 AbstractTracerContext

org.skywalking.apm.agent.core.context.AbstractTracerContext ,鏈路追蹤背景關係介面。定義瞭如下方法:

  • #getReadableGlobalTraceId() 方法,獲得關聯的全域性鏈路追蹤編號。
  • #createEntrySpan(operationName) 方法,建立 EntrySpan 物件。
  • #createLocalSpan(operationName) 方法,建立 LocalSpan 物件。
  • #createExitSpan(operationName, remotePeer) 方法,建立 ExitSpan 物件。
  • #activeSpan() 方法,獲得當前活躍的 Span 物件。
  • #stopSpan(AbstractSpan) 方法,停止( 完成 )指定 AbstractSpan 物件。
  • ——— 跨行程( cross-process ) ———
  • #inject(ContextCarrier) 方法,將 Context 註入到 ContextCarrier ,用於跨行程,傳播背景關係。
  • #extract(ContextCarrier) 方法,將 ContextCarrier 解壓到 Context ,用於跨行程,接收背景關係。
  • ——— 跨執行緒( cross-thread ) ———
  • #capture() 方法,將 Context 快照到 ContextSnapshot ,用於跨執行緒,傳播背景關係。
  • #continued(ContextSnapshot) 方法,將 ContextSnapshot 解壓到 Context ,用於跨執行緒,接收背景關係。

3.2.1 TracingContext

org.skywalking.apm.agent.core.context.TracingContext ,實現 AbstractTracerContext 介面,鏈路追蹤背景關係實現類

  • segment 屬性,背景關係對應的 TraceSegment 物件。
  • activeSpanStack 屬性,AbstractSpan 連結串列陣列,收集當前活躍的 Span 物件。正如方法的呼叫與執行一樣,在一個呼叫棧中,先執行的方法後結束。
  • spanIdGenerator 屬性,Span 編號自增序列。建立的 Span 的編號,透過該變數自增生成。

TracingContext 構造方法 ,程式碼如下:

  • 第 80 行:建立 TraceSegment 物件。
  • 第 81 行:設定 spanIdGenerator = 0 。

#getReadableGlobalTraceId() 實現方法,獲得 TraceSegment 的首個 DistributedTraceId 作為傳回。

3.2.1.1 建立 EntrySpan

呼叫 ContextManager#createEntrySpan(operationName, carrier) 方法,建立 EntrySpan 物件。程式碼如下:

  • 第 121 至 131 行:呼叫 #getOrCreate(operationName, forceSampling) 方法,獲取 AbstractTracerContext 物件。若不存在,進行建立。
    • 第 122 至 125 行:有傳播 Context 的情況下,強制收集 Trace 資料。
    • 第 127 行:呼叫 `TracingContext#extract(ContextCarrier)` 方法,將 ContextCarrier 解壓到 Context ,跨行程,接收背景關係。在 「3.2.3 ContextCarrier」 詳細解析。
  • 第 133 行:呼叫 TracingContext#createEntrySpan(operationName) 方法,建立 EntrySpan 物件。

呼叫 TracingContext#createEntrySpan(operationName) 方法,建立 EntrySpan 物件。程式碼如下:

  • 第 223 至 227 行:呼叫 `#isLimitMechanismWorking()` 方法,判斷 Span 數量超過上限,建立 NoopSpan 物件,並呼叫 #push(AbstractSpan) 方法,新增到 activeSpanStack 中。
  • 第 229 至 231 行:呼叫 `#peek()` 方法,獲得當前活躍的 AbstractSpan 物件。
  • 第 232 至 249 行:若 Span 物件不存在,建立 EntrySpan 物件。
    • 第 235 至 244 行:建立 EntrySpan 物件。
    • 第 247 行:呼叫 `EntrySpan#start()` 方法,開始 EntrySpan 。
    • 第 249 行:呼叫 `#push(AbstractSpan)` 方法,新增到 `activeSpanStack` 中。
  • 第 251 至 264 行:若 EntrySpan 物件存在,重新開始 EntrySpan 。參見 「2.2.2.2.1 EntrySpan」 。
  • 第 265 至 267 行:"The Entry Span can't be the child of Non-Entry Span" 。

3.2.1.2 建立 LocalSpan

呼叫 ContextManager#createLocalSpan(operationName) 方法,建立 LocalSpan 物件。

  • 第 138 行:呼叫 #getOrCreate(operationName, forceSampling) 方法,獲取 AbstractTracerContext 物件。若不存在,進行建立。
  • 第 140 行:呼叫 TracingContext#createLocalSpan(operationName) 方法,建立 LocalSpan 物件。

呼叫 TracingContext#createLocalSpan(operationName) 方法,建立 LocalSpan 物件。程式碼如下:

  • 第 280 至 283 行:呼叫 `#isLimitMechanismWorking()` 方法,判斷 Span 數量超過上限,建立 NoopSpan 物件,並呼叫 #push(AbstractSpan) 方法,新增到 activeSpanStack 中。
  • 第 284 至 286 行:呼叫 `#peek()` 方法,獲得當前活躍的 AbstractSpan 物件。
  • 第 288 至 300 行:建立 LocalSpan 物件。
  • 第 302 行:呼叫 LocalSpan#start() 方法,開始 LocalSpan 。
  • 第 304 行:呼叫 #push(AbstractSpan) 方法,新增到 activeSpanStack 中。

3.2.1.3 建立 ExitSpan

呼叫 ContextManager#createExitSpan(operationName, carrier, remotePeer) 方法,建立 ExitSpan 物件。

  • 第 148 行:呼叫 #getOrCreate(operationName, forceSampling) 方法,獲取 AbstractTracerContext 物件。若不存在,進行建立。
  • 第 150 行:呼叫 TracingContext#createExitSpan(operationName, remotePeer) 方法,建立 ExitSpan 物件。
  • 第 160 行:TracingContext#inject(ContextCarrier) 方法,將 Context 註入到 ContextCarrier ,跨行程,傳播背景關係。在 「3.2.3 ContextCarrier」 詳細解析。

呼叫 TracingContext#createEntrySpan(operationName) 方法,建立 ExitSpan 物件。程式碼如下:

  • 第 319 行:呼叫 `#peek()` 方法,獲得當前活躍的 AbstractSpan 物件。
  • 第 320 至 322 行:若 ExitSpan 物件存在,直接使用,不重新建立。參見 「2.2.2.2.2 ExitSpan」 。
  • 第 324 至 377 行:建立 ExitSpan 物件,並新增到 activeSpanStack 中。
    • 第 327 行:根據 `remotePeer` 引數,查詢 `peerId` 。註意,此處會建立一個 Application 物件,透過 ServiceMapping 表,和遠端的 Application 進行匹配對映。後續有文章會分享這塊。
    • 第 322 至 324 行 || 第 335 至 358 行:判斷 Span 數量超過上限,建立 NoopExitSpan 物件,並呼叫 `#push(AbstractSpan)` 方法,新增到 `activeSpanStack` 中。
  • 第 380 行:開始 ExitSpan 。

3.2.1.4 結束 Span

呼叫 ContextManager#stopSpan() 方法,結束 Span 。程式碼如下:

  • 第 199 行:呼叫 TracingContext#stopSpan(AbstractSpan) 方法,結束 Span 。當所有活躍的 Span 都被結束後,當前執行緒的 TraceSegment 完成

呼叫 TracingContext#stopSpan(AbstractSpan) 方法,結束 Span 。程式碼如下:

  • 第 405 行:呼叫 `#peek()` 方法,獲得當前活躍的 AbstractSpan 物件。
  • 第 408 至 414 行:當 Span 為 AbstractTracingSpan 的子類,即記錄鏈路追蹤的 Span ,呼叫 AbstractTracingSpan#finish(TraceSegment) 方法,完成 Span 。
    • 當完成成功時,呼叫 `#pop()` 方法,移除出 `activeSpanStack` 。
    • 當完成失敗時,原因參見 「2.2.2.2 StackBasedTracingSpan」 。
  • 第 416 至 419 行:當 Span 為 NoopSpan 的子類,即不記錄鏈路追蹤的 Span ,呼叫 #pop() 方法,移除出 activeSpanStack 。
  • 第 425 至 427 行:當所有活躍的 Span 都被結束後,呼叫 #finish() 方法,當前執行緒的 TraceSegment 完成。

呼叫 TracingContext#stopSpan(AbstractSpan) 方法,完成 Context 。程式碼如下:

  • 第 436 行:呼叫 TraceSegment#finish(isSizeLimited) 方法,完成 TraceSegment 。
  • 第 444 至 448 行:若滿足條件,呼叫 TraceSegment#setIgnore(true) 方法,標記該 TraceSegment 忽略,不傳送給 Collector 。
    • `!samplingService.trySampling()` :不取樣。
    • `!segment.hasRef()` :無父 TraceSegment 指向。如果此處忽略取樣,則會導致整條分散式鏈路追蹤不完整
    • `segment.isSingleSpanSegment()` :TraceSegment 只有一個 Span 。
    • TODO 【4010】
  • 第 450 行:呼叫 `TracingContext.ListenerManager#notifyFinish(TraceSegment)` 方法,通知監聽器,一次 TraceSegment 完成。透過這樣的方式,TraceSegment 會被 TraceSegmentServiceClient 非同步傳送給 Collector 。下一篇文章,我們詳細分享傳送的過程。

3.2.2 IgnoredTracerContext

org.skywalking.apm.agent.core.context.IgnoredTracerContext ,實現 AbstractTracerContext 介面,忽略( 不記錄 )鏈路追蹤的背景關係。程式碼如下:

  • NOOP_SPAN 靜態屬性,NoopSpan 單例。
    • 所有的建立 Span 方法,傳回的都是該物件。
  • stackDepth 屬性,棧深度。
    • 不同於 TracingContext 使用鏈式陣列來處理 Span 的出入棧,IgnoredTracerContext 使用 `stackDepth` 來計數,從而實現出入棧的效果。
  • 透過這兩個屬性和相應方法的實現,以減少 NoopSpan 時的物件建立,達到減少記憶體使用和 GC 時間。

程式碼比較簡單,胖友自己閱讀該類的實現。

3.2.3 ContextCarrier

org.skywalking.apm.agent.core.context.ContextCarrier ,實現 java.io.Serializable介面,跨行程 Context 傳輸載體

3.2.3.1 解壓

我們來開啟 #TraceSegmentRef(ContextCarrier) 構造方法,該方法用於將 ContextCarrier 轉換成 TraceSegmentRef ,對比下兩者的屬性,基本一致,差異如下:

  • peerHost 屬性,節點地址。
    • 當字串以 `#` 號開頭,代表節點編號,格式為 `${peerId}` ,例如 `”123″` 。
    • 當字串以 `#` 號開頭,代表地址,格式為 `${peerHost}` ,例如 `”192.168.16.1:8080″` 。
  • entryOperationName 屬性,入口操作名。
    • 當字串以 `#` 號開頭,代表入口操作編號,格式為 `#${entryOperationId}` ,例如 `”666″` 。
    • 當字串以 `#` 號開頭,代表入口操作名,格式為 `#${entryOperationName}` ,例如 `”#user/login”` 。
  • parentOperationName 屬性,父操作名。類似 entryOperationName 屬。
  • primaryDistributedTraceId 屬性,分散式鏈路追蹤全域性編號。它不在此處處理,而在 `TracingContext#extract(ContextCarrier)` 方法中

在 ContextManager#createEntrySpan(operationName, carrier) 方法中,當存在 ContextCarrier 傳遞時,建立 Context 後,會將 ContextCarrier 解壓到 Context 中,以達到跨行程傳播。TracingContext#extract(ContextCarrier) 方法,程式碼如下:

  • 第 148 行:將 ContextCarrier 轉換成 TraceSegmentRef 物件,呼叫 TraceSegment#ref(TraceSegmentRef) 方法,進行指向父 TraceSegment。
  • 第 149 行:呼叫 TraceSegment#relatedGlobalTraces(DistributedTraceId) 方法,將傳播的分散式鏈路追蹤全域性編號,新增到 TraceSegment 中,進行指向全域性編號。

另外,ContextManager 單獨提供 #extract(ContextCarrier) 方法,將多個 ContextCarrier 註入到一個Context 中,從而解決”多個爸爸“的場景,例如 RocketMQ 外掛的 AbstractMessageConsumeInterceptor#beforeMethod(...) 方法。

3.2.3.2 註入

在 ContextManager#createExitSpan(operationName, carrier, remotePeer) 方法中,當需要Context 跨行程傳遞時,將 Context 註入到 ContextCarrier 中,為 「3.2.3.3 傳輸」 做準備。TracingContext#inject(ContextCarrier) 方法,程式碼比較易懂,胖友自己閱讀理解。

3.2.3.3 傳輸

友情提示:胖友,請先閱讀 《Skywalking Cross Process Propagation Headers Protocol》 。

org.skywalking.apm.agent.core.context.CarrierItem ,傳輸載體。程式碼如:

  • headKey 屬性,Header 鍵。
  • headValue 屬性,Header 值。
  • next 屬性,下一個項。

CarrierItem 有兩個子類:

  • CarrierItemHead :Carrier 項的頭( Head ),即首個元素。
  • SW3CarrierItem :essay-header = sw3 ,用於傳輸 ContextCarrier 。

如下是 Dubbo 外掛,使用 CarrierItem 的程式碼例子:

  • `ContextCarrier#serialize()`
  • `ContextCarrier#deserialize(text)`

3.2.4 ContextSnapshot

org.skywalking.apm.agent.core.context.ContextSnapshot ,跨執行緒 Context 傳遞快照。和 ContextCarrier 基本一致,由於不需要跨行程傳輸,可以少傳遞一些屬性:

  • parentApplicationInstanceId
  • peerHost

ContextSnapshot 和 ContextCarrier 比較類似,筆者就列舉一些方法:

  • `#TraceSegmentRef(ContextSnapshot)`
  • `TracingContext#capture()`
  • `TracingContext#continued(ContextSnapshot)`

3.3 SamplingService

org.skywalking.apm.agent.core.sampling.SamplingService ,實現 Service 介面,Agent 抽樣服務。該服務的作用是,如何對 TraceSegment 抽樣收集。考慮到如果每條 TraceSegment 都進行追蹤,會帶來一定的 CPU ( 用於序列化與反序列化 ) 和網路的開銷。透過配置 Config.Agent.SAMPLE_N_PER_3_SECS 屬性,設定每三秒,收集 TraceSegment 的條數。預設情況下,不開啟抽樣服務,即全部收集。

程式碼如下:

  • on 屬性,是否開啟抽樣服務。
  • samplingFactorHolder 屬性,抽樣計數器。透過定時任務,每三秒重置一次。
  • scheduledFuture 屬性,定時任務。
  • #boot() 實現方法,若開啟抽樣服務( Config.Agent.SAMPLE_N_PER_3_SECS > 0 ) 時,建立定時任務,每三秒,呼叫一次 #resetSamplingFactor() 方法,重置計數器。
  • #trySampling() 方法,若開啟抽樣服務,判斷是否超過每三秒的抽樣上限。若不是,傳回 true ,並增加計數器。否則,傳回 false 。
  • #forceSampled() 方法,強制增加計數器加一。一般情況下,該方法用於鏈路追蹤背景關係傳播時,被呼叫服務必須記錄鏈路,參見呼叫處的程式碼。
  • #resetSamplingFactor() 方法,重置計數器。

已同步到看一看
贊(0)

分享創造快樂