.NET Core 中的日誌與分佈式鏈路追蹤html
分佈式鏈路追蹤框架的基本實現原理(當前)前端
開源一個簡單的兼容 Jaeger 的框架git
檸檬(Lemon丶)大佬在一月份開業了檸檬研究院,研究院指導成員學習分佈式和雲原生技術,本月課題是分佈式鏈路追蹤,學習 Dapper 論文、Jaeger 的使用,以及完成一個兼容 Jaeger 的鏈路追蹤框架。github
筆者將做業分爲三部分,三篇文章加上實現代碼,本文是第二篇。數據庫
當咱們使用 Google 或者 百度搜索時,查詢服務會將關鍵字分發到多臺查詢服務器,每臺服務器在本身的索引範圍內進行搜索,搜索引擎能夠在短期內得到大量準確的搜索結果;同時,根據關鍵字,廣告子系統會推送合適的相關廣告,還會從競價排名子系統得到網站權重。一般一個搜索可能須要成千上萬臺服務器參與,須要通過許多不一樣的系統提供服務。編程
多臺計算機經過網絡組成了一個龐大的系統,這個系統便是分佈式系統。json
在微服務或者雲原生開發中,通常認爲分佈式系統是經過各類中間件/服務網格鏈接的,這些中間件提供了共享資源、功能(API等)、文件等,使得整個網絡能夠看成一臺計算機進行工做。segmentfault
在分佈式系統中,用戶的一個請求會被分發到多個子系統中,被不一樣的服務處理,最後將結果返回給用戶。用戶發出請求和得到結果這段時間是一個請求週期。後端
當咱們購物時,只須要一個很簡單的過程:api
獲取優惠劵 -> 下單 -> 付款 -> 等待收貨
然而在後臺系統中,每個環節都須要通過多個子系統進行協做,而且有嚴格的流程。例如在下單時,須要檢查是否有優惠卷、優惠劵能不能用於當前商品、當前訂單是否符合使用優惠劵條件等。
下圖是一個用戶請求後,系統處理請求的流程。
【圖片來源:鷹眼下的淘寶分佈式調用跟蹤系統介紹】
圖中出現了不少箭頭,這些箭頭指向了下一步要流經的服務/子系統,這些箭頭組成了鏈路網絡。
在一個複雜的分佈式系統中,任何子系統出現性能不佳的狀況,都會影響整個請求週期。根據上圖,咱們設想:
1.系統中有可能天天都在增長新服務或刪除舊服務,也可能進行升級,當系統出現錯誤,咱們如何定位問題?
2.當用戶請求時,響應緩慢,怎麼定位問題?
3.服務可能由不一樣的編程語言開發,一、2 定位問題的方式,是否適合全部編程語言?
隨着微服務和雲原生開發的興起,愈來愈多應用基於分佈式進行開發,可是大型應用拆分爲微服務後,服務之間的依賴和調用變得愈來愈複雜,這些服務是不一樣團隊、使用不一樣語言開發的,部署在不一樣機器上,他們之間提供的接口可能不一樣(gRPC、Restful api等)。
爲了維護這些服務,軟件領域出現了 Observability 思想,在這個思想中,對微服務的維護分爲三個部分:
Metrics
):用於監控和報警;這三部分並非獨立開來的,例如 Metrics 能夠監控 Tracing 、Logging 服務是否正常運行。Tacing 和 Metrics 服務在運行過程當中會產生日誌。
深刻了解請戳爆你的屏幕:https://peter.bourgon.org/blog/2017/02/21/metrics-tracing-and-logging.html
近年來,出現了 APM 系統,APM 稱爲 應用程序性能管理系統,能夠進行 軟件性能監視和性能分析。APM 是一種 Metrics,可是如今有融合 Tracing 的趨勢。
迴歸正題,分佈式追蹤系統(Tracing)有什麼用呢?這裏能夠以 Jaeger 舉例,它能夠:
Jaeger 須要結合後端進行結果分析,jaeger 有個 Jaeger UI,可是功能並很少,所以還須要依賴 Metrics 框架從結果呈現中可視化,以及自定義監控、告警規則,因此很天然 Metrics 可能會把 Tracing 的事情也作了。
Dapper 是 Google 內部使用的分佈式鏈路追蹤系統,並無開源,可是 Google 發佈了一篇 《Dapper, a Large-Scale Distributed Systems Tracing Infrastructure》 論文,這篇論文講述了分佈式鏈路追蹤的理論和 Dapper 的設計思想。
有不少鏈路追蹤系統是基於 Dapper 論文的,例如淘寶的鷹眼、Twitter 的 Zipkin、Uber 開源的 Jaeger,分佈式鏈路追蹤標準 OpenTracing 等。
論文地址:
https://static.googleusercontent.com/media/research.google.com/en//archive/papers/dapper-2010-1.pdf
譯文:
http://bigbully.github.io/Dapper-translation/
不能訪問 github.io 的話,能夠 clone 倉庫去看 https://github.com/bigbully/Dapper-translation/tree/gh-pages
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 文檔:https://opentracing.io/docs/
接下來咱們要一點點弄清楚 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。
【圖片來源:http://www.javashuo.com/article/p-zqcwamkm-d.html】
在 OpenTracing 中,跟蹤信息被分爲 Trace、Span 兩個核心,它們按照必定的結構存儲跟蹤信息,因此它們是 OpenTracing 中數據模型的核心。
Trace 是一次完整的跟蹤,Trace 由多個 Span 組成。下圖是一個 Trace 示例,由 8 個 Span 組成。
[Span A] ←←←(the root span) | +------+------+ | | [Span B] [Span C] ←←←(Span C is a `ChildOf` Span A) | | [Span D] +---+-------+ | | [Span E] [Span F] >>> [Span G] >>> [Span H] ↑ ↑ ↑ (Span G `FollowsFrom` Span F)
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 在時間上的關係,咱們能夠用下圖表示:
––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–> time [Span A···················································] [Span B··············································] [Span D··········································] [Span C········································] [Span E·······] [Span F··] [Span G··] [Span H··]
有個要注意的地方, 並非 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 由如下信息組成:
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
能夠建立Spans
並瞭解如何跨流程邊界對它們的元數據進行Inject
(序列化)和Extract
(反序列化)。它具備如下功能:
Span
Inject
一個SpanContext
到一個載體Extract
一個SpanContext
從載體由起點進程建立一個 Tracer,而後啓動進程發起請求,每一個動做產生一個 Span,若是有父子關係,Tracer 能夠將它們關聯起來。當請求完成後, Tracer 將跟蹤信息推送到 Jaeger-Collector中。
詳細請查閱文檔:https://opentracing.io/docs/overview/tracers/
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。
這個過程比較複雜,筆者講很差,建議讀者參與 OpenTracing 的官方文檔。
詳細的 OpenTracing API,能夠經過編程語言編寫相應服務時,去學習各類 API 的使用。
.NET Core 筆者寫了一篇,讀者有興趣能夠閱讀:【.NET Core 中的日誌與分佈式鏈路追蹤】http://www.javashuo.com/article/p-oooxbaem-nz.html