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 屬性,如下:
-
屬性,TraceSegment 的編号,全局唯一。在 「2.1 ID」 詳細解析。traceSegmentId
-
屬性,TraceSegmentRef 數組,指向的父 TraceSegment 數組。refs
- 為什麼會有多個爸爸?下面統一講。
- TraceSegmentRef ,在 「2.3 TraceSegmentRef」 詳細解析。
-
屬性,關聯的 DistributedTraceId 數組。relatedGlobalTraces
- 為什麼會有多個爸爸?下面統一講。
- DistributedTraceId ,在 「2.1.2 DistributedTraceId」 詳細解析。
-
屬性,包含的 Span 數組。在 「2.2 AbstractSpan」 詳細解析。這是 TraceSegment 的主體,總的來說,TraceSegment 是 Span 數組的封裝。spans
-
屬性,是否忽略該條 TraceSegment 。在一些情況下,我們會忽略 TraceSegment ,即不收集鍊路追蹤,在下面 「3. Context」 部分内容,我們将會看到這些情況。ignore
-
屬性,Span 是否超過上限(isSizeLimited
)。超過上限,不在記錄 Span 。Config.Agent.SPAN_LIMIT_PER_SEGMENT
為什麼會有多個爸爸?
- 我們先來看看一個爸爸的情況,常見于 RPC 調用。例如,【服務 A】調用【服務 B】時,【服務 B】建立一個 TraceSegment 對象:
- 将自己的
指向【服務 A】的 TraceSegment 。refs
- 将自己的
設定為 【服務 A】的 DistributedTraceId 對象。relatedGlobalTraces
- 将自己的
- 我們再來看看多個爸爸的情況,常見于 MQ / Batch 調用。例如,MQ 批量消費消息時,消息來自【多個服務】。每次批量消費時,【消費者】建立一個 TraceSegment 對象:
- 将自己的
指向【多個服務】的多個 TraceSegment 。refs
- 将自己的
設定為【多個服務】的多個 DistributedTraceId 。relatedGlobalTraces
- 将自己的
友情提示:多個爸爸的故事,可能比較難懂,等胖友讀完全文,在回過頭想想。或者拿起來代碼調試調試。
下面,我們來具體看看 TraceSegment 的每個元素,最後,我們會回過頭,在 「2.4 TraceSegment」 詳細解析它。
2.1 ID
org.skywalking.apm.agent.core.context.ids.ID
,編号。從類的定義上,這是一個通用的編号,由三段整數組成。
目前使用 GlobalIdGenerator 生成,作為全局唯一編号。屬性如下:
-
屬性,應用執行個體編号。part1
-
屬性,線程編号。part2
-
屬性,時間戳串,生成方式為part3
。例如:15127007074950012 。具體生成方法的代碼,在 GlobalIdGenerator 中詳細解析。${時間戳} * 10000 + 線程自增序列([0, 9999])
-
屬性,編碼後的字元串。格式為encoding
。例如,"${part1}.${part2}.${part3}"
。"12.35.15127007074950000"
- 使用
方法,編碼編号。#encode()
- 使用
-
屬性,編号是否合法。isValid
- 使用
構造方法,解析字元串,生成 ID 。ID(encodingString)
- 使用
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()
- 第 70 行:
- 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()
方法,建立 ID 對象。GlobalIdGenerator#generate()
- org.skywalking.apm.agent.core.context.ids.PropagatedTraceId ,傳播的分布式鍊路追蹤編号。例如,A 服務調用 B 服務時,A 服務會将 DistributedTraceId 對象帶給 B 服務,B 服務會調用
構造方法 ,建立 PropagatedTraceId 對象。該構造方法内部會解析 id ,生成 ID 對象。PropagatedTraceId(String id)
2.1.3 DistributedTraceIds
org.skywalking.apm.agent.core.context.ids.DistributedTraceIds
,DistributedTraceId 數組的封裝。
-
屬性,關聯的 DistributedTraceId 鍊式數組。relatedGlobalTraces
#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 通用屬性的接口方法:
-
方法,獲得 Span 編号。一個整數,在 TraceSegment 内唯一,從 0 開始自增,在建立 Span 對象時生成。#getSpanId()
-
方法,設定操作名。#setOperationName(operationName)
- 操作名,定義如下:
-
方法,設定操作編号。考慮到操作名是字元串,Agent 發送給 Collector 占用流量較大。是以,Agent 會将操作注冊到 Collector ,生成操作編号。在 《SkyWalking 源碼分析 —— Agent DictionaryManager 字典管理》 有詳細解析。#setOperationId(operationId)
-
方法,設定#setComponent(Component)
,例如:MongoDB / SpringMVC / Tomcat 等等。目前,官方在org.skywalking.apm.network.trace.component.Component
定義了目前已經支援的 Component 。org.skywalking.apm.network.trace.component.ComponentsDefine
-
方法,直接設定 Component 名字。大多數情況下,我們不使用該方法。#setComponent(componentName)
Only use this method in explicit instrumentation, like opentracing-skywalking-bridge.
It it higher recommend don’t use this for performance consideration.
-
-
方法,設定#setLayer(SpanLayer)
。目前有,DB 、RPC_FRAMEWORK 、HTTP 、MQ ,未來會增加 CACHE 。org.skywalking.apm.agent.core.context.trace.SpanLayer
-
方法,設定鍵值對的标簽。可以調用多次,構成 Span 的标簽集合。在 「2.2.1 Tag」 詳細解析。#tag(key, value)
- 日志相關
-
方法,記錄一條通用日志,包含#log(timestampMicroseconds, fields)
鍵值對集合。fields
-
方法,記錄一條異常日志,包含異常資訊。#log(Throwable)
-
-
方法,标記發生異常。大多數情況下,配置#errorOccurred()
方法一起使用。#log(Throwable)
-
方法,開始 Span 。一般情況的實作,設定開始時間。#start()
-
方法,是否是入口 Span ,在 「2.2.2.1 EntrySpan」 詳細解析。#isEntry()
-
方法,是否是出口 Span ,在 「2.2.2.2 ExitSpan」 詳細解析。#isExit()
2.2.1 Tag
2.2.1.1 AbstractTag
org.skywalking.apm.agent.core.context.tag.AbstractTag<T>
,标簽抽象類。注意,這個類的用途是将标簽屬性設定到 Span 上,或者說,它是設定 Span 的标簽的工具類。代碼如下:
-
屬性,标簽的鍵。key
-
抽象方法,設定 Span 的标簽鍵#set(AbstractSpan span, T tagValue)
的值為key
。tagValue
2.2.1.2 StringTag
org.skywalking.apm.agent.core.context.tag.StringTag
,值類型為 String 的标簽實作類。
-
實作方法,設定 Span 的标簽鍵#set(AbstractSpan span, String tagValue)
的值為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
-
實作方法,完成( 結束 ) Span ,将目前 Span ( 自己 )添加到 TraceSegment 。當且僅當#finish(TraceSegment)
時,添加成功。代碼如下:stackDepth == 0
- 第 53 至 73 行:棧深度為零,出棧成功。調用
方法,完成( 結束 ) Span ,将目前 Span ( 自己 )添加到 TraceSegment 。super#finish(TraceSegment)
- 第 55 至 72 行:當操作編号為空時,嘗試使用操作名獲得操作編号并設定。用于減少 Agent 發送 Collector 資料的網絡流量。
- 第 74 至 76 行:棧深度非零,出棧失敗。
- 第 53 至 73 行:棧深度為零,出棧成功。調用
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 。
-
屬性,指向類型( SegmentRefType ) 。不同的指向類型,使用不同的構造方法。type
-
,跨程序,例如遠端調用,對應構造方法 #TraceSegmentRef(ContextCarrier) 。CROSS_PROCESS
-
,跨線程,例如異步線程任務,對應構造方法 #TraceSegmentRef(ContextSnapshot) 。CROSS_THREAD
- 構造方法的代碼,在 「3. Context」 中,伴随着調用過程,一起解析。
-
-
屬性,父 TraceSegment 編号。重要traceSegmentId
-
屬性,父 Span 編号。重要spanId
-
屬性,節點編号。注意,此處的節點編号就是應用( Application )編号。peerId
-
屬性,節點位址。peerHost
-
屬性,入口應用執行個體編号。例如,在一個分布式鍊路entryApplicationInstanceId
中,此字段為 A 應用的執行個體編号。A->B->C
-
屬性,父應用執行個體編号。parentApplicationInstanceId
-
屬性,入口操作名。entryOperationName
-
屬性,入口操作編号。entryOperationId
-
屬性,父操作名。parentOperationName
-
屬性,父操作編号。parentOperationId
2.4 TraceSegment
在看完了 TraceSegment 的各個元素,我們來看看 TraceSegment 内部實作的方法。
TraceSegment 構造方法,代碼如下:
- 第 80 行:調用
方法,生成 ID 對象,指派給GlobalIdGenerator#generate()
。traceSegmentId
- 第 81 行:建立
數組。spans
-
方法,被#archive(AbstractTracingSpan)
方法調用,添加到AbstractSpan#finish(TraceSegment)
數組。spans
-
- 第 83 至 84 行:建立 DistributedTraceIds 對象,并添加 NewDistributedTraceId 到它。
- 注意,當 TraceSegment 是一次分布式鍊路追蹤的首條記錄,建立的 NewDistributedTraceId 對象,即為分布式鍊路追蹤的全局編号。
-
方法,添加 DistributedTraceId 對象。被#relatedGlobalTraces(DistributedTraceId)
或者TracingContext#continued(ContextSnapshot)
方法調用,在 「3. Context」 詳細解析。TracingContext#extract(ContextCarrier)
#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()
-
方法,建立 EntrySpan 對象。#createEntrySpan(operationName)
-
方法,建立 LocalSpan 對象。#createLocalSpan(operationName)
-
方法,建立 ExitSpan 對象。#createExitSpan(operationName, remotePeer)
-
方法,獲得目前活躍的 Span 對象。#activeSpan()
-
方法,停止( 完成 )指定 AbstractSpan 對象。#stopSpan(AbstractSpan)
- ——— 跨程序( cross-process ) ———
-
方法,将 Context 注入到 ContextCarrier ,用于跨程序,傳播上下文。#inject(ContextCarrier)
-
方法,将 ContextCarrier 解壓到 Context ,用于跨程序,接收上下文。#extract(ContextCarrier)
- ——— 跨線程( cross-thread ) ———
-
方法,将 Context 快照到 ContextSnapshot ,用于跨線程,傳播上下文。#capture()
-
方法,将 ContextSnapshot 解壓到 Context ,用于跨線程,接收上下文。#continued(ContextSnapshot)
3.2.1 TracingContext
org.skywalking.apm.agent.core.context.TracingContext
,實作 AbstractTracerContext 接口,鍊路追蹤上下文實作類。
-
屬性,上下文對應的 TraceSegment 對象。segment
-
屬性,AbstractSpan 連結清單數組,收集目前活躍的 Span 對象。正如方法的調用與執行一樣,在一個調用棧中,先執行的方法後結束。activeSpanStack
-
屬性,Span 編号自增序列。建立的 Span 的編号,通過該變量自增生成。spanIdGenerator
TracingContext 構造方法 ,代碼如下:
- 第 80 行:建立 TraceSegment 對象。
- 第 81 行:設定
。spanIdGenerator = 0
#getReadableGlobalTraceId()
實作方法,獲得 TraceSegment 的首個 DistributedTraceId 作為傳回。
3.2.1.1 建立 EntrySpan
調用
ContextManager#createEntrySpan(operationName, carrier)
方法,建立 EntrySpan 對象。代碼如下:
- 第 121 至 131 行:調用
方法,擷取 AbstractTracerContext 對象。若不存在,進行建立。#getOrCreate(operationName, forceSampling)
- 第 122 至 125 行:有傳播 Context 的情況下,強制收集 Trace 資料。
- 第 127 行:調用
方法,将 ContextCarrier 解壓到 Context ,跨程序,接收上下文。在 「3.2.3 ContextCarrier」 詳細解析。TracingContext#extract(ContextCarrier)
- 第 133 行:調用
方法,建立 EntrySpan 對象。TracingContext#createEntrySpan(operationName)
調用
TracingContext#createEntrySpan(operationName)
方法,建立 EntrySpan 對象。代碼如下:
- 第 223 至 227 行:調用
方法,判斷 Span 數量超過上限,建立 NoopSpan 對象,并調用#isLimitMechanismWorking()
方法,添加到#push(AbstractSpan)
中。activeSpanStack
- 第 229 至 231 行:調用
方法,獲得目前活躍的 AbstractSpan 對象。#peek()
- 第 232 至 249 行:若父 Span 對象不存在,建立 EntrySpan 對象。
- 第 235 至 244 行:建立 EntrySpan 對象。
- 第 247 行:調用
方法,開始 EntrySpan 。EntrySpan#start()
- 第 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 行:調用
方法,擷取 AbstractTracerContext 對象。若不存在,進行建立。#getOrCreate(operationName, forceSampling)
- 第 140 行:調用
方法,建立 LocalSpan 對象。TracingContext#createLocalSpan(operationName)
調用
TracingContext#createLocalSpan(operationName)
方法,建立 LocalSpan 對象。代碼如下:
- 第 280 至 283 行:調用
方法,判斷 Span 數量超過上限,建立 NoopSpan 對象,并調用#isLimitMechanismWorking()
方法,添加到#push(AbstractSpan)
中。activeSpanStack
- 第 284 至 286 行:調用
方法,獲得目前活躍的 AbstractSpan 對象。#peek()
- 第 288 至 300 行:建立 LocalSpan 對象。
- 第 302 行:調用
方法,開始 LocalSpan 。LocalSpan#start()
- 第 304 行:調用
方法,添加到#push(AbstractSpan)
中。activeSpanStack
3.2.1.3 建立 ExitSpan
調用
ContextManager#createExitSpan(operationName, carrier, remotePeer)
方法,建立 ExitSpan 對象。
- 第 148 行:調用
方法,擷取 AbstractTracerContext 對象。若不存在,進行建立。#getOrCreate(operationName, forceSampling)
- 第 150 行:調用
方法,建立 ExitSpan 對象。TracingContext#createExitSpan(operationName, remotePeer)
- 第 160 行:
方法,将 Context 注入到 ContextCarrier ,跨程序,傳播上下文。在 「3.2.3 ContextCarrier」 詳細解析。TracingContext#inject(ContextCarrier)
調用
TracingContext#createEntrySpan(operationName)
方法,建立 ExitSpan 對象。代碼如下:
- 第 319 行:調用
方法,獲得目前活躍的 AbstractSpan 對象。#peek()
- 第 320 至 322 行:若 ExitSpan 對象存在,直接使用,不重新建立。參見 「2.2.2.2.2 ExitSpan」 。
- 第 324 至 377 行:建立 ExitSpan 對象,并添加到
中。activeSpanStack
- 第 327 行:根據
參數,查找remotePeer
。注意,此處會建立一個 Application 對象,通過 ServiceMapping 表,和遠端的 Application 進行比對映射。後續有文章會分享這塊。peerId
- 第 322 至 324 行 || 第 335 至 358 行:判斷 Span 數量超過上限,建立 NoopExitSpan 對象,并調用
方法,添加到#push(AbstractSpan)
中。activeSpanStack
- 第 327 行:根據
- 第 380 行:開始 ExitSpan 。
3.2.1.4 結束 Span
調用
ContextManager#stopSpan()
方法,結束 Span 。代碼如下:
- 第 199 行:調用
方法,結束 Span 。當所有活躍的 Span 都被結束後,目前線程的 TraceSegment 完成。TracingContext#stopSpan(AbstractSpan)
調用
TracingContext#stopSpan(AbstractSpan)
方法,結束 Span 。代碼如下:
- 第 405 行:調用
方法,獲得目前活躍的 AbstractSpan 對象。#peek()
- 第 408 至 414 行:當 Span 為 AbstractTracingSpan 的子類,即記錄鍊路追蹤的 Span ,調用
方法,完成 Span 。AbstractTracingSpan#finish(TraceSegment)
- 當完成成功時,調用
方法,移除出#pop()
。activeSpanStack
- 當完成失敗時,原因參見 「2.2.2.2 StackBasedTracingSpan」 。
- 當完成成功時,調用
- 第 416 至 419 行:當 Span 為 NoopSpan 的子類,即不記錄鍊路追蹤的 Span ,調用
方法,移除出#pop()
。activeSpanStack
- 第 425 至 427 行:當所有活躍的 Span 都被結束後,調用
方法,目前線程的 TraceSegment 完成。#finish()
調用
TracingContext#stopSpan(AbstractSpan)
方法,完成 Context 。代碼如下:
- 第 436 行:調用
方法,完成 TraceSegment 。TraceSegment#finish(isSizeLimited)
- 第 444 至 448 行:若滿足條件,調用
方法,标記該 TraceSegment 忽略,不發送給 Collector 。TraceSegment#setIgnore(true)
-
:不采樣。!samplingService.trySampling()
-
:無父 TraceSegment 指向。如果此處忽略采樣,則會導緻整條分布式鍊路追蹤不完整。!segment.hasRef()
-
:TraceSegment 隻有一個 Span 。segment.isSingleSpanSegment()
- TODO 【4010】
-
- 第 450 行:調用
方法,通知監聽器,一次 TraceSegment 完成。通過這樣的方式,TraceSegment 會被 TraceSegmentServiceClient 異步發送給 Collector 。下一篇文章,我們詳細分享發送的過程。TracingContext.ListenerManager#notifyFinish(TraceSegment)
3.2.2 IgnoredTracerContext
org.skywalking.apm.agent.core.context.IgnoredTracerContext
,實作 AbstractTracerContext 接口,忽略( 不記錄 )鍊路追蹤的上下文。代碼如下:
-
靜态屬性,NoopSpan 單例。NOOP_SPAN
- 所有的建立 Span 方法,傳回的都是該對象。
-
屬性,棧深度。stackDepth
- 不同于 TracingContext 使用鍊式數組來處理 Span 的出入棧,IgnoredTracerContext 使用
來計數,進而實作出入棧的效果。stackDepth
- 不同于 TracingContext 使用鍊式數組來處理 Span 的出入棧,IgnoredTracerContext 使用
- 通過這兩個屬性和相應空方法的實作,以減少 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。TraceSegment#ref(TraceSegmentRef)
- 第 149 行:調用
方法,将傳播的分布式鍊路追蹤全局編号,添加到 TraceSegment 中,進行指向全局編号。TraceSegment#relatedGlobalTraces(DistributedTraceId)
另外,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
,傳輸載體項。代碼如:
-
屬性,Header 鍵。headKey
-
屬性,Header 值。headValue
-
屬性,下一個項。next
CarrierItem 有兩個子類:
- CarrierItemHead :Carrier 項的頭( Head ),即首個元素。
- SW3CarrierItem :
,用于傳輸 ContextCarrier 。header = sw3
如下是 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()