.NET?Core分布式鏈路追蹤框架的基本實現(xiàn)原理
分布式追蹤
什么是分布式追蹤
分布式系統(tǒng)
當我們使用 Google 或者 百度搜索時,查詢服務會將關鍵字分發(fā)到多臺查詢服務器,每臺服務器在自己的索引范圍內(nèi)進行搜索,搜索引擎可以在短時間內(nèi)獲得大量準確的搜索結果;同時,根據(jù)關鍵字,廣告子系統(tǒng)會推送合適的相關廣告,還會從競價排名子系統(tǒng)獲得網(wǎng)站權重。通常一個搜索可能需要成千上萬臺服務器參與,需要經(jīng)過許多不同的系統(tǒng)提供服務。
多臺計算機通過網(wǎng)絡組成了一個龐大的系統(tǒng),這個系統(tǒng)即是分布式系統(tǒng)。
在微服務或者云原生開發(fā)中,一般認為分布式系統(tǒng)是通過各種中間件/服務網(wǎng)格連接的,這些中間件提供了共享資源、功能(API等)、文件等,使得整個網(wǎng)絡可以當作一臺計算機進行工作。
分布式追蹤
在分布式系統(tǒng)中,用戶的一個請求會被分發(fā)到多個子系統(tǒng)中,被不同的服務處理,最后將結果返回給用戶。用戶發(fā)出請求和獲得結果這段時間是一個請求周期。
當我們購物時,只需要一個很簡單的過程:
獲取優(yōu)惠劵 -> 下單 -> 付款 -> 等待收貨
然而在后臺系統(tǒng)中,每一個環(huán)節(jié)都需要經(jīng)過多個子系統(tǒng)進行協(xié)作,并且有嚴格的流程。例如在下單時,需要檢查是否有優(yōu)惠卷、優(yōu)惠劵能不能用于當前商品、當前訂單是否符合使用優(yōu)惠劵條件等。
下圖是一個用戶請求后,系統(tǒng)處理請求的流程。

圖中出現(xiàn)了很多箭頭,這些箭頭指向了下一步要流經(jīng)的服務/子系統(tǒng),這些箭頭組成了鏈路網(wǎng)絡。
在一個復雜的分布式系統(tǒng)中,任何子系統(tǒng)出現(xiàn)性能不佳的情況,都會影響整個請求周期。根據(jù)上圖,我們設想:
1.系統(tǒng)中有可能每天都在增加新服務或刪除舊服務,也可能進行升級,當系統(tǒng)出現(xiàn)錯誤,我們?nèi)绾味ㄎ粏栴}?
2.當用戶請求時,響應緩慢,怎么定位問題?
3.服務可能由不同的編程語言開發(fā),1、2 定位問題的方式,是否適合所有編程語言?
分布式追蹤有什么用呢
隨著微服務和云原生開發(fā)的興起,越來越多應用基于分布式進行開發(fā),但是大型應用拆分為微服務后,服務之間的依賴和調用變得越來越復雜,這些服務是不同團隊、使用不同語言開發(fā)的,部署在不同機器上,他們之間提供的接口可能不同(gRPC、Restful api等)。
為了維護這些服務,軟件領域出現(xiàn)了 Observability 思想,在這個思想中,對微服務的維護分為三個部分:
- 度量(
Metrics):用于監(jiān)控和報警; - 分布式追蹤(Tracing):用于記錄系統(tǒng)中所有的跟蹤信息;
- 日志(Logging):記錄每個服務只能中離散的信息;
這三部分并不是獨立開來的,例如 Metrics 可以監(jiān)控 Tracing 、Logging 服務是否正常運行。Tacing 和 Metrics 服務在運行過程中會產(chǎn)生日志。

近年來,出現(xiàn)了 APM 系統(tǒng),APM 稱為 應用程序性能管理系統(tǒng),可以進行 軟件性能監(jiān)視和性能分析。APM 是一種 Metrics,但是現(xiàn)在有融合 Tracing 的趨勢。
回歸正題,分布式追蹤系統(tǒng)(Tracing)有什么用呢?這里可以以 Jaeger 舉例,它可以:
- 分布式跟蹤信息傳遞
- 分布式事務監(jiān)控
- 服務依賴性分析
- 展示跨進程調用鏈
- 定位問題
- 性能優(yōu)化
Jaeger 需要結合后端進行結果分析,jaeger 有個 Jaeger UI,但是功能并不多,因此還需要依賴 Metrics 框架從結果呈現(xiàn)中可視化,以及自定義監(jiān)控、告警規(guī)則,所以很自然 Metrics 可能會把 Tracing 的事情也做了。
Dapper
Dapper 是 Google 內(nèi)部使用的分布式鏈路追蹤系統(tǒng),并沒有開源。
Dapper 用戶接口:

分布式追蹤系統(tǒng)的實現(xiàn)
下圖是一個由用戶 X 請求發(fā)起的,穿過多個服務的分布式系統(tǒng),A、B、C、D、E 表示不同的子系統(tǒng)或處理過程。

在這個圖中, A 是前端,B、C 是中間層、D、E 是 C 的后端。這些子系統(tǒng)通過 rpc 協(xié)議連接,例如 gRPC。
一個簡單實用的分布式鏈路追蹤系統(tǒng)的實現(xiàn),就是對服務器上每一次請求以及響應收集跟蹤標識符(message identifiers)和時間戳(timestamped events)。
分布式服務的跟蹤系統(tǒng)需要記錄在一次特定的請求后系統(tǒng)中完成的所有工作的信息。用戶請求可以是并行的,同一時間可能有大量的動作要處理,一個請求也會經(jīng)過系統(tǒng)中的多個服務,系統(tǒng)中時時刻刻都在產(chǎn)生各種跟蹤信息,必須將一個請求在不同服務中產(chǎn)生的追蹤信息關聯(lián)起來。
為了將所有記錄條目與一個給定的發(fā)起者X關聯(lián)上并記錄所有信息,現(xiàn)在有兩種解決方案,黑盒(black-box)和基于標注(annotation-based)的監(jiān)控方案。
黑盒方案:
假定需要跟蹤的除了上述信息之外沒有額外的信息,這樣使用統(tǒng)計回歸技術來推斷兩者之間的關系。
基于標注的方案:
依賴于應用程序或中間件明確地標記一個全局ID,從而連接每一條記錄和發(fā)起者的請求。
優(yōu)缺點:
雖然黑盒方案比標注方案更輕便,他們需要更多的數(shù)據(jù),以獲得足夠的精度,因為他們依賴于統(tǒng)計推論?;跇俗⒌姆桨缸钪饕娜秉c是,很明顯,需要代碼植入。在我們的生產(chǎn)環(huán)境中,因為所有的應用程序都使用相同的線程模型,控制流和 RPC 系統(tǒng),我們發(fā)現(xiàn),可以把代碼植入限制在一個很小的通用組件庫中,從而實現(xiàn)了監(jiān)測系統(tǒng)的應用對開發(fā)人員是有效地透明。
Dapper 基于標注的方案,接下來我們將介紹 Dapper 中的一些概念知識。
跟蹤樹和 span
從形式上看,Dapper 跟蹤模型使用的是樹形結構,Span 以及 Annotation。
在前面的圖片中,我們可以看到,整個請求網(wǎng)絡是一個樹形結構,用戶請求是樹的根節(jié)點。在 Dapper 的跟蹤樹結構中,樹節(jié)點是整個架構的基本單元。
span 稱為跨度,一個節(jié)點在收到請求以及完成請求的過程是一個 span,span 記錄了在這個過程中產(chǎn)生的各種信息。每個節(jié)點處理每個請求時都會生成一個獨一無二的的 span id,當 A -> C -> D 時,多個連續(xù)的 span 會產(chǎn)生父子關系,那么一個 span 除了保存自己的 span id,也需要關聯(lián)父、子 span id。生成 span id 必須是高性能的,并且能夠明確表示時間順序,這點在后面介紹 Jaeger 時會介紹。
Annotation 譯為注釋,在一個 span 中,可以為 span 添加更多的跟蹤細節(jié),這些額外的信息可以幫助我們監(jiān)控系統(tǒng)的行為或者幫助調試問題。Annotation 可以添加任意內(nèi)容。

到此為止,簡單介紹了一些分布式追蹤以及 Dapper 的知識,但是這些不足以嚴謹?shù)恼f明分布式追蹤的知識和概念,建議讀者有空時閱讀 Dapper 論文。
要實現(xiàn) Dapper,還需要代碼埋點、采樣、跟蹤收集等,這里就不再細談了,后面會介紹到,讀者也可以看看論文。
Jaeger 和 OpenTracing
OpenTracing
OpenTracing 是與分布式系統(tǒng)無關的API和用于分布式跟蹤的工具,它不僅提供了統(tǒng)一標準的 API,還致力于各種工具,幫助開發(fā)者或服務提供者開發(fā)程序。
OpenTracing 為標準 API 提供了接入 SDK,支持這些語言:Go, JavaScript, Java, Python, Ruby, PHP, Objective-C, C++, C#。
當然,我們也可以自行根據(jù)通訊協(xié)議,自己封裝 SDK。
接下來我們要一點點弄清楚 OpenTracing 中的一些概念和知識點。由于 jaeger 是 OpenTracing 最好的實現(xiàn),因此后面講 Jaeger 就是 Opentracing ,不需要將兩者嚴格區(qū)分。
Jaeger 結構
首先是 JAEGER 部分,這部分是代碼埋點等流程,在分布式系統(tǒng)中處理,當一個跟蹤完成后,通過 jaeger-agent 將數(shù)據(jù)推送到 jaeger-collector。jaeger-collector 負責處理四面八方推送來的跟蹤信息,然后存儲到后端,可以存儲到 ES、數(shù)據(jù)庫等。Jaeger-UI 可以將讓用戶在界面上看到這些被分析出來的跟蹤信息。
OpenTracing API 被封裝成編程語言的 SDK(jaeger-client),例如在 C# 中是 .dll ,Java 是 .jar,應用程序代碼通過調用 API 實現(xiàn)代碼埋點。
jaeger-Agent 是一個監(jiān)聽在 UDP 端口上接收 span 數(shù)據(jù)的網(wǎng)絡守護進程,它會將數(shù)據(jù)批量發(fā)送給 collector。

OpenTracing 數(shù)據(jù)模型
在 OpenTracing 中,跟蹤信息被分為 Trace、Span 兩個核心,它們按照一定的結構存儲跟蹤信息,所以它們是 OpenTracing 中數(shù)據(jù)模型的核心。
Trace 是一次完整的跟蹤,Trace 由多個 Span 組成。下圖是一個 Trace 示例,由 8 個 Span 組成。

Tracing:
a Trace can be thought of as a directed acyclic graph (DAG) of Spans。
有點難翻譯,大概意思是 Trace 是多個 Span 組成的有向非循環(huán)圖。
在上面的示例中,一個 Trace 經(jīng)過了 8 個服務,A -> C -> F -> G 是有嚴格順序的,但是從時間上來看,B 、C 是可以并行的。為了準確表示這些 Span 在時間上的關系,我們可以用下圖表示:

有個要注意的地方, 并不是 A -> C -> F 表示 A 執(zhí)行結束,然后 C 開始執(zhí)行,而是 A 執(zhí)行過程中,依賴 C,而 C 依賴 F。因此,當 A 依賴 C 的過程完成后,最終回到 A 繼續(xù)執(zhí)行。所以上圖中 A 的跨度最大。
Span 格式
要深入學習,就必須先了解 Span,請讀者認真對照下面的圖片和 Json:

json 地址:https://github.com/whuanle/DistributedTracing/issues/1
后續(xù)將圍繞這張圖片和 Json 來舉例講述 Span 相關知識。
Trace
一個簡化的 Trace 如下:
注:不同編程語言的字段名稱有所差異,gRPC 和 Restful API 的格式也有所差異。
"traceID": "790e003e22209ca4",
"spans":[...],
"processes":{...}前面說到,在 OpenTracing 中,Trace 是一個有向非循環(huán)圖,那么 Trace 必定有且只有一個起點。
這個起點會創(chuàng)建一個 Trace 對象,這個對象一開始初始化了 trace id 和 process,trace id 是一個 32 個長度的字符串組成,它是一個時間戳,而 process 是起點進程所在主機的信息。
下面筆者來說一些一下 trace id 是怎么生成的。trace id 是 32個字符串組成,而實際上只使用了 16 個,因此,下面請以 16 個字符長度去理解這個過程。
首先獲取當前時間戳,例如獲得 1611467737781059 共 16 個數(shù)字,單位是微秒,表示時間 2021-01-24 13:55:37,秒以下的單位這里就不給出了,明白表示時間就行。
在 C# 中,將當前時間轉為這種時間戳的代碼:
public static long ToTimestamp(DateTime dateTime)
{
DateTime dt1970 = new DateTime(1970, 1, 1, 0, 0, 0, 0);
return (dateTime.Ticks - dt1970.Ticks)/10;
}
// 結果:1611467737781059如果我們直接使用 Guid 生成或者 string 存儲,都會消耗一些性能和內(nèi)存,而使用 long,剛剛好可以表示時間戳,還可以節(jié)約內(nèi)存。
獲得這個時間戳后,要傳輸?shù)?Jaeger Collector,要轉為 byet 數(shù)據(jù),為什么要這樣不太清楚,按照要求傳輸就是了。
將 long 轉為一個 byte 數(shù)組:
var bytes = BitConverter.GetBytes(time);
// 大小端
if (BitConverter.IsLittleEndian)
{
Array.Reverse(bytes);
}long 占 8 個字節(jié),每個 byte 值如下:
0x00 0x05 0xb9 0x9f 0x12 0x13 0xd3 0x43
然后傳輸?shù)?Jaeger Collector 中,那么獲得的是一串二進制,怎么表示為字符串的 trace id?
可以先還原成 long,然后將 long 輸出為 16 進制的字符串:
轉為字符串(這是C#):
Console.WriteLine(time.ToString("x016"));結果:
0005b99f1213d343
Span id 也是這樣轉的,每個 id 因為與時間戳相關,所以在時間上是唯一的,生成的字符串也是唯一的。
這就是 trace 中的 trace id 了,而 trace process 是發(fā)起請求的機器的信息,用 Key-Value 的形式存儲信息,其格式如下:
{
"key": "hostname",
"type": "string",
"value": "Your-PC"
},
{
"key": "ip",
"type": "string",
"value": "172.6.6.6"
},
{
"key": "jaeger.version",
"type": "string",
"value": "CSharp-0.4.2.0"
}Ttace 中的 trace id 和 process 這里說完了,接下來說 trace 的 span。
Span
Span 由以下信息組成:
- An operation name:操作名稱,必有;
- A start timestamp:開始時間戳,必有;
- A finish timestamp:結束時間戳,必有;
- Span Tags.:Key-Value 形式表示請求的標簽,可選;
- Span Logs:Key-Value 形式表示,記錄簡單的、結構化的日志,必須是字符串類型,可選;
- SpanContext :跨度上下文,在不同的 span 中傳遞,建立關系;
- Referencest:引用的其它 Span;
span 之間如果是父子關系,則可以使用 SpanContext 綁定這種關系。父子關系有 ChildOf、FollowsFrom 兩種表示,ChildOf 表示 父 Span 在一定程度上依賴子 Span,而 FollowsFrom 表示父 Span 完全不依賴其子Span 的結果。
一個 Span 的簡化信息如下(不用理會字段名稱大小寫):
{
"traceID": "790e003e22209ca4",
"spanID": "4b73f8e8e77fe9dc",
"flags": 1,
"operationName": "print-hello",
"references": [],
"startTime": 1611318628515966,
"duration": 259,
"tags": [
{
"key": "internal.span.format",
"type": "string",
"value": "proto"
}
],
"logs": [
{
"timestamp": 1611318628516206,
"fields": [
{
"key": "event",
"type": "string",
"value": "WriteLine"
}
]
}
]
}OpenTracing API
在 OpenTracing API 中,有三個主要對象:
- Tracer
- Span
- SpanContext
Tracer可以創(chuàng)建Spans并了解如何跨流程邊界對它們的元數(shù)據(jù)進行Inject(序列化)和Extract(反序列化)。它具有以下功能:
- 開始一個新的
Span Inject一個SpanContext到一個載體Extract一個SpanContext從載體
由起點進程創(chuàng)建一個 Tracer,然后啟動進程發(fā)起請求,每個動作產(chǎn)生一個 Span,如果有父子關系,Tracer 可以將它們關聯(lián)起來。當請求完成后, Tracer 將跟蹤信息推送到 Jaeger-Collector中。

SpanContext 是在不同的 Span 中傳遞信息的,SpanContext 包含了簡單的 Trace id、Span id 等信息。
我們繼續(xù)以下圖作為示例講解。
A 創(chuàng)建一個 Tracer,然后創(chuàng)建一個 Span,代表自己 (A),再創(chuàng)建兩個 Span,分別代表 B、C,然后通過 SpanContext 傳遞一些信息到 B、C;B 和 C 收到 A 的消息后,也創(chuàng)建一個 Tracer ,用來 Tracer.extract(...) ;其中 B 沒有后續(xù),可以直接返回結果;而 C 的 Tracer 繼續(xù)創(chuàng)建兩個 Span,往 D、E 傳遞 SpanContext。

這個過程比較復雜,筆者講不好,建議讀者參與 OpenTracing 的官方文檔。
詳細的 OpenTracing API,可以通過編程語言編寫相應服務時,去學習各種 API 的使用。
到此這篇關于.NET Core分布式鏈路追蹤框架的基本實現(xiàn)原理的文章就介紹到這了。希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
相關文章
Asp.Net?Core7?preview4限流中間件新特性詳解
這篇文章主要為大家介紹了Asp.Net?Core7?preview4限流中間件的新特性示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-05-05
淺析Repeater控件的使用 (原樣導出和動態(tài)顯示/隱藏Repeater中的列)
本文主要介紹了淺析Repeater控件的使用 (原樣導出和動態(tài)顯示/隱藏Repeater中的列)的具體方法,需要的朋友可以看下2016-12-12
Repeater的FooterTemplate中控件內(nèi)容設置方法
Repeater的FooterTemplate中控件內(nèi)容設置方法,需要的朋友可以參考下。2009-12-12
Asp.net實時顯示文本框字數(shù)實現(xiàn)代碼
實時顯示文本框字數(shù)在日常開發(fā)中很常見,也很實用,接下來為大家介紹下如何實現(xiàn)實時顯示,感興趣的朋友可以參考下哈,希望可以幫助到你2013-04-04
asp.net音頻轉換之.amr轉.mp3(利用七牛轉換法)
相信很多人都遇到amr格式的音頻文件不能直接在網(wǎng)頁播放的問題,有人使用QuickTime插件的輔助,下面這篇文章主要給大家介紹了asp.net音頻轉換之利用七牛轉換法將.amr格式轉.mp3格式,需要的朋友可以參考借鑒,下面來一起看看吧。2016-12-12

