SkyWalking 中 Trace 的相關(guān)概念以及實(shí)現(xiàn)類與 OpenTracing 中的概念基本類似,像 Trace、Span、Tags、Logs 等核心概念,在 SkyWalking Agent 中都有對(duì)應(yīng)實(shí)現(xiàn),只是在細(xì)微實(shí)現(xiàn)上略有區(qū)別的,其中最重要的是: SkyWalking 的設(shè)計(jì)在 Trace 級(jí)別和 Span 級(jí)別之間加了一個(gè) Segment 概念,用于表示一個(gè)服務(wù)實(shí)例內(nèi)的 Span 集合。
Trace ID
在分布式鏈路追蹤系統(tǒng)中,用戶請(qǐng)求的處理過程會(huì)形成一條 Trace 。Trace ID 作為 Trace 數(shù)據(jù)的唯一標(biāo)識(shí),在面對(duì)海量請(qǐng)求的時(shí)候,需要保證其唯一性。與此同時(shí),還要保證生成 Trace ID 不會(huì)帶來過多開銷,所以在業(yè)務(wù)場(chǎng)景中依賴數(shù)據(jù)庫(自增鍵或是類似 Meituan-Dianping/Leaf 的 ID 生成方式)都不適合 Trace 的場(chǎng)景。
這種要求快速、高性能生成唯一 ID 的需求場(chǎng)景,一般會(huì)將 snowflake 算法與實(shí)際的場(chǎng)景集合進(jìn)行改造。
snowflake 算法是 Twitter 開源的分布式 ID 生成算法 。snowflake 算法的核心思想是將一個(gè) ID(long類型)的 64 個(gè) bit 進(jìn)行切分,其中使用 41 個(gè) bit 作為毫秒數(shù),10 個(gè) bit 作為機(jī)器的 ID( 5 個(gè) bit 記錄數(shù)據(jù)中心的 ID,5 個(gè) bit 記錄機(jī)器的 ID ),12 bit 作為毫秒內(nèi)的自增 ID,還有一個(gè) bit 位永遠(yuǎn)是 0。snowflake 算法生成的 ID 結(jié)構(gòu)如下圖所示:
snowflake 算法的好處是 ID 可以直接靠算法在內(nèi)存中產(chǎn)生,內(nèi)存內(nèi)的鎖控制并發(fā),不需依賴 MySQL 這樣的外部依賴,無維護(hù)成本。缺點(diǎn)就是每個(gè)機(jī)器節(jié)點(diǎn)在每毫秒內(nèi)只可以產(chǎn)生 4096 個(gè) ID,超出這個(gè)范圍就會(huì)溢出。另外,如果機(jī)器回?fù)芰藭r(shí)間,就會(huì)生成重復(fù)的 ID。
ID 類是 SkyWalking 中對(duì)全局唯一標(biāo)識(shí)的抽象,其生成策略與 snowflake 算法類似。SkyWalking ID 由三個(gè) long 類型的字段(part1、part2、part3)構(gòu)成,分別記錄了 ServiceInstanceId、Thread ID 和 Context 生成序列。Context 生成序列的格式是:
${時(shí)間戳}?*?10000?+?線程自增序列([0,?9999])
ID 對(duì)象序列化之后的格式是將 part1、part2、part3 三部分用“.”分割連接起來 :
${ServiceInstanceId}.${Thread?ID}.(${時(shí)間戳}?*?10000?+?線程自增序列([0,?9999]))
GlobalIdGenerator 是 Agent 中用來生成全局唯一 ID 的基礎(chǔ)工具類,在 generate() 方法中的實(shí)現(xiàn)如下:
public static ID generate() {
? ?// THREAD_ID_SEQUENCE是 ThreadLocal<IDContext>類型,即每個(gè)線程
? ?// 維護(hù)一個(gè) IDContext對(duì)象
? ?IDContext context = THREAD_ID_SEQUENCE.get();
? ?return new ID(SERVICE_INSTANCE_ID, // service_intance_id
? ? ? ?Thread.currentThread().getId(), // 當(dāng)前線程的ID
? ? ? ?context.nextSeq() // 線程內(nèi)生成的序列號(hào)
? ?);
}
IDContext.nextSeq() 方法的實(shí)現(xiàn)如下,其中 timestamp() 方法在返回時(shí)間戳的時(shí)候,會(huì)處理時(shí)間回?fù)艿膱?chǎng)景(使用 Random 隨機(jī)生成一個(gè)時(shí)間戳),nextThreadSeq() 方法的返回值在 [0 , 9999] 這個(gè)范圍內(nèi)循環(huán):
private long nextSeq() {
? ?return timestamp() * 10000 + nextThreadSeq();
}
GlobalIdGenerator 不僅用于生成 Trace ID ,其他需要唯一 ID 的地方也會(huì)通過其 nextSeq() 方法生成。
SkyWalking 中使用 DistributedTraceId 類來抽象 Trace ID,其中封裝了一個(gè) ID 類型的字段。DistributedTraceId 有兩個(gè)實(shí)現(xiàn)類,如下圖所示:
其中,NewDistirbutedTraceId 負(fù)責(zé)生成新 Trace ID,請(qǐng)求剛剛進(jìn)入系統(tǒng)時(shí),會(huì)創(chuàng)建 NewDistirbutedTraceId 對(duì)象,其構(gòu)造方法內(nèi)部會(huì)調(diào)用 GlobalIdGenerator.generate() 方法生成 ID 對(duì)象。
PropagatedTraceId 負(fù)責(zé)處理 Trace 傳播過程中的 TraceId。PropagatedTraceId 的構(gòu)造方法接收一個(gè) String 類型參數(shù)(也就是在跨進(jìn)程傳播時(shí)序列化后的 Trace ID),解析之后得到 ID 對(duì)象。
在后面的介紹中還會(huì)涉及另一個(gè)與 Trace ID 相關(guān)的類 —— DistributedTraceIds,它表示多個(gè) Trace ID 的集合,其底層封裝了一個(gè) LinkedList<DistributedTraceId>?集合,用于記錄相關(guān)的 Trace ID。
TraceSegment
在 SkyWalking 中,TraceSegment 是一個(gè)介于 Trace 與 Span 之間的概念,它是一條 Trace 的一段,可以包含多個(gè) Span。在微服務(wù)架構(gòu)中,一個(gè)請(qǐng)求基本都會(huì)涉及跨進(jìn)程(以及跨線程)的操作,例如, RPC 調(diào)用、通過 MQ 異步執(zhí)行、HTTP 請(qǐng)求遠(yuǎn)端資源等,處理一個(gè)請(qǐng)求就需要涉及到多個(gè)服務(wù)的多個(gè)線程。TraceSegment 記錄了一個(gè)請(qǐng)求在一個(gè)線程中的執(zhí)行流程(即 Trace 信息)。將該請(qǐng)求關(guān)聯(lián)的 TraceSegment 串聯(lián)起來,就能得到該請(qǐng)求對(duì)應(yīng)的完整 Trace。
下面我們先來介紹 TraceSegment 的核心字段:
- traceSegmentId(ID 類型):TraceSegment 的全局唯一標(biāo)識(shí),是由前面介紹的 GlobalIdGenerator 生成的。
- refs(List<TraceSegmentRef> 類型):它指向父 TraceSegment。在我們常見的 RPC 調(diào)用、HTTP 請(qǐng)求等跨進(jìn)程調(diào)用中,一個(gè) TraceSegment 最多只有一個(gè)父 TraceSegment,但是在一個(gè) Consumer 批量消費(fèi) MQ 消息時(shí),同一批內(nèi)的消息可能來自不同的 Producer,這就會(huì)導(dǎo)致 Consumer 線程對(duì)應(yīng)的 TraceSegment 有多個(gè)父 TraceSegment 了,當(dāng)然,該 Consumer TraceSegment 也就屬于多個(gè) Trace 了。
- relatedGlobalTraces(DistributedTraceIds 類型):記錄當(dāng)前 TraceSegment 所屬 Trace 的 Trace ID。
- spans(List<AbstractTracingSpan> 類型):當(dāng)前 TraceSegment 包含的所有 Span。
- ignore(boolean 類型):ignore 字段表示當(dāng)前 TraceSegment 是否被忽略。主要是為了忽略一些問題 TraceSegment(主要是對(duì)只包含一個(gè) Span 的 Trace 進(jìn)行采樣收集)。
- isSizeLimited(boolean 類型):這是一個(gè)容錯(cuò)設(shè)計(jì),例如業(yè)務(wù)代碼出現(xiàn)了死循環(huán) Bug,可能會(huì)向相應(yīng)的 TraceSegment 中不斷追加 Span,為了防止對(duì)應(yīng)用內(nèi)存以及后端存儲(chǔ)造成不必要的壓力,每個(gè) TraceSegment 中 Span 的個(gè)數(shù)是有上限的(默認(rèn)值為 300),超過上限之后,就不再添加 Span了。
下圖展示了一個(gè) TraceSegment 的核心結(jié)構(gòu):
Span
TraceSegment 是由多個(gè) Span 構(gòu)成的,AbstractSpan 抽象類是 SkyWalking 對(duì) Span 概念的抽象,下圖是 Span 的繼承關(guān)系:
首先需要明確的是,我們最終直接使用的 Span 分為 3 類:
- EntrySpan:當(dāng)請(qǐng)求進(jìn)入服務(wù)時(shí)會(huì)創(chuàng)建 EntrySpan 類型的 Span,它也是 TraceSegment 中的第一個(gè) Span。例如,HTTP 服務(wù)、RPC 服務(wù)、MQ-Consumer 等入口服務(wù)的插件在接收到請(qǐng)求時(shí)都會(huì)創(chuàng)建相應(yīng)的 EntrySpan。
- LocalSpan:它是在本地方法調(diào)用時(shí)可能創(chuàng)建的 Span 類型,在后面介紹 @Trace 注解的時(shí)候我們還會(huì)看到 LocalSpan。
- ExitSpan:當(dāng)請(qǐng)求離開當(dāng)前服務(wù)、進(jìn)入其他服務(wù)時(shí)會(huì)創(chuàng)建 ExitSpan 類型的 Span。例如, Http Client 、RPC Client 發(fā)起遠(yuǎn)程調(diào)用或是 MQ-producer 生產(chǎn)消息時(shí),都會(huì)產(chǎn)生該類型的 Span。
下面我們按照 Span 的繼承結(jié)構(gòu),自頂層接口開始逐個(gè)向下介紹。首先,AsyncSpan 接口定義了一個(gè)異步 Span 的基本行為:
- prepareForAsync() 方法:Span 在當(dāng)前線程結(jié)束了,但是未被徹底關(guān)閉,依然是存活的。
- asyncFinish()方法:當(dāng)前 Span 真正關(guān)閉。它與 prepareForAsync() 方法成對(duì)出現(xiàn)。
這兩個(gè)方法在異步框架的插件中會(huì)見到。
AbstractSpan 也是一個(gè)接口,其中定義了 Span 的基本行為,其中的方法比較重要:
- getSpanId() 方法:用來獲得當(dāng)前 Span 的 ID,Span ID 是一個(gè) int 類型的值,在其所屬的 TraceSegment 中唯一,在創(chuàng)建 Span 對(duì)象時(shí)生成,從 0 開始自增。
- setOperationName()/setOperationId() 方法:用來設(shè)置 operation 名稱(或 operation ID),這兩個(gè)信息是互斥的。它們?cè)?AbstractSpan 的具體實(shí)現(xiàn)(即 AbstractTracingSpan)中,分別對(duì)應(yīng) operationId 和 operationName 兩個(gè)字段,兩者只能有一個(gè)字段有值。
operationName 即前文介紹的 EndpointName,可以是任意字符串,例如,在 Tomcat 插件中 operationName 就是 URI 地址,Dubbo 插件中 operationName 為 URL + 接口方法簽名。
- setComponent() 方法:用于設(shè)置組件類型。它有兩個(gè)重載,在 AbstractTracingSpan 實(shí)現(xiàn)中,有 componentId 和 componentName 兩個(gè)字段,兩個(gè)重載分別用于設(shè)置這兩個(gè)字段。在 ComponentsDefine 中可以找到 SkyWalking 目前支持的組件類型。
- setLayer() 方法:用于設(shè)置 SpanLayer,也就是當(dāng)前 Span 所處的位置。SpanLayer 是個(gè)枚舉,可選項(xiàng)有 DB、RPC_FRAMEWORK、HTTP、MQ、CACHE。
- tag(AbstractTag, String) 方法:用于為當(dāng)前 Span 添加鍵值對(duì)的 Tags。一個(gè) Span 可以有多個(gè) Tags。AbstractTag 中不僅包含了 String 類型的 Key 值,還包含了 Tag 的 ID 以及 canOverwrite 標(biāo)識(shí)。AbstractTracingSpan 實(shí)現(xiàn)通過維護(hù)一個(gè) ?List<TagValuePair> 集合(tags 字段)來記錄 Tag 信息,TagValuePair 中則封裝了 AbstractTag 類型的 Key 以及 String 類型的 Value。
- log() 方法:用于向當(dāng)前 Span 中添加 Log,一個(gè) Span 可以包含多條日志。在 AbstractTracingSpan 實(shí)現(xiàn)中通過維護(hù)一個(gè) List<LogDataEntity> 集合(logs 字段)來記錄 Log。LogDataEntity 會(huì)記錄日志的時(shí)間戳以及 KV 信息,以異常日志為例,其中就會(huì)包含一個(gè) Key 為“stack”的 KV,其 value 為異常堆棧。
- start() 方法:開啟 Span,其中會(huì)設(shè)置當(dāng)前 Span 的開始時(shí)間以及調(diào)用層級(jí)等信息。
- isEntry() 方法:判斷當(dāng)前是否是 EntrySpan。EntrySpan 的具體實(shí)現(xiàn)后面詳細(xì)介紹。
- isExit() 方法:判斷當(dāng)前是否是 ExitSpan。ExitSpan ?的具體實(shí)現(xiàn)后面詳細(xì)介紹。
- ref() 方法:用于設(shè)置關(guān)聯(lián)的 TraceSegment 。
AbstractTracingSpan 實(shí)現(xiàn)了 AbstractSpan 接口,定義了一些 Span 的公共字段,其中的部分字段在介紹 AbstractSpan 接口時(shí)已經(jīng)提到了,下面簡(jiǎn)單介紹一下前面未涉及的字段含義:
protected int spanId; // span的ID
protected int parentSpanId; // 記錄父Span的ID
protected List<TagValuePair> tags; // 記錄Tags的集合
protected long startTime, endTime; // Span的起止時(shí)間
protected boolean errorOccurred = false; // 標(biāo)識(shí)該Span中是否發(fā)生異常
protected List<TraceSegmentRef> refs; // 指向所屬TraceSegment
// context字段指向TraceContext,TraceContext與當(dāng)前線程綁定,與TraceSegment
// 一一對(duì)應(yīng)
protected volatile AbstractTracerContext context;
AbstractTracingSpan 中提供的方法也比較簡(jiǎn)單,基本都是上述字段的 getter/setter 方法,這些方法不再展開贅述。這里需要注意兩個(gè)方法:
- finish(TraceSegment) 方法:該方法會(huì)關(guān)閉當(dāng)前 Span ,具體行為是用 endTime 字段記錄當(dāng)前時(shí)間,并將當(dāng)前 Span 記錄到所屬 TraceSegment 的 spans 集合中。
- transform() 方法:該方法會(huì)在 Agent 上報(bào) TraceSegment 數(shù)據(jù)之前調(diào)用,它會(huì)將當(dāng)前 AbstractTracingSpan 對(duì)象轉(zhuǎn)換成 SpanObjectV2 對(duì)象。SpanObjectV2 是在 proto 文件中定義的結(jié)構(gòu)體,后面 gRPC 上報(bào) TraceSegment 數(shù)據(jù)時(shí)會(huì)將其序列化。
StackBasedTracingSpan 在繼承 AbstractTracingSpan 存儲(chǔ) Span 核心數(shù)據(jù)能力的同時(shí),還引入了棧的概念,這種 Span 可以多次調(diào)用 start() 方法和 end() 方法,但是兩者調(diào)用次數(shù)必須要配對(duì),類似出棧和入棧的操作。
下面以 EntrySpan 為例說明為什么需要“?!边@個(gè)概念,EntrySpan 表示的是一個(gè)服務(wù)的入口 Span,是 TraceSegment 的第一個(gè) Span,出現(xiàn)在服務(wù)提供方的入口,例如,Dubbo Provider、Tomcat、Spring MVC,等等。 那么為什么 EntrySpan 繼承 StackBasedTracingSpan 呢? 從前面對(duì) SkyWalking Agent 的分析來看,Agent 插件只會(huì)攔截指定類的指定方法并對(duì)其進(jìn)行增強(qiáng),例如,Tomcat、Spring MVC 等插件的增強(qiáng)邏輯中就包含了創(chuàng)建 EntrySpan 的邏輯(后面在分析具體插件實(shí)現(xiàn)的時(shí)候,會(huì)看到具體的實(shí)現(xiàn)代碼)。很多 Web 項(xiàng)目會(huì)同時(shí)使用到這兩個(gè)插件,難道一個(gè) TraceSegment 要有兩個(gè) EntrySpan 嗎?顯然不行。
SkyWalking 的處理方式是讓 EntrySpan 繼承了 StackBasedTracingSpan,多個(gè)插件同時(shí)使用時(shí),整個(gè)架構(gòu)如下所示:
其中,請(qǐng)求相應(yīng)的 EntrySpan 處理流程如下:
- 當(dāng)請(qǐng)求經(jīng)過 Tomcat 插件時(shí)(即圖中 ① 處),會(huì)創(chuàng)建 EntrySpan 并第一次調(diào)用 start() 方法,啟動(dòng)該 EntrySpan。
在 start() 方法中會(huì)有下面幾個(gè)操作:
- 將 stackDepth 字段(定義在 StackBasedTracingSpan 中)加 1,stackDepth 表示當(dāng)前所處的插件棧深度 。
- 更新 currentMaxDepth 字段(定義在 EntrySpan 中),currentMaxDepth 會(huì)記錄該EntrySpan 到達(dá)過的插件棧的最深位置。
- 此時(shí)第一次啟動(dòng) EntrySpan 時(shí)會(huì)更新 startTime 字段,記錄請(qǐng)求開始時(shí)間。
此時(shí)插件棧(這是為了方便理解而虛擬出來一個(gè)棧結(jié)構(gòu),實(shí)際上只有 stackDepth、currentMaxDepth 兩個(gè)字段,并不會(huì)用到棧結(jié)構(gòu),也不會(huì)記錄請(qǐng)求經(jīng)過的插件)的狀態(tài)如下圖所示:
- 當(dāng)請(qǐng)求經(jīng)過 Spring MVC 插件時(shí)(即圖中 ② 處),不會(huì)再創(chuàng)建新的 EntrySpan 了,而重新調(diào)用該 EntrySpan 的 start() 方法,其中會(huì)繼續(xù)將 stackDepth 以及 currentMaxDepth 字段加 1 。注意,再次調(diào)用 start() 方法時(shí)不會(huì)更新 startTime 字段了,因?yàn)檎?qǐng)求已經(jīng)開始處理了。此時(shí)插件棧的狀態(tài)如下圖:
- 當(dāng)請(qǐng)求經(jīng)過業(yè)務(wù)邏輯處理完成之后,開始進(jìn)入 Spring MVC 插件的后置處理邏輯時(shí)(即圖中 ③ 處),會(huì)第 1 次調(diào)用 EntrySpan.finish() 方法,其中會(huì)將 stackDepth 減 1,即 Spring MVC 插件出棧,此時(shí)插件棧的狀態(tài)如下圖:
- 最后進(jìn)入 Tomcat 插件的后置處理邏輯(即圖中 ④ 處),其中會(huì)第 2 次調(diào)用 finish() 方法,此時(shí) stackDepth 再次減 1,此時(shí) stackDepth 減到了 0 ,整個(gè)插件棧已經(jīng)空了,會(huì)調(diào)用父類 AbstractTracingSpan 的 finish() 方法將當(dāng)前 EntrySpan 添加到關(guān)聯(lián)的 TraceSegment 中。
這里需要注意兩個(gè)點(diǎn),一是在調(diào)用 start() 方法時(shí),會(huì)將之前設(shè)置的 component、Tags、Log 等信息全部清理掉(startTime不會(huì)清理),上例中請(qǐng)求到 Spring MVC 插件之前(即 ② 處之前)設(shè)置的這些信息都會(huì)被清理掉。二是 stackDepth 與 currentMaxDepth 不相等時(shí)(上例中 ③ 處),無法記錄上述字段的信息。通過這兩點(diǎn),我們知道 EntrySpan 實(shí)際上只會(huì)記錄最貼近業(yè)務(wù)側(cè)的 Span 信息。
StackBasedTracingSpan 除了將“?!备拍钆c EntrySpan 結(jié)合之外,還添加了 peer(以及 peerId)字段來記錄遠(yuǎn)端地址,在發(fā)送遠(yuǎn)程調(diào)用時(shí)創(chuàng)建的 ExitSpan 會(huì)將該記錄用于對(duì)端地址。
ExitSpan 表示的是出口 Span,如果在一個(gè)調(diào)用棧里面出現(xiàn)多個(gè)插件嵌套的場(chǎng)景,也需要通過“?!钡姆绞竭M(jìn)行處理,與上述邏輯類似,只會(huì)在第一個(gè)插件中創(chuàng)建 ExitSpan,后續(xù)調(diào)用的 ExitSpan.start() 方法并不會(huì)更新 startTime,只會(huì)增加棧的深度。當(dāng)然,在設(shè)置 Tags、Log 等信息時(shí)也會(huì)進(jìn)行判斷,只有 stackDepth 為 1 的時(shí)候,才會(huì)能正常寫入相應(yīng)字段。也就是說,ExitSpan 中只會(huì)記錄最貼近當(dāng)前服務(wù)側(cè)的 Span 信息。
一個(gè) TraceSegment 可以有多個(gè) ExitSpan,例如,Dubbo A 服務(wù)在處理一個(gè)請(qǐng)求時(shí),會(huì)調(diào)用 Dubbo B 服務(wù),在得到響應(yīng)之后,會(huì)緊接著調(diào)用 Dubbo C 服務(wù),這樣,該 TraceSegment 就有了兩個(gè)完全獨(dú)立的 ExitSpan。文章來源:http://www.zghlxwxcb.cn/news/detail-552617.html
LocalSpan 則比較簡(jiǎn)單,它表示一個(gè)本地方法調(diào)用。LocalSpan 直接繼承了 AbstractTracingSpan,由于它未繼承 StackBasedTracingSpan,所以也不能 start 或 end 多次,在后面介紹 @Trace 注解的相關(guān)實(shí)現(xiàn)時(shí),還會(huì)看到 LocalSpan 的身影。文章來源地址http://www.zghlxwxcb.cn/news/detail-552617.html
到了這里,關(guān)于第12講:剖析 Trace 在 SkyWalking 中的落地實(shí)現(xiàn)方案(上)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!