個人部落格原文連結:分散式鏈路追蹤(OpenTracing標準)和 Jaeger 實現
OpenTracing 簡介
OpenTracing 是一個中立的(廠商無關、平台無關)分散式追蹤的 API 規範,提供了統一介面方便開發者在自己的服務中整合一種或者多種分散式追蹤的實現。
OpenTracing 誕生的背景
開發和工程團隊因為系統套件水平擴展、開發團隊小型化、敏捷開發、CD(持續整合)、解耦等各種需求,開始使用微服務的架構取代以前好的單機系統。 也就是說,當一個生產系統面對真正的高併發,或者解耦成大量微服務時,以前很容易實現的重點任務變得困難了。過程中需要面臨一系列問題:使用者體驗調校、後台真是錯誤原因分析,分散式系統內各套件的呼叫情況等。隨著服務數量的增多和內部呼叫鏈的複雜化,僅憑藉日誌和效能監控很難做到 「See the Whole Picture」,在進行問題排查或是效能分析的時候,無異於盲人摸象。
分散式追蹤能夠幫助開發者直觀分析請求鏈路,快速定位效能瓶頸,逐漸調校服務間依賴,也有助於開發者從更巨集觀的角度更好地理解整個分散式系統。已有的分散式追蹤系統(例如,Zipkin, Dapper, HTrace, X-Trace等)旨在解決這些問題,但是他們使用不相容的 API 來實現各自的應用需求。儘管這些分散式追蹤系統有著相似的 API 語法,但各種語言的開發人員依然很難將他們各自的系統(使用不同的語言和技術)和特定的分散式追蹤系統進行整合。
在這種情況下,OpenTracing 透過提供平台無關、廠商無關的API,使得開發人員能夠方便的新增(或更換)追蹤系統的實現。OpenTracing 定義了一套通用的資料上報介面,要求各個分散式追蹤系統都來實現這套介面。這樣一來,應用程式只需要對接 OpenTracing,而無需關心後端採用的到底什麼分散式追蹤系統,因此開發者可以無縫切換分散式追蹤系統,也使得在通用程式碼庫增加對分散式追蹤的支援成為可能。
目前,主流的分散式追蹤實現基本上都已經支援 OpenTracing,包括 Jaeger,Zipkin,Appdash 等。
分散式追蹤的相關概念
Tracing
Wikipedia 中,對 Tracing 的定義是,在軟體工程中,Tracing 指使用特定的日誌紀錄程式的執行訊息,與之相近的還有兩個概念,它們分別是 Logging 和 Metrics。
- Logging:用於紀錄離散的事件,包含程式執行到某一點或某一階段的詳細訊息。例如,應用程式的除錯訊息或錯誤訊息。它是我們診斷問題的依據。
- Metrics:可聚合的資料,且通常是固定型別的時序資料。例如,佇列的當前深度可被定義為一個度量值,在元素入隊或出隊時被更新;HTTP 請求個數可被定義為一個計數器,新請求到來時進行累加。
- Tracing:紀錄單個請求的處理流程,其中包括服務呼叫和處理時長等訊息。
這三者也有相交重疊的部門:
- Logging & Metrics:可聚合的事件。例如分析某物件儲存的 Nginx 日誌,統計某段時間內 GET、PUT、DELETE、OPTIONS 操作的總數。
- Metrics & Tracing:單個請求中的可計量資料。例如 SQL 執行總時長、gRPC 呼叫總次數。
- Tracing & Logging:請求階段的標籤資料。例如在 Tracing 的訊息中標記詳細的錯誤原因。
針對每種分析需求,都有非常強大的集中式分析工具。
- Logging:ELK,近幾年勢頭最猛的日誌分析服務。
- Metrics:Prometheus,第二個加入 CNCF 的開源專案,非常好用。
- Tracing:OpenTracing 和 Jaeger,Jaeger 是 Uber 開源的一個相容 OpenTracing 標準的分散式追蹤服務。目前 Jaeger 也加入了 CNCF。
分散式追蹤的核心步驟
分散式追蹤系統大體分為三個部分,資料採集、資料永續化、資料展示。
資料採集是指在程式碼中埋點,設定請求中要上報的階段,以及設定當前紀錄的階段隸屬於哪個上級階段。資料永續化則是指將上報的資料落盤儲存,例如 Jaeger 就支援多種儲存後端,可選用 Cassandra 或者 Elasticsearch。資料展示則是前端根據 Trace ID 搜尋與之關聯的請求階段,並在介面上呈現。
一個典型的 Trace 範例如下:
請求從客戶端發出,請求首先到達負載均衡,接著進行認證服務,計費服務,然後請求資源,最後回傳結果。
當資料被採集儲存之後,分散式追蹤系統會採用包含時間軸的的時序圖來呈現這個 Trace:
OpenTracing 資料模型
Traces
一個 trace 代表一個潛在的,分散式的,存在並行資料或並行執行軌跡(潛在的分散式、並行)的系統。也可以理解成一個呼叫鏈,一個 trace 可以認為是多個 span 的有向無環圖(DAG)。
Spans
一個 span 代表系統中具有開始時間和執行時長的邏輯執行單元,可以理解成某個處理階段,一次方法呼叫,一個程式塊的呼叫,或者一次 RPC/資料庫訪問。只要是一個具有完整時間週期的程式訪問,都可以被認為是一個 span。span 之間透過巢狀或者順序排列建立邏輯因果關係。
每個 Span 包含以下的狀態:
- 操作名稱(An operation name)
- 起始時間(A start timestamp)
- 結束時間(A finish timestamp)
- Span Tag:一組 KV 值,作為階段的標籤集合。鍵值對中,鍵必須為 string,值可以是字串,布林,或者數值型別。
- 階段日誌(Span Logs):一組 span 的日誌集合。 每次 log 操作包含一個鍵值對,以及一個時間戳。 鍵值對中,鍵必須為 string,值可以是任意型別。 但是需要注意,不是所有的支援 OpenTracing 的 Tracer,都需要支援所有的實值型別。
- 階段上下文(SpanContext),其中包含 TraceID 和 SpanID
- 引用關係(Reference):Span 和 Span 之間的關係被命名 Reference,Span 之間透過 SpanContext 建立這種關係。
Span Reference
一個 span 可以和一個或者多個 span 間存在因果關係。OpenTracing 定義了兩種關係:
- 一個 RPC 呼叫的伺服端的 span,和 RPC 服務客戶端的 span 構成 ChildOf 關係
- 一個 sql insert 操作的 span,和 ORM 的 save 方法的 span 構成 ChildOf 關係
- 很多 span 可以並行工作(或者分散式工作)都可能是一個父級的 span 的子項,他會合併所有子span的執行結果,並在指定期限內回傳
一個具有
1 2 3 4 5 6 7 8 9 | [-Parent Span---------] [-Child Span----] [-Parent Span--------------] [-Child Span A----] [-Child Span B----] [-Child Span C----] [-Child Span D---------------] [-Child Span E----] |
一個具有
1 2 3 4 5 6 7 8 9 | [-Parent Span-] [-Child Span-] [-Parent Span--] [-Child Span-] [-Parent Span-] [-Child Span-] |
綜上,在一個 tracer 過程中,各 span 可以有如下關係:
1 2 3 4 5 6 7 8 9 10 11 12 13 | [Span A] ←←←(the root span) | +------+------+ | | [Span B] [Span C] ←←←(Span C 是 Span A 的孩子節點, ChildOf) | | [Span D] +---+-------+ | | [Span E] [Span F] >>> [Span G] >>> [Span H] ↑ ↑ ↑ (Span G 在 Span F 後被呼叫, FollowsFrom) |
上述 tracer 與 span 的時間軸關係如下:
1 2 3 4 5 6 7 | ––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–> time [Span A···················································] [Span B··············································] [Span D··········································] [Span C········································] [Span E·······] [Span F··] [Span G··] [Span H··] |
SpanContext
每個 span 必須提供方法訪問 SpanContext。SpanContext 代表跨越行程邊界,傳遞到下級 span 的狀態。(例如,包含
每一個 SpanContext 包含以下特點:
- 任何一個 OpenTracing 的實現,都需要將當前呼叫鏈的狀態(例如:trace 和 span 的 id),依賴一個獨特的 Span 去跨行程邊界傳輸。
- Baggage Items,Trace 的隨行資料,是一個鍵值對集合,它存在於 trace 中,也需要跨行程邊界傳輸
Inject 和 Extract
SpanContexts 可以透過 Injected 操作向載體( Carrier)增加,或者透過 Extracted 從 Carrier 中取得,跨行程通訊資料(例如:將 HTTP 頭作為 Carrier 攜帶 SpanContexts)。透過這種方式,SpanContexts 可以跨越行程邊界,並提供足夠的訊息來建立跨行程的 span 間關係(因此可以實現跨行程連續追蹤)。
Baggage
**Baggage **是儲存在 SpanContext 中的一個鍵值對( SpanContext )集合。它會在一條追蹤鏈路上的所有 span 當中傳輸,包含這些 span 對應的 SpanContexts。在這種情況下,「Baggage」 會隨著 trace 一同傳播,他因此得名(Baggage 可理解為隨著 trace 執行過程傳送的行李)。鑒於全棧 OpenTracing 整合的需要,Baggage 透過透明化的傳輸任意應用程式的資料,實現強大的功能。例如:可以在最終使用者的行動電話端新增一個 Baggage 元素,並透過分散式追蹤系統傳遞到儲存層,然後再透過反向構建呼叫棧,定位過程中耗用很大的 SQL 搜尋敘述。
Baggage 擁有強大功能,也會有很大的耗用。由於 Baggage 的全域傳輸,如果包含的數量量太大,或者元素太多,它將降低系統的吞吐量或增加 RPC 的延遲。
Baggage 與 Span Tags 的區別
- Baggage 在全域範圍內,伴隨業務系統的呼叫跨進城傳輸資料。而 Span 的 tag 不會進行傳輸,因為它們不會被子級的 span 繼承。
- span 的 tag 可以用來紀錄業務相關的資料,並儲存與追蹤系統中。而在實現 OpenTracing 時,可以選擇是否儲存 Baggage 中的非業務資料,OpenTracing 標準不強制要求實現此屬性。
OpenTracing 定義的 API
各平台 API 支援
目前來說,下面這些平台都支援了 OpenTracing 規範定義的 API:
- Go - https://github.com/opentracing/opentracing-go
- Python - https://github.com/opentracing/opentracing-python
- Javascript - https://github.com/opentracing/opentracing-javascript
- Objective-C - https://github.com/opentracing/opentracing-objc
- Java - https://github.com/opentracing/opentracing-java
- C++ - https://github.com/opentracing/opentracing-cpp
PHP 和 Ruby 的 API 目前也正在研發當中。
相關 API
OpenTracing 標準中有三個重要的相互關聯的型別,分別是
當討論「可選」引數時,需要強調的是,不同的語言針對可選引數有不同理解,概念和實現方式 。例如,在Go中,習慣使用 」functional Options」,而在 Java 中,可能使用 builder 模式。
Tracer
建立一個新 Span
必填引數
- operation name, 操作名, 一個具有可讀性的字串,代表這個span所做的工作(例如:RPC 方法名,方法名,或者一個大型計算中的某個階段或子任務)。操作名應該是一個抽象、通用,明確、具有統計意義的名稱。因此,
"get_user" 作為操作名,比"get_user/314159" 更好。
例如,假設一個取得賬戶訊息的span會有如下可能的名稱:
操作名 | 指導意見 |
---|---|
太抽象 | |
太明確 | |
正確的操作名,關於 |
可選引數
- 零個或者多個關聯(references)的
SpanContext ,如果可能,同時快速指定關係型別,ChildOf 還是FollowsFrom 。 - 一個可選的顯性傳遞的開始時間;如果忽略,當前時間被用作開始時間。
- 零個或者多個tag。
回傳值,回傳一個已經啟動
將 SpanContext 上下文 Inject(注入)到 carrier
必填引數:
SpanContext 例項- format(格式化)描述,一般會是一個字串常數,但不做強制要求。透過此描述,通知
Tracer ,如何對SpanContext 進行編碼放入到 carrier 中。 - carrier,根據 format 確定。
Tracer 根據 format 宣告的格式,將SpanContext 序列化到 carrier 物件中。
將 SpanContext 上下文從 carrier 中 Extract(選取)
必填引數
- format(格式化)描述,一般會是一個字串常數,但不做強制要求。透過此描述,通知
Tracer ,如何從 carrier 中解碼SpanContext 。 - carrier,根據format確定。
Tracer 根據 format 宣告的格式,從 carrier 中解碼SpanContext 。
回傳值,回傳一個
注意,對於Inject(注入)和Extract(選取),format 是必須的。
Inject(注入)和 Extract(選取)依賴於可擴展的 format 引數。format 引數規定了另一個引數 」carrier」 的型別,同時約束了 」carrier」 中
- Text Map: 基於字串:字串的 map,對於 key 和 value,不約束編碼表。
- HTTP Headers: 適合作為 HTTP 頭訊息的,基於字串:字串的map。(RFC 7230.在工程實踐中,如何處理 HTTP 頭具有多樣性,強烈建議 trace r的使用者謹慎使用 HTTP 頭的鍵值空間和轉義符)
- Binary: 一個簡單的二進位制大物件,紀錄
SpanContext 的訊息。
Span
取得 Span 的 SpanContext
不需要任何引數。
回傳值,
複寫操作名(operation name)
必填引數
- 新的操作名 operation name,覆蓋構建
Span 時,傳入的操作名。
結束Span
可選引數
- 一個明確的完成時間;如果省略此引數,使用當前時間作為完成時間。當
Span 結束後(span.finish() ),除了透過Span 取得SpanContext 外,他所有方法都不允許被呼叫。
為Span 設定tag
必填引數
- tag key,必須是 string 型別
- tag value,型別為字串,布林或者數值
Log結構化資料
必填引數
- 一個或者多個鍵值對,其中鍵必須是字串型別,值可以是任意型別。某些 OpenTracing 實現,可能支援更多的log實值型別。
可選引數
- 一個明確的時間戳。如果指定時間戳,那麼它必須在 span 的開始和結束時間之內。
設定一個baggage(隨行資料)元素
Baggage 元素是一個鍵值對集合,將這些值設定給給定的
帶內傳遞,在這裡指隨應用程式呼叫過程一起傳遞
Baggage 元素具有強大的功能,使得 OpenTracing 能夠實現全棧整合(例如:任意的應用程式資料,可以在行動裝置建立它,顯然的,它會一直傳遞了系統最底層的儲存系統),同時他也會產生巨大的開銷,每一個鍵值都會被拷貝到每一個本地和遠端的下級相關的 span 中,因此,總體上,他會有明顯的網路和CPU開銷。
必填引數
- baggage key, 字串型別
- baggage value, 字串型別
取得一個 baggage 元素
必填引數
- baggage key, 字串型別
回傳值,相應的 baggage value ,或者可以標識元素值不存在的回傳值。
SpanContext
相對於 OpenTracing 中其他的功能,
OpenTracing 要求,
遍歷所有的 baggage 元素
遍歷模型依賴於語言,實現方式可能不一致。在語意上,要求呼叫者可以透過給定的
NoopTracer
所有的 OpenTracing API 實現,必須提供某種方式的
可選 API 元素
有些語言的 OpenTracing 實現,為了在串列處理中,傳遞活躍的
OpenTracing 入門實踐
這裡以官方一個簡單 demo 紀錄如何 OpenTracing:
server.go
在 server.go 當中,首先定義了幾個呼叫點 handler,這些呼叫點共同組成了一個 server。
接著,為了監控這個程式,在入口處(HomeHandler)中設定了一個 span,這個 span 紀錄了 HomeHandler 方法完成所需要的時間,同時透過判斷 homeHandler 方法是否正確回傳,決定是否透過 tags 和 logs 紀錄方法呼叫的錯誤訊息。
另一方面,為了構建真正的端到端追蹤,還需要包含呼叫HTTP請求的客戶端的span訊息。所以需要在端到端過程中傳遞 span 的上下文訊息,使得各端中的 span 可以合併到一個追蹤過程中。這就是API中 Inject/Extract 的職責。homeHandler方法在第一次被呼叫時,建立一個根span,將關於本地追蹤呼叫的 span 的元訊息,設定到 http 的頭上,並傳遞出去。
在 ServiceHandler 中,透過 HTTP標頭取得到前面注入的元資料,並根據取得情況,還可以指定 span 之間的關係。
具體的程式碼例項如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 | package server import ( "fmt" "github.com/opentracing/opentracing-go" "log" "math/rand" "net/http" "time" ) func IndexHandler(w http.ResponseWriter, r *http.Request) {<!-- --> _, _ = w.Write([]byte(`<a href = "/home"> Click here to start a request </a>`)) } // HomeHandler "/home" 路徑下 func HomeHandler(w http.ResponseWriter, r *http.Request) {<!-- --> _, _ = w.Write([]byte("Request started ")) // 在入口處設定一個 span span := opentracing.StartSpan("GET /home") defer span.Finish() // 建立請求 asyncReq, _ := http.NewRequest("GET", "http://localhost:8888/async", nil) // 將關於本地追蹤呼叫的 span 的元訊息,設定到 http 的頭上,並準備傳遞出去 err := span.Tracer().Inject(span.Context(), opentracing.TextMap, opentracing.HTTPHeadersCarrier(asyncReq.Header)) if err != nil {<!-- --> log.Fatalf("%s: Could not inject span context into async request header: %v", r.URL.Path, err) } // 為 span 設定 tags 和 logs go func() {<!-- --> sleepMilli(50) // 透過判斷 homeHandler 方法是否正確回傳,決定是否紀錄方法呼叫的錯誤訊息 if _, err = http.DefaultClient.Do(asyncReq); err != nil {<!-- --> // 方法呼叫出錯,為期設定 tag 和 log span.SetTag("error", true) span.LogKV(fmt.Sprintf("%s: Async call failed (%v)", r.URL.Path, err)) } }() sleepMilli(10) syncReq, _ := http.NewRequest("GET", "http://localhost:8888/service", nil) err = span.Tracer().Inject(span.Context(), opentracing.TextMap, opentracing.HTTPHeadersCarrier(syncReq.Header)) if err != nil {<!-- --> log.Fatalf("%s: Could not inject span context into service request header: %v", r.URL.Path, err) } if _, err = http.DefaultClient.Do(syncReq); err != nil {<!-- --> span.SetTag("error", true) span.LogKV(fmt.Sprintf("%s: GET /service error: %v", r.URL.Path, err)) } _, _ = w.Write([]byte("Request done! ")) } // ServiceHandler "/service" 路徑下 func ServiceHandler(w http.ResponseWriter, r *http.Request) {<!-- --> // 在 ServiceHandler 服務中選取上面的元資料訊息 var sp opentracing.Span opName := fmt.Sprintf("%s %s", r.Method, r.URL.Path) // 嘗試透過請求標頭取得 span 的上下文訊息 wireContext, err := opentracing.GlobalTracer().Extract( opentracing.TextMap, opentracing.HTTPHeadersCarrier(r.Header)) if err != nil {<!-- --> // 如果由於某種原因導致無法取得訊息,則繼續啟動一個新的根路徑下的 span sp = opentracing.StartSpan(opName) log.Printf("err: %v for the wireContext: %v", err, wireContext) } else {<!-- --> // 沒有出錯則可以指定 span 之間的關係 sp = opentracing.StartSpan(opName, opentracing.ChildOf(wireContext)) log.Printf("the wireContext: %v", wireContext) } defer sp.Finish() sleepMilli(50) dbReq, _ := http.NewRequest("GET", "http://localhost:8888/db", nil) err = sp.Tracer().Inject(sp.Context(), opentracing.TextMap, opentracing.HTTPHeadersCarrier(dbReq.Header)) if err != nil {<!-- --> log.Fatalf("%s: Couldn't inject headers (%v)", r.URL.Path, err) } if _, err = http.DefaultClient.Do(dbReq); err != nil {<!-- --> sp.LogKV("da request error", err) } } // DbHandler "/db"路徑下 func DbHandler(w http.ResponseWriter, r *http.Request) {<!-- --> var sp opentracing.Span spanCtx, err := opentracing.GlobalTracer().Extract(opentracing.TextMap, opentracing.HTTPHeadersCarrier(r.Header)) if err != nil {<!-- --> sp = opentracing.StartSpan("GET /db") log.Printf("%s: Coule not join trace (%v) ", r.URL.Path, err) return } else {<!-- --> sp = opentracing.StartSpan("GET /db", opentracing.ChildOf(spanCtx)) } defer sp.Finish() sleepMilli(25) } func sleepMilli(min int) {<!-- --> time.Sleep(time.Millisecond * time.Duration(min+rand.Intn(100))) } |
連接到追蹤系統
當系統按照 OpenTracing 標準被監控之後,增加一個追蹤系統便變得非常簡單,只需要在啟動之前,指定所連接的鏈路追蹤系統即可,下面使用 appdash 追蹤系統,透過在 main 函式中新增一小段程式碼來啟動 Appdash 例項,不需要修改任何監控程式碼就可以實現追蹤:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | package main import ( "flag" "fmt" "github.com/opentracing/opentracing-go" "log" "net" "net/http" "net/url" "opentracing-demo/server" "sourcegraph.com/sourcegraph/appdash" appdashot "sourcegraph.com/sourcegraph/appdash/opentracing" "sourcegraph.com/sourcegraph/appdash/traceapp" ) var ( port = flag.Int("port", 8888, "Example app port.") appdashPort = flag.Int("appdash.port", 8700, "Run appdash locally on this port.") ) func main() {<!-- --> flag.Parse() // 連接到追蹤系統(Appdash) addr := startAppdashServer(*appdashPort) tracer := appdashot.NewTracer(appdash.NewRemoteCollector(addr)) opentracing.InitGlobalTracer(tracer) addr = fmt.Sprintf(":%d", *port) mux := http.NewServeMux() mux.HandleFunc("/", server.IndexHandler) mux.HandleFunc("/home", server.HomeHandler) mux.HandleFunc("/async", server.ServiceHandler) mux.HandleFunc("/service", server.ServiceHandler) mux.HandleFunc("/db", server.DbHandler) fmt.Printf("Go to http://localhost:%d/home to start arequest! ", *port) log.Fatal(http.ListenAndServe(addr, mux)) } // startAppdashServer 連接到 Appdash 鏈路追蹤系統 func startAppdashServer(appdashPort int) (collectorPortStr string) {<!-- --> store := appdash.NewMemoryStore() // 在本地偵聽任何可用的 TCP 連接埠 l, err := net.ListenTCP("tcp", &net.TCPAddr{<!-- -->IP: net.IPv4(127, 0, 0, 1), Port: 0}) if err != nil {<!-- --> log.Fatal(err) } collectorPort := l.Addr().(*net.TCPAddr).Port collectorPortStr = fmt.Sprintf(":%d", collectorPort) // 啟動一個 Appdash 收集伺服器,該伺服器將偵聽 span 和註解並將其新增到本地收集器(儲存在記憶體中) cs := appdash.NewServer(l, appdash.NewLocalCollector(store)) go cs.Start() // 列印將執行 web 介面的 URL appdashURLStr := fmt.Sprintf("http://localhost:%d", appdashPort) appdashURL, err := url.Parse(appdashURLStr) if err != nil {<!-- --> log.Fatalf("Error parsing %s: %s", appdashURLStr, err) } fmt.Printf("To see your traces,go to %s/traces ", appdashURL) // 在單獨的 goroutine 中啟動 web UI 介面 tapp, err := traceapp.New(nil, appdashURL) if err != nil {<!-- --> log.Fatalf("Error creating traceapp: %v", err) } tapp.Store = store tapp.Queryer = store go func() {<!-- --> log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", appdashPort), tapp)) }() return } |
執行之後,就可以透過 UI 介面觀察到本地程式的請求鏈路,耗時情況等:
OpenTracing 最佳實踐
針對準備使用 OpenTracing API 的實踐
追蹤 Function(函式)
假設現在最頂層的函式如下:
1 2 3 4 5 6 | def top_level_function(): span1 = tracer.start_span('top_level_function') try: . . . # business logic,業務邏輯 finally: span1.finish() |
在後面的流程中,作為業務邏輯的一部分,我們呼叫的
現在,假設一個
1 2 3 4 5 6 7 8 | def function2(): span2 = get_current_span().start_child('function2') if get_current_span() else None try: . . . # business logic finally: if span2: span2.finish() |
假設如果這個追蹤還未被啟動,無論什麼原因,開發者都不想在這個函式內啟動一個新的追蹤,所以
伺服端追蹤
當一個應用伺服器要追蹤一個請求的執行情況,一般需要以下幾步:
- 試圖從請求中取得傳輸過來的 SpanContext(防止呼叫鏈在客戶端已經開啟),如果無法取得 SpanContext,則新開啟一個追蹤。
- 在
request context 中儲存最新建立的 span,request context 會透過應用程式碼或者 RPC 框架進行傳輸 - 最終,當伺服端完成請求處理後,使用
span.finish() 關閉 span。
從請求中取得(Extracting)SpanContext
假設,有一個HTTP伺服器,SpanContext 透過 HTTP 頭從客戶端傳遞到伺服端,可透過
1 2 3 4 | extracted_context = tracer.extract( format=opentracing.HTTP_HEADER_FORMAT, carrier=request.headers ) |
這裡,使用
從請求中取得一個已經存在的追蹤,或者開啟一個新的追蹤
如果無法在請求的相關的頭訊息中取得所需的值,上文中的
在這種情況下,伺服端需要新建立一個追蹤(新呼叫鏈)。
1 2 3 4 5 6 7 8 9 10 | extracted_context = tracer.extract( format=opentracing.HTTP_HEADER_FORMAT, carrier=request.headers ) if extracted_context is None: span = tracer.start_span(operation_name=operation) else: span = tracer.start_span(operation_name=operation, child_of=extracted_context) span.set_tag('http.method', request.method) span.set_tag('http.url', request.full_url) |
可以透過呼叫
上面提到的
行程內請求上下文傳輸
請求的上下文傳輸是指,對於一個請求,所有處理這個請求的層都需要可以訪問到同一個
下面有兩種常用的上下文傳輸技術:
隱式傳輸
隱式傳輸技術要求
這種方式的缺點在於,有明顯的效能損耗,有些平台例如 Go 不知道基於 thread-local 的儲存,隱式傳輸將幾乎不可能實現。
顯示傳輸
顯示傳輸技術要求應用程式碼,包裝並傳遞
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | func HandleHttp(w http.ResponseWriter, req *http.Request) {<!-- --> ctx := context.Background() ... BusinessFunction1(ctx, arg1, ...) } func BusinessFunction1(ctx context.Context, arg1...) {<!-- --> ... BusinessFunction2(ctx, arg1, ...) } func BusinessFunction2(ctx context.Context, arg1...) {<!-- --> parentSpan := opentracing.SpanFromContext(ctx) childSpan := opentracing.StartSpan( "...", opentracing.ChildOf(parentSpan.Context()), ...) ... } |
顯示傳輸的缺點在於,它嚮應用程式碼,暴露了底層的實現。
追蹤客戶端呼叫
當一個應用程式作為一個 RPC 客戶端時,它可能希望在發起呼叫之前,啟動一個新的追蹤的 span,並將這個新的 span 隨請求一起傳輸。下面,透過一個HTTP請求的例項,展現如何做到這點。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | def traced_request(request, operation, http_client): # retrieve current span from propagated request context parent_span = get_current_span() # start a new span to represent the RPC span = tracer.start_span( operation_name=operation, child_of=parent_span.context, tags={<!-- -->'http.url': request.full_url} ) # propagate the Span via HTTP request headers tracer.inject( span.context, format=opentracing.HTTP_HEADER_FORMAT, carrier=request.headers) # define a callback where we can finish the span def on_done(future): if future.exception(): span.log(event='rpc exception', payload=exception) span.set_tag('http.status_code', future.result().status_code) span.finish() try: future = http_client.execute(request) future.add_done_callback(on_done) return future except Exception e: span.log(event='general exception', payload=e) span.finish() raise |
get_current_span() 函式不是 OpenTracing API 的一部分。它僅僅代表一個工具類的方法,透過當前的請求上下文取得當前的span。(在Python一般會這樣用)。- 假定 HTTP 請求是非同步的,所以他會回傳一個 Future。我們為這次呼叫增加的成功回呼函式,在回呼函式內部完成當前的 span。
- 如果 HTTP 客戶端回傳一個異常,則透過 log 方法將異常紀錄到 span 中。
- 因為 HTTP 請求可以在回傳 Future 後發生異常,則使用 try/catch 塊,在任何情況下都會完成 span,保證這個 span 會被上報,並避免記憶體溢位。
使用 Baggage / 分散式上下文傳輸
上面透過網路在客戶端和伺服端間傳輸的 Span 和 Trace,包含了任意的 Baggage。客戶端可以使用 Baggage 將一些額外的資料傳遞到伺服端,以及這個伺服端的下游其他伺服器。
1 2 3 4 5 | # client side span.context.set_baggage_item('auth-token', '.....') # server side (one or more levels down from the client) token = span.context.get_baggage_item('auth-token') |
Logging 事件
在客戶端 span 的范常式式碼中,已經使用過
1 2 | span = get_current_span() span.log(event='cache-miss') |
tracer 會為事件自動增加一個時間戳,這點和 Span 的 tag 操作時不同的。也可以將外部的時間戳和事件相關聯。
使用外部的時間戳,紀錄Span
因為多種多樣的原因,有些場景下,會將 OpenTracing 相容的 tracer 整合到一個服務中。例如,一個使用者有一個日誌檔案,其中包含大量的來自黑盒行程(如:HAProxy)產生的 span。為了讓這些資料接入 OpenTracing 相容的系統,API 需要提供一種方法透過外部的時間戳紀錄 span 的訊息。
1 2 3 4 5 6 7 8 9 | explicit_span = tracer.start_span( operation_name=external_format.operation, start_time=external_format.start, tags=external_format.tags ) explicit_span.finish( finish_time=external_format.finish, bulk_logs=map(..., external_format.logs) ) |
在追蹤開始之前,設定採樣優先順序
很多分散式追蹤系統,透過採樣來降低追蹤資料的數量。有時,開發者想有一種方式,確保這 trace 一定會被紀錄(採樣),例如:HTTP 請求中包含特定的引數,如
1 2 3 4 5 | if request.get('debug'): span = tracer.start_span( operation_name=operation, tags={<!-- -->tags.SAMPLING_PRIORITY: 1} ) |
針對準備將 OpenTracing 整合到 web,RPC 或者其他框架的實踐
總體來說,整合 OpenTracing 需要做兩件事情:
伺服端框架修改需求:
- 過濾器、攔截器、中介軟體或其他處理輸入請求的套件
- span 的儲存,儲存一個 request context 或者 request 到 span 的對映表
- 透過某種方式對 tracer 進行配置
客戶端框架修改需求:
- 過濾器、攔截器、中介軟體或其他處理對外呼叫的請求的套件
- 透過某種方式對 tracer 進行配置
伺服端追蹤
伺服端追蹤的目的是追蹤請求在這個伺服器內部的全生命週期的情況,並保證能夠和前置的客戶端追蹤訊息連接起來。可以在伺服器收到請求時,建立 span,並在伺服器完成請求處理後,關閉這些 span。追蹤一個伺服端請求的流程如下:
- 伺服器接收到請求
- 從網路請求(跨行程的呼叫:HTTP等)取得當前的追蹤鏈狀態
- 建立一個新的 span
- 儲存當前的追蹤狀態
- 伺服器完成請求處理 / 回傳回應
- 結束上面建立的 span
由於呼叫流程決定於請求的處理情況,所以需要知道如果修改框架的請求和回應處理——是否需要透過修改過濾器、中介軟體、配置棧或者其他機制。
取得當前的追蹤鏈的狀態
為了在分散式系統中,跨行程邊界追蹤呼叫情況,RPC 服務需要能夠銜接每一個服務請求的伺服端和客戶端。OpenTracing 允許透過 inject 和 extract 方法,將 span 的上下文訊息編碼到 carrier 中。
如果客戶端發起一個請求時,span 的上下文就已經被加到了請求內容中。需要做的工作是使用 io.opentracing.Tracer.extract 方法,從請求中取得 span 的上下文。carrier 判斷使用哪種服務,決定使用哪種方法從請求中取得上下文;例如,web 服務透過 HTTP 頭作為 carrier,從 HTTP 請求中獲 span 上下文(如下所示):
Python:
1 | span_ctx = tracer.extract(opentracing.Format.HTTP_HEADERS, request.headers) |
Java:
1 2 3 4 5 6 | import io.opentracing.propagation.Format; import io.opentracing.propagation.TextMap; Map<String, String> headers = request.getHeaders(); SpanContext parentSpan = tracer.getTracer().extract(Format.Builtin.HTTP_HEADERS, new TextMapExtractAdapter(headers)); |
OpenTracing 當選取失敗時,可以選擇丟擲異常,所以確保會捕獲異常,防止異常造成伺服器宕機。這種情況通常意味著請求來自於第三方應用(沒有被追蹤的應用),此時應該開啟一個新的追蹤。
儲存當前的span上下文
在處理請求期間,讓使用者可以訪問 span 上下文是十分重要的。衹有取得上下文,才能為伺服端,進行自定義的 tag 設定,紀錄事件(log event),建立子級的 span,用於最終展現服務內部的工作情況。為了滿足這個目標,必須決定如何讓使用者訪問當前的 span。這將由框架的架構決定。這裡有兩個常見用例:
使用請求上下文
如果使用的框架有一個請求上下文,上下文可以儲存任意值,這樣可以在請求處理過程中,一直把現在的 span 儲存到上下文中。如果框架中有過濾器(Filter),這種實現方式是一種很好的方式。例如有一個請求上下文叫做 ctx,那麼可以這樣實現一個過濾器(Filter):
1 2 3 4 5 | def filter(request): span = # extract / start span from request with (ctx.active_span = span): process_request(request) span.finish() |
現在,在請求處理的任何時候,使用者都可以透過
建立請求和 span 的對映關係
如果存在這種情況:如有可能沒有一個可用的請求上下文,或者針對請求的預處理和後處理有不同的過濾器方法, 可以選擇建立一個請求和 span 的對映表。其中一種實現方式是建立一個框架特有的 tracer 的包裝器(tracer wrapper),儲存這個對映表,例如:
1 2 3 4 5 6 7 8 9 10 11 12 | class MyFrameworkTracer: def __init__(opentracing_tracer): self.internal_tracer = opentracing_tracer self.active_spans = {<!-- -->} def add_span(request, span): self.active_spans[request] = span def get_span(request): return self.active_spans[request] def finish_span(request): span = self.active_spans[request] span.finish() del self.active_spans[request] |
- 如果伺服器可以並行的處理請求,需要確保 span 的對映表是執行緒安全的。
- 過濾器處理范常式式碼如下:
1 2 3 4 5 | def process_request(request): span = # extract / start span from request tracer.add_span(request, span) def process_response(request, response): tracer.finish_span(request) |
注意:使用者在處理 reponse 時,呼叫
客戶端追蹤
當框架有一個客戶端套件的時候,需要在初始化 request 的時候,開啟客戶端的追蹤。這樣做是為了將生成的 span 放到請求標頭中,這樣 span 才能請求隨著請求,傳遞到伺服端。
類似於伺服端追蹤,需要知道如何修改客戶端程式碼,來發送請求,和接收相應。當客戶端完成修改,就可以完成端到端的追蹤了。
追蹤一個客戶端請求的流程如下:
- 準備請求物件
- 讀取現在的追蹤狀態
- 新建一個 span
- 將 span 注入(Inject)到請求中
- 發送請求
- 接收回應
- 完成並關閉 span
讀取現在的追蹤狀態 / 新建一個span
正如伺服端一樣,必須知道是應該開啟一個新的追蹤或者和一個已有的追蹤連接上。例如,一個基於微服務架構分散式架構中,一個應用可能即是伺服端又是客戶端。一個服務的提供方同時又是另一個服務的發起方,這個東西需要被聯繫起來。如果存在一個活躍的呼叫鏈,需要將它的活躍 span 作為父級 span,並在客戶端請求出開啟一個新的 span。否則,需要新建沒有沒有父級節點的 span。
如何判斷是否存在一個活躍的追蹤,取決於如何儲存的活躍的 span。如果使用一個請求上下文,你可以這樣處理:
1 2 3 4 5 6 | if hasattr(ctx, active_span): parent_span = getattr(ctx, active_span) span = tracer.start_span(operation_name=operation_name, child_of=parent_span) else: span = tracer.start_span(operation_name=operation_name) |
如果使用 request 到 span 的對映機制,可以這樣處理:
1 2 3 4 | parent_span = tracer.get_span(request) span = tracer.start_span( operation_name=operation_name, child_of=parent_span) |
注入(Inject) Span
注入 span 的時候,會把當前追蹤的上下文訊息放到客戶端的請求中,這樣當呼叫發生時,追蹤可以在伺服端被還原,並繼續進行。如果是使用HTTP請求,可以使用HTTP標頭作為上下文資料的carrier(載體)。
1 | span = # 從請求標頭中取得當前的追蹤狀態 `tracer.inject(span, opentracing.Format.HTTP_HEADERS, request.headers)` |
完成並關閉span
當收到相應後,完成並關閉 span,標誌著客戶端呼叫結束。和伺服端一樣,如果完成這個操作取決於在客戶端如何處理請求和回應。如果存在過濾器(filter),可以這樣處理:
1 2 3 4 5 6 7 | def filter(request, response): span = # start span from the current trace state tracer.inject(span, opentracing.Format.HTTP_HEADERS, request.headers) response = send_request(request) if response.error: span.set_tag(opentracing., true) span.finish() |
否則,如果請求和相應是分開處理的,可能需要擴展 tracer,包含請求和 span 的對映關係。參考實現如下:
1 2 3 4 5 6 | def process_request(request): span = # start span from the current trace state tracer.inject(span. opentracing.Format.HTTP_HEADERS, request.headers) tracer.add_client_span(request, span) def process_response(request, response): tracer.finish_client_span(request) |
追蹤大規模分散式系統的實踐
Spans 和它們之間的關係
實現 OpenTracing 完成分散式追蹤的兩個基本概念就是
-
Spans 是系統中的一個邏輯工作單元,包含這個工作單元啟動時間和執行時間。在一條追蹤鏈路中,各個 span 與系統中的不同套件有關,並體現這些套件的執行路徑。
-
Relationships 是 span 間的連接關係。一個span可以和 0-n 個套件存在因果關係。這種關係是的各個 span 被串接起來,並用來幫助定位追蹤鏈路的關鍵路徑。
專註高價值區域
從 RPC 層和 web 框架開始構建追蹤,是一個好方法。這兩部分將包含交易路徑中的大部分內容。
下一步,應該著手在沒有被服務框架覆蓋的交易路徑上。為足夠多的套件增加監控,為高價值的交易建立一條關鍵鏈路的追蹤軌跡。
監控的首要目標,是基於關鍵路徑上的 span,尋找最耗時的操作,為可量化的調校操作提供最重要的資料支援。例如,對於只佔用交易時間 1% 的操作(一個大粒度的 span)增加更細粒度的監控,對於理解端到端的延遲(效能問題)不會有太大意義。
先走再跑,逐步提高
如果你正在構建你的跨應用追蹤系統實現,使用這套系統建立高價值的關鍵交易與平衡關鍵交易和程式碼覆蓋率的概念。最大的價值,在於為關鍵交易生成端到端的追蹤。視覺化展現追蹤結果是非常重要的。它可能幫助你確定那塊區域(程式碼塊/系統模組)需要更細粒度的追蹤。
一旦有了端到端的監控,很容易評估在哪些區域增加投入,進行更細粒度的追蹤,並能確定事情的優先順序。如果開始深入處理監控問題,可以考慮哪些部分能夠復用。透過這些復用建立一套可以在多個服務間服用的監控類別庫。
這種方法可以提供廣泛的覆蓋(如:RPC,web 框架等),也能為關鍵業務的交易增加高價值的埋點。即使有些埋點(生成 span)的程式碼是一次性工作,也能透過這種模式發現未來工作的優先順序,調校工作效率。
範例例項
下面的範例讓上述的概念更具體一些:
在這個範例中,我們想追蹤一個,由行動電話端發起,呼叫了多個服務的呼叫鏈。
-
首先,必須說明這個交易的大體情況。在範例中,交易如下所示:
一個客戶透過行動電話客戶端向 web 發起了一個HTTP請求,產生一個複雜的呼叫流程:mobile client (HTTP) → web tier (RPC) → auth service (RPC) → billing service (RPC) → resource request (API) → response to web tier (API) → response to client (HTTP)
-
現在,對交易的大概情況了解了之後,需要去監控一些通用的協定和框架。最好的選擇是從 RPC 服務框架開始,這將是收集 web 請求背後發生的呼叫情況的最好方式。(或者說,任何在分散式過程中發生的問題,都會在直接體現在 RPC 服務中)
-
下一個重點監控的套件應該是 web 框架。透過增加 web 框架的監控,能夠得到一個端到端的追蹤鏈路。雖然這點追蹤鏈路有點粗,但是至少,追蹤系統取得到了完整的呼叫棧。
-
透過上面的工作,可以看到所需的呼叫鏈,並評估我們細化哪一塊的追蹤。在範例中可以看到,請求中最耗時的操作時取得資源的操作。所以,應該細化這塊的監控粒度,監控資源定位內部的套件。一旦完成資源請求的監控,可以看到資源請求被分解成下圖所示的情況:
*resource request (API) → container startup (API) → storage allocation (API) → startup scripts (API) → resource ready response (API)*
-
一旦完成資源套件的追蹤,可以看到大量的時間耗用在提供上,下一步,深入分析,如果可能,調校資源取得程式,使用並行處理替代串列處理。
-
現在有了一條基於端到端呼叫流程的視覺化展現以及基線,可以為這個服務建立明確的 SLO。另外,為內部服務建立 SLO,可以成為對服務正常和錯誤執行的時間的討論的基礎。
-
下一次迭代,回到最頂層的追蹤,去尋找下一個長耗時的任務,但是沒有明細展現,這時需要更細粒度的追蹤。如果展現的粒度已經足夠,可以進行下一個關鍵交易的追蹤和調校處理了。
-
重複上述步驟。
Jaeger 鏈路追蹤系統
Jaeger 是 Uber 開源的分散式追蹤系統,相容 OpenTracing 標準。其功能包括
- 分散式上下文傳播
- 分散式交易監控
- 根本原因分析
- 服務依賴性分析
- 效能/延遲調校
Jaeger 的系統架構
Jaeger 的架構圖如下:
Jaeger 主要包括以下這些套件:(每一個套件都支援單獨建置)
- jaeger-client:Jaeger 的客戶端,實現了 OpenTracing 的 API,支援主流程式設計語言。客戶端直接整合在目標 Application 中,其作用是紀錄和發送 Span 到 Jaeger Agent。在 Application 中呼叫 Jaeger Client Library 紀錄 Span 的過程通常被稱為埋點。
- jaeger-agent:暫存 Jaeger Client 發來的 Span,並批量向 Jaeger Collector 發送 Span,一般每台機器上都會建置一個 Jaeger Agent。官方的介紹中還強調了 Jaeger Agent 可以將服務發現的功能從 Client 中抽離出來,不過從架構角度講,如果是建置在 Kubernetes 或者是 Nomad 中,Jaeger Agent 存在的意義並不大。
- jaeger-collector:接受 Jaeger Agent 發來的資料,並將其寫入儲存後端,目前支援採用 Cassandra 和 Elasticsearch 作為儲存後端。比較推薦用 Elasticsearch,既可以和日誌服務共用同一個 ES,又可以使用 Kibana 對 Trace 資料進行額外的分析。架構圖中的儲存後端是 Cassandra,旁邊還有一個 Spark,講的就是可以用 Spark 等其他工具對儲存後端中的 Span 進行直接分析。
- jaeger-query & jaeger-ui:讀取儲存後端中的資料,以直觀的形式呈現。
jaeger-client 庫
Jaeger客戶端是 OpenTracing API 的特定於語言的實現。 它們可用於手動或透過與 OpenTracing 整合的各種現有開源框架(例如 Flask,Dropwizard,gRPC 等)來檢測應用程式以進行分散式追蹤。
接收請求的服務會在接收到新請求時建立 spans,並將上下文訊息(spans,id,span id 和 baggage) 附加到傳出的請求。 衹有各種 id 和 baggage 隨請求一起傳播;而其他的 Spans 訊息,例如操作名稱,日誌等不會被進行傳播。取而代之的是,採樣的 spans 會在後台非同步傳輸到 Jaeger Agents。
該架構的開銷很小,並且設計為始終在生產中啟用。為了最大程度的減少開銷,Jaeger-client 採用了各種採樣策略。當對 trace 進行採樣時,對 span 的分析資料將被捕獲並將其傳輸到 Jeager 後端。如果沒有對 trace 進行採樣,則不會採集任何效能分析資料,並且對 OpenTracing API 的呼叫會被短路,以產生最小的開銷。預設情況下,Jaeger client 對 0.1% 的 traces 進行採樣(每1000個中的1個),並且能夠從代理中搜尋採樣策略。
jaeger-agent
jaeger-agent 是一個網路守護程式,它偵聽透過 UDP 發送的spans,然後將其分批發送給 Collector 收集器。它旨在作為基礎結構套件建置到所有主機。jaeger-agent 將 Collector 的路由和發現從 jaeger-client 抽象出來。
jaeger-collector
Jaeger-collector 從 Jaeger-agent 接收追蹤,並透過處理管道執行它們。當前,管道會驗證追蹤,為其建立索引, 執行任何轉換並最終儲存它們。Jaeger 的儲存裝置是可插拔套件,目前支援 Cassandra,Elasticsearch 和 Kafka。
jaeger-query
搜尋是一項從儲存中搜尋 traces 並透過 UI 來顯示的服務。
Jaeger 採樣
Jaeger 庫實現了一致的前期(或基於頭)的採樣。例如,假設有一個簡單的呼叫圖,其中服務 A 呼叫服務 B,服務 B 呼叫服務C:
支援設定採樣率是 Jaeger 的一個亮點,在生產環境中,如果對每個請求都開啟 Trace,必然會對系統效能帶來一定壓力,除此之外,數量龐大的 Span 也會佔用大量的儲存空間。為了盡量消除分散式追蹤採樣對系統帶來的影響,設定採樣率是一個很好的辦法。
客戶端採樣策略配置
當使用配置物件來例項化 tracer 時,可以透過
- Constant(
sampler.type=const ):const 意為常數,採樣器始終對所有 traces 做出相同的決定。sample.param=1 則採樣所有 tracer,sample.param=0 則都不採樣。 - Probabilistic (
sampler.type=probabilistic ):概率採樣,採樣概率介於0-1之間,透過sample.param 屬性進行配置,例如,在sampler.param=0.1 的情況下,將在10條 traces 中大約採樣1條。 - Rate Limiting (
sampler.type=ratelimiting ):設定每秒的採樣次數上限。當sampler.param=2 的時候,將以每秒 2 條 traces 的速率對請求進行採樣。 - Remote (
sampler.type=remote ):預設配置,client 將從 jaeger-agent 中取得當前服務使用的採樣策略,這允許 Client 從 Jaeger Agent 中動態取得採樣率設定。
自適應採樣器
自適應採樣器是一個組合了兩個功能的複合採樣器:
- 它基於每個操作(即基於 span 操作名稱)做出抽樣決策。這在API服務中特別有用,這些 API 服務的端點的流量可能非常不同,並且對整個服務使用單個概率採樣器可能會使某些低 QPS 端點餓死(從不採樣)。
- 它支援最低的保證採樣率,例如始終允許每秒最多 N 條 traces,然後以一定的概率採樣所有高於此值的採樣率(一切都是針對每個操作,而不是針對每個服務)。
可以靜態配置每個操作引數,也可以在遠端採樣器的幫助下從 Jaeger 後端定期選取每個操作引數。自適應採樣器旨在與 Jaeger 後端即將推出的自適應採樣功能一起使用。
Jaeger 採樣配置範例
收集器可以透過
如果未提供任何配置,則收集器將為所有服務回傳預設概率抽樣策略,概率為 0.001(0.1%)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | {<!-- --> "service_strategies": [ {<!-- --> "service": "foo", "type": "probabilistic", "param": 0.8, "operation_strategies": [ {<!-- --> "operation": "op1", "type": "probabilistic", "param": 0.2 }, {<!-- --> "operation": "op2", "type": "probabilistic", "param": 0.4 } ] }, {<!-- --> "service": "bar", "type": "ratelimiting", "param": 5 } ], "default_strategy": {<!-- --> "type": "probabilistic", "param": 0.5, "operation_strategies": [ {<!-- --> "operation": "/health", "type": "probabilistic", "param": 0.0 }, {<!-- --> "operation": "/metrics", "type": "probabilistic", "param": 0.0 } ] } } |
在上面的範例中,
服務
Jaeger 建置
以上這些套件,官方提供了
收集器直接寫入資料庫
收集器將資料寫入 kafka 作為初步緩衝區
圖示中的 Ingester 是一項從 Kafka topic 讀取並寫入另一個儲存後端(Cassandra,Elasticsearch)的服務。
使用範例
以上面 OpenTracing 的範例,對其改用 Jaeger 追蹤系統,由於 Jaeger 遵循 OpenTracing 規範,並不需要修改伺服端程式碼,只需要修改啟動的 main 程式碼內容。
jaeger 支援以下的客戶端庫:
Language | GitHub Repo |
---|---|
Go | jaegertracing/jaeger-client-go |
Java | jaegertracing/jaeger-client-java |
Node.js | jaegertracing/jaeger-client-node |
Python | jaegertracing/jaeger-client-python |
C++ | jaegertracing/jaeger-client-cpp |
C# | jaegertracing/jaeger-client-csharp |
修改後的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | var ( port = flag.Int("port", 8888, "Example app port.") appdashPort = flag.Int("appdash.port", 8700, "Run appdash locally on this port.") ) func main() {<!-- --> flag.Parse() // 連接到追蹤系統(Appdash) //addr := startAppdashServer(*appdashPort) //tracer := appdashot.NewTracer(appdash.NewRemoteCollector(addr)) //opentracing.InitGlobalTracer(tracer) // 連接到追蹤系統(Jaeger) cfg := jaegercfg.Configuration{<!-- --> Sampler: &jaegercfg.SamplerConfig{<!-- --> Type: jaeger.SamplerTypeConst, Param: 1, }, Reporter: &jaegercfg.ReporterConfig{<!-- --> LogSpans: true, }, } jLogger := jaegerlog.StdLogger jMetricsFactory := metrics.NullFactory closer, err := cfg.InitGlobalTracer( "serviceName", jaegercfg.Logger(jLogger), jaegercfg.Metrics(jMetricsFactory), ) if err != nil {<!-- --> log.Printf("Could not initialize jaeger trace: %s",err.Error()) return } defer closer.Close() addr := fmt.Sprintf(":%d", *port) mux := http.NewServeMux() mux.HandleFunc("/", server.IndexHandler) mux.HandleFunc("/home", server.HomeHandler) mux.HandleFunc("/async", server.ServiceHandler) mux.HandleFunc("/service", server.ServiceHandler) mux.HandleFunc("/db", server.DbHandler) fmt.Printf("Go to http://localhost:%d/home to start arequest! ", *port) log.Fatal(http.ListenAndServe(addr, mux)) } |
執行之前,需要先將 Jaeger 的各個套件都跑起來,官方提供了
之後,執行 main 函式,就可以透過
關於 Jeager 在 go 語言中的更多使用細則,可以檢視 jaeger-client-go官方倉庫 以及倉庫下的 config/example_test.go 目錄。
參考連結:
OpenTracing 檔案中文版(翻譯)吳晟
OpenTracing 詳解
OpenTracing 語意標準
OpenTracing 語意慣例
opentracing-go
開放分散式追蹤(OpenTracing)入門與 Jaeger 實現
jaeger-client-go
Jaeger 教學
jaeger-doc-zh
Jaeger-doc
Go整合Opentracing(分散式鏈路追蹤)
微服務鏈路追蹤之Jaeger