天天看點

SkyWalking 源碼分析 —— Agent 收集 Trace 資料1. 概述2. Trace3. Context

1. 概述

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

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

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

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

不包括插件對 Context 收集的方法的調用,後續單獨文章專門分享,胖友也可以閱讀完本文後,自己去看 

apm-sdk-plugin

 的實作代碼。

本文涉及到的代碼如下圖:

SkyWalking 源碼分析 —— Agent 收集 Trace 資料1. 概述2. Trace3. Context
  • 紅框部分: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)

     方法,設定操作名。
    • 操作名,定義如下:
      SkyWalking 源碼分析 —— Agent 收集 Trace 資料1. 概述2. Trace3. Context
    • #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<T>

 ,标簽抽象類。注意,這個類的用途是将标簽屬性設定到 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 實作類如下圖:

SkyWalking 源碼分析 —— Agent 收集 Trace 資料1. 概述2. Trace3. Context
  • 左半邊的 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

     時,添加成功。代碼如下:
    • 第 53 至 73 行:棧深度為零,出棧成功。調用 

      super#finish(TraceSegment)

       方法,完成( 結束 ) Span ,将目前 Span ( 自己 )添加到 TraceSegment 。
      • 第 55 至 72 行:當操作編号為空時,嘗試使用操作名獲得操作編号并設定。用于減少 Agent 發送 Collector 資料的網絡流量。
    • 第 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 展示的例子:

SkyWalking 源碼分析 —— Agent 收集 Trace 資料1. 概述2. Trace3. Context

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 展示的例子:

SkyWalking 源碼分析 —— Agent 收集 Trace 資料1. 概述2. Trace3. Context

那麼為什麼 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 展示的例子:

SkyWalking 源碼分析 —— Agent 收集 Trace 資料1. 概述2. Trace3. Context

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 :

    header = sw3

     ,用于傳輸 ContextCarrier 。

如下是 Dubbo 插件,使用 CarrierItem 的代碼例子:

SkyWalking 源碼分析 —— Agent 收集 Trace 資料1. 概述2. Trace3. Context
  • 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()

     方法,重置計數器。

繼續閱讀