本文主要基於 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. 概述
分散式鏈路追蹤系統,鏈路的追蹤大體流程如下:
- Agent 收集 Trace 資料。
- Agent 傳送 Trace 資料給 Collector 。
- Collector 接收 Trace 資料。
- 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()
方法,重置計數器。
朋友會在“發現-看一看”看到你“在看”的內容