您好,登錄后才能下訂單哦!
本文小編為大家詳細介紹“.NET Core分布式鏈路追蹤框架的實現原理是什么”,內容詳細,步驟清晰,細節處理妥當,希望這篇“.NET Core分布式鏈路追蹤框架的實現原理是什么”文章能幫助大家解決疑惑,下面跟著小編的思路慢慢深入,一起來學習新知識吧。
當我們使用 Google 或者 百度搜索時,查詢服務會將關鍵字分發到多臺查詢服務器,每臺服務器在自己的索引范圍內進行搜索,搜索引擎可以在短時間內獲得大量準確的搜索結果;同時,根據關鍵字,廣告子系統會推送合適的相關廣告,還會從競價排名子系統獲得網站權重。通常一個搜索可能需要成千上萬臺服務器參與,需要經過許多不同的系統提供服務。
多臺計算機通過網絡組成了一個龐大的系統,這個系統即是分布式系統。
在微服務或者云原生開發中,一般認為分布式系統是通過各種中間件/服務網格連接的,這些中間件提供了共享資源、功能(API等)、文件等,使得整個網絡可以當作一臺計算機進行工作。
在分布式系統中,用戶的一個請求會被分發到多個子系統中,被不同的服務處理,最后將結果返回給用戶。用戶發出請求和獲得結果這段時間是一個請求周期。
當我們購物時,只需要一個很簡單的過程:
獲取優惠劵 -> 下單 -> 付款 -> 等待收貨
然而在后臺系統中,每一個環節都需要經過多個子系統進行協作,并且有嚴格的流程。例如在下單時,需要檢查是否有優惠卷、優惠劵能不能用于當前商品、當前訂單是否符合使用優惠劵條件等。
下圖是一個用戶請求后,系統處理請求的流程。
圖中出現了很多箭頭,這些箭頭指向了下一步要流經的服務/子系統,這些箭頭組成了鏈路網絡。
在一個復雜的分布式系統中,任何子系統出現性能不佳的情況,都會影響整個請求周期。根據上圖,我們設想:
1.系統中有可能每天都在增加新服務或刪除舊服務,也可能進行升級,當系統出現錯誤,我們如何定位問題?
2.當用戶請求時,響應緩慢,怎么定位問題?
3.服務可能由不同的編程語言開發,1、2 定位問題的方式,是否適合所有編程語言?
隨著微服務和云原生開發的興起,越來越多應用基于分布式進行開發,但是大型應用拆分為微服務后,服務之間的依賴和調用變得越來越復雜,這些服務是不同團隊、使用不同語言開發的,部署在不同機器上,他們之間提供的接口可能不同(gRPC、Restful api等)。
為了維護這些服務,軟件領域出現了 Observability 思想,在這個思想中,對微服務的維護分為三個部分:
度量(Metrics
):用于監控和報警;
分布式追蹤(Tracing):用于記錄系統中所有的跟蹤信息;
日志(Logging):記錄每個服務只能中離散的信息;
這三部分并不是獨立開來的,例如 Metrics 可以監控 Tracing 、Logging 服務是否正常運行。Tacing 和 Metrics 服務在運行過程中會產生日志。
近年來,出現了 APM 系統,APM 稱為 應用程序性能管理系統,可以進行 軟件性能監視和性能分析。APM 是一種 Metrics,但是現在有融合 Tracing 的趨勢。
回歸正題,分布式追蹤系統(Tracing)有什么用呢?這里可以以 Jaeger 舉例,它可以:
分布式跟蹤信息傳遞
分布式事務監控
服務依賴性分析
展示跨進程調用鏈
定位問題
性能優化
Jaeger 需要結合后端進行結果分析,jaeger 有個 Jaeger UI,但是功能并不多,因此還需要依賴 Metrics 框架從結果呈現中可視化,以及自定義監控、告警規則,所以很自然 Metrics 可能會把 Tracing 的事情也做了。
Dapper 是 Google 內部使用的分布式鏈路追蹤系統,并沒有開源。
Dapper 用戶接口:
下圖是一個由用戶 X 請求發起的,穿過多個服務的分布式系統,A、B、C、D、E 表示不同的子系統或處理過程。
在這個圖中, A 是前端,B、C 是中間層、D、E 是 C 的后端。這些子系統通過 rpc 協議連接,例如 gRPC。
一個簡單實用的分布式鏈路追蹤系統的實現,就是對服務器上每一次請求以及響應收集跟蹤標識符(message identifiers)和時間戳(timestamped events)。
分布式服務的跟蹤系統需要記錄在一次特定的請求后系統中完成的所有工作的信息。用戶請求可以是并行的,同一時間可能有大量的動作要處理,一個請求也會經過系統中的多個服務,系統中時時刻刻都在產生各種跟蹤信息,必須將一個請求在不同服務中產生的追蹤信息關聯起來。
為了將所有記錄條目與一個給定的發起者X關聯上并記錄所有信息,現在有兩種解決方案,黑盒(black-box)和基于標注(annotation-based)的監控方案。
黑盒方案:
假定需要跟蹤的除了上述信息之外沒有額外的信息,這樣使用統計回歸技術來推斷兩者之間的關系。
基于標注的方案:
依賴于應用程序或中間件明確地標記一個全局ID,從而連接每一條記錄和發起者的請求。
優缺點:
雖然黑盒方案比標注方案更輕便,他們需要更多的數據,以獲得足夠的精度,因為他們依賴于統計推論。基于標注的方案最主要的缺點是,很明顯,需要代碼植入。在我們的生產環境中,因為所有的應用程序都使用相同的線程模型,控制流和 RPC 系統,我們發現,可以把代碼植入限制在一個很小的通用組件庫中,從而實現了監測系統的應用對開發人員是有效地透明。
Dapper 基于標注的方案,接下來我們將介紹 Dapper 中的一些概念知識。
從形式上看,Dapper 跟蹤模型使用的是樹形結構,Span 以及 Annotation。
在前面的圖片中,我們可以看到,整個請求網絡是一個樹形結構,用戶請求是樹的根節點。在 Dapper 的跟蹤樹結構中,樹節點是整個架構的基本單元。
span 稱為跨度,一個節點在收到請求以及完成請求的過程是一個 span,span 記錄了在這個過程中產生的各種信息。每個節點處理每個請求時都會生成一個獨一無二的的 span id,當 A -> C -> D 時,多個連續的 span 會產生父子關系,那么一個 span 除了保存自己的 span id,也需要關聯父、子 span id。生成 span id 必須是高性能的,并且能夠明確表示時間順序,這點在后面介紹 Jaeger 時會介紹。
Annotation 譯為注釋,在一個 span 中,可以為 span 添加更多的跟蹤細節,這些額外的信息可以幫助我們監控系統的行為或者幫助調試問題。Annotation 可以添加任意內容。
到此為止,簡單介紹了一些分布式追蹤以及 Dapper 的知識,但是這些不足以嚴謹的說明分布式追蹤的知識和概念,建議讀者有空時閱讀 Dapper 論文。
要實現 Dapper,還需要代碼埋點、采樣、跟蹤收集等,這里就不再細談了,后面會介紹到,讀者也可以看看論文。
OpenTracing 是與分布式系統無關的API和用于分布式跟蹤的工具,它不僅提供了統一標準的 API,還致力于各種工具,幫助開發者或服務提供者開發程序。
OpenTracing 為標準 API 提供了接入 SDK,支持這些語言:Go, JavaScript, Java, Python, Ruby, PHP, Objective-C, C++, C#。
當然,我們也可以自行根據通訊協議,自己封裝 SDK。
接下來我們要一點點弄清楚 OpenTracing 中的一些概念和知識點。由于 jaeger 是 OpenTracing 最好的實現,因此后面講 Jaeger 就是 Opentracing ,不需要將兩者嚴格區分。
首先是 JAEGER 部分,這部分是代碼埋點等流程,在分布式系統中處理,當一個跟蹤完成后,通過 jaeger-agent 將數據推送到 jaeger-collector。jaeger-collector 負責處理四面八方推送來的跟蹤信息,然后存儲到后端,可以存儲到 ES、數據庫等。Jaeger-UI 可以將讓用戶在界面上看到這些被分析出來的跟蹤信息。
OpenTracing API 被封裝成編程語言的 SDK(jaeger-client),例如在 C# 中是 .dll ,Java 是 .jar,應用程序代碼通過調用 API 實現代碼埋點。
jaeger-Agent 是一個監聽在 UDP 端口上接收 span 數據的網絡守護進程,它會將數據批量發送給 collector。
在 OpenTracing 中,跟蹤信息被分為 Trace、Span 兩個核心,它們按照一定的結構存儲跟蹤信息,所以它們是 OpenTracing 中數據模型的核心。
Trace 是一次完整的跟蹤,Trace 由多個 Span 組成。下圖是一個 Trace 示例,由 8 個 Span 組成。
Tracing:
a Trace can be thought of as a directed acyclic graph (DAG) of Spans。
有點難翻譯,大概意思是 Trace 是多個 Span 組成的有向非循環圖。
在上面的示例中,一個 Trace 經過了 8 個服務,A -> C -> F -> G 是有嚴格順序的,但是從時間上來看,B 、C 是可以并行的。為了準確表示這些 Span 在時間上的關系,我們可以用下圖表示:
有個要注意的地方, 并不是 A -> C -> F 表示 A 執行結束,然后 C 開始執行,而是 A 執行過程中,依賴 C,而 C 依賴 F。因此,當 A 依賴 C 的過程完成后,最終回到 A 繼續執行。所以上圖中 A 的跨度最大。
要深入學習,就必須先了解 Span,請讀者認真對照下面的圖片和 Json:
json 地址:https://github.com/whuanle/DistributedTracing/issues/1
后續將圍繞這張圖片和 Json 來舉例講述 Span 相關知識。
一個簡化的 Trace 如下:
注:不同編程語言的字段名稱有所差異,gRPC 和 Restful API 的格式也有所差異。
"traceID": "790e003e22209ca4", "spans":[...], "processes":{...}
前面說到,在 OpenTracing 中,Trace 是一個有向非循環圖,那么 Trace 必定有且只有一個起點。
這個起點會創建一個 Trace 對象,這個對象一開始初始化了 trace id 和 process,trace id 是一個 32 個長度的字符串組成,它是一個時間戳,而 process 是起點進程所在主機的信息。
下面筆者來說一些一下 trace id 是怎么生成的。trace id 是 32個字符串組成,而實際上只使用了 16 個,因此,下面請以 16 個字符長度去理解這個過程。
首先獲取當前時間戳,例如獲得 1611467737781059
共 16 個數字,單位是微秒,表示時間 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 存儲,都會消耗一些性能和內存,而使用 long,剛剛好可以表示時間戳,還可以節約內存。
獲得這個時間戳后,要傳輸到 Jaeger Collector,要轉為 byet 數據,為什么要這樣不太清楚,按照要求傳輸就是了。
將 long 轉為一個 byte 數組:
var bytes = BitConverter.GetBytes(time); // 大小端 if (BitConverter.IsLittleEndian) { Array.Reverse(bytes); }
long 占 8 個字節,每個 byte 值如下:
0x00 0x05 0xb9 0x9f 0x12 0x13 0xd3 0x43
然后傳輸到 Jaeger Collector 中,那么獲得的是一串二進制,怎么表示為字符串的 trace id?
可以先還原成 long,然后將 long 輸出為 16 進制的字符串:
轉為字符串(這是C#):
Console.WriteLine(time.ToString("x016"));
結果:
0005b99f1213d343
Span id 也是這樣轉的,每個 id 因為與時間戳相關,所以在時間上是唯一的,生成的字符串也是唯一的。
這就是 trace 中的 trace id 了,而 trace process 是發起請求的機器的信息,用 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 由以下信息組成:
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 中,有三個主要對象:
Tracer
Span
SpanContext
Tracer
可以創建Spans
并了解如何跨流程邊界對它們的元數據進行Inject
(序列化)和Extract
(反序列化)。它具有以下功能:
開始一個新的 Span
Inject
一個SpanContext
到一個載體
Extract
一個SpanContext
從載體
由起點進程創建一個 Tracer,然后啟動進程發起請求,每個動作產生一個 Span,如果有父子關系,Tracer 可以將它們關聯起來。當請求完成后, Tracer 將跟蹤信息推送到 Jaeger-Collector中。
SpanContext 是在不同的 Span 中傳遞信息的,SpanContext 包含了簡單的 Trace id、Span id 等信息。
我們繼續以下圖作為示例講解。
A 創建一個 Tracer,然后創建一個 Span,代表自己 (A),再創建兩個 Span,分別代表 B、C,然后通過 SpanContext 傳遞一些信息到 B、C;B 和 C 收到 A 的消息后,也創建一個 Tracer ,用來 Tracer.extract(...)
;其中 B 沒有后續,可以直接返回結果;而 C 的 Tracer 繼續創建兩個 Span,往 D、E 傳遞 SpanContext。
讀到這里,這篇“.NET Core分布式鏈路追蹤框架的實現原理是什么”文章已經介紹完畢,想要掌握這篇文章的知識點還需要大家自己動手實踐使用過才能領會,如果想了解更多相關內容的文章,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。