1. 程式人生 > >Jaeger Client Go 鏈路追蹤|入門詳解

Jaeger Client Go 鏈路追蹤|入門詳解

[TOC] ### 從何說起 之前參加檸檬大佬的訓練營(免費白嫖),在大佬的指導下我們技術蒸蒸日上,然後作業我們需要實現一個 Jaeger 後端,筆者採用 .NET + MongoDB 來實現(大佬說用C#寫的扣10分,嗚嗚嗚...),C# 版本的實現專案地址[https://github.com/whuanle/DistributedTracing](https://github.com/whuanle/DistributedTracing),專案支援 Jaeger Collector、Query 等。 現在筆者開始轉 Go 語言,所以開始 Go 重新實現一次,下一篇文章將完整介紹如何實現一個 Jaeger Collector。在這篇文章,我們可以先學習 Jaeger client Go 的使用方法,以及 Jaeger Go 的一些概念。 在此之前,建議讀者稍微看一下 [分散式鏈路追蹤框架的基本實現原理](https://www.cnblogs.com/whuanle/p/14321107.html) 這篇文章,需要了解 Dapper 論文和一些 Jaeger 的概念。 接下來我們將一步步學習 Go 中的一些技術,後面慢慢展開 Jaeger Client。 ### Jaeger OpenTracing 是開放式分散式追蹤規範,OpenTracing API 是一致,可表達,與供應商無關的API,用於分散式跟蹤和上下文傳播。 OpenTracing 的客戶端庫以及規範,可以到 Github 中檢視:https://github.com/opentracing/ Jaeger 是 Uber 開源的分散式跟蹤系統,詳細的介紹可以自行查閱資料。 ### 部署 Jaeger 這裡我們需要部署一個 Jaeger 例項,以供微服務以及後面學習需要。 使用 Docker 部署很簡單,只需要執行下面一條命令即可: ```shell docker run -d -p 5775:5775/udp -p 16686:16686 -p 14250:14250 -p 14268:14268 jaegertracing/all-in-one:latest ``` 訪問 16686 埠,即可看到 UI 介面。 後面我們生成的鏈路追蹤資訊會推送到此服務,而且可以通過 Jaeger UI 查詢這些追蹤資訊。 ![JaegerUI](https://img2020.cnblogs.com/blog/1315495/202101/1315495-20210109224307467-2054331430.png) ### 從示例瞭解 Jaeger Client Go 這裡,我們主要了解一些 Jaeger Client 的介面和結構體,瞭解一些程式碼的使用。 為了讓讀者方便了解 Trace、Span 等,可以看一下這個 Json 的大概結構: ```json { "traceID": "2da97aa33839442e", "spans": [ { "traceID": "2da97aa33839442e", "spanID": "ccb83780e27f016c", "flags": 1, "operationName": "format-string", "references": [...], "tags": [...], "logs": [...], "processID": "p1", "warnings": null }, ... ... ], "processes": { "p1": { "serviceName": "hello-world", "tags": [...] }, "p2": ..., "warnings": null } ``` 建立一個 client1 的專案,然後引入 Jaeger client 包。 ```shell go get -u github.com/uber/jaeger-client-go/ ``` 然後引入包 ``` import ( "github.com/uber/jaeger-client-go" ) ``` ### 瞭解 trace、span 鏈路追蹤中的一個程序使用一個 trace 例項標識,每個服務或函式使用一個 span 標識,jaeger 包中有個函式可以建立空的 trace: ```go tracer := opentracing.GlobalTracer() // 生產中不要使用 ``` 然後就是呼叫鏈中,生成父子關係的 Span: ```go func main() { tracer := opentracing.GlobalTracer() // 建立第一個 span A parentSpan := tracer.StartSpan("A") defer parentSpan.Finish() // 可手動呼叫 Finish() } func B(tracer opentracing.Tracer,parentSpan opentracing.Span){ // 繼承上下文關係,建立子 span childSpan := tracer.StartSpan( "B", opentracing.ChildOf(parentSpan.Context()), ) defer childSpan.Finish() // 可手動呼叫 Finish() } ``` 每個 span 表示呼叫鏈中的一個結點,每個結點都需要明確父 span。 現在,我們知道了,如何生成 `trace{span1,span2}`,且 `span1 -> span2` 即 span1 呼叫 span2,或 span1 依賴於 span2。 ### tracer 配置 由於服務之間的呼叫是跨程序的,每個程序都有一些特點的標記,為了標識這些程序,我們需要在上下文間、span 攜帶一些資訊。 例如,我們在發起請求的第一個程序中,配置 trace,配置服務名稱等。 ```go // 引入 jaegercfg "github.com/uber/jaeger-client-go/config" cfg := jaegercfg.Configuration{ ServiceName: "client test", // 對其發起請求的的呼叫鏈,叫什麼服務 Sampler: &jaegercfg.SamplerConfig{ Type: jaeger.SamplerTypeConst, Param: 1, }, Reporter: &jaegercfg.ReporterConfig{ LogSpans: true, }, } ``` Sampler 是客戶端取樣率配置,可以通過 `sampler.type` 和 `sampler.param` 屬性選擇取樣型別,後面詳細聊一下。 Reporter 可以配置如何上報,後面獨立小節聊一下這個配置。 傳遞上下文的時候,我們可以列印一些日誌: ```go jLogger := jaegerlog.StdLogger ``` 配置完畢後就可以建立 tracer 物件了: ```go tracer, closer, err := cfg.NewTracer( jaegercfg.Logger(jLogger), ) defer closer.Close() if err != nil { } ``` 完整程式碼如下: ```go import ( "github.com/opentracing/opentracing-go" "github.com/uber/jaeger-client-go" jaegercfg "github.com/uber/jaeger-client-go/config" jaegerlog "github.com/uber/jaeger-client-go/log" ) func main() { cfg := jaegercfg.Configuration{ ServiceName: "client test", // 對其發起請求的的呼叫鏈,叫什麼服務 Sampler: &jaegercfg.SamplerConfig{ Type: jaeger.SamplerTypeConst, Param: 1, }, Reporter: &jaegercfg.ReporterConfig{ LogSpans: true, }, } jLogger := jaegerlog.StdLogger tracer, closer, err := cfg.NewTracer( jaegercfg.Logger(jLogger), ) defer closer.Close() if err != nil { } // 建立第一個 span A parentSpan := tracer.StartSpan("A") defer parentSpan.Finish() B(tracer,parentSpan) } func B(tracer opentracing.Tracer, parentSpan opentracing.Span) { // 繼承上下文關係,建立子 span childSpan := tracer.StartSpan( "B", opentracing.ChildOf(parentSpan.Context()), ) defer childSpan.Finish() } ``` 啟動後: ``` 2021/03/30 11:14:38 Initializing logging reporter 2021/03/30 11:14:38 Reporting span 689df7e83255d05d:75668e8ed5ec61da:689df7e83255d05d:1 2021/03/30 11:14:38 Reporting span 689df7e83255d05d:689df7e83255d05d:0000000000000000:1 2021/03/30 11:14:38 DEBUG: closing tracer 2021/03/30 11:14:38 DEBUG: closing reporter ``` ### Sampler 配置 sampler 配置程式碼示例: ```go Sampler: &jaegercfg.SamplerConfig{ Type: jaeger.SamplerTypeConst, Param: 1, } ``` 這個 sampler 可以使用 `jaegercfg.SamplerConfig`,通過 `type`、`param` 兩個欄位來配置取樣器。 為什麼要配置取樣器?因為服務中的請求千千萬萬,如果每個請求都要記錄追蹤資訊併發送到 Jaeger 後端,那麼面對高併發時,記錄鏈路追蹤以及推送追蹤資訊消耗的效能就不可忽視,會對系統帶來較大的影響。當我們配置 sampler 後,jaeger 會根據當前配置的取樣策略做出取樣行為。 詳細可以參考:[https://www.jaegertracing.io/docs/1.22/sampling/](https://www.jaegertracing.io/docs/1.22/sampling/) jaegercfg.SamplerConfig 結構體中的欄位 Param 是設定取樣率或速率,要根據 Type 而定。 下面對其關係進行說明: | Type | Param | 說明 | | --------------- | ------- | ------------------------------------------------------------ | | "const" | 0或1 | 取樣器始終對所有 tracer 做出相同的決定;要麼全部取樣,要麼全部不採樣 | | "probabilistic" | 0.0~1.0 | 取樣器做出隨機取樣決策,Param 為取樣概率 | | "ratelimiting" | N | 取樣器一定的恆定速率對tracer進行取樣,Param=2.0,則限制每秒採集2條 | | "remote" | 無 | 取樣器請諮詢Jaeger代理以獲取在當前服務中使用的適當取樣策略。 | `sampler.Type="remote"`/`sampler.Type=jaeger.SamplerTypeRemote` 是取樣器的預設值,當我們不做配置時,會從 Jaeger 後端中央配置甚至動態地控制服務中的取樣策略。 ### Reporter 配置 看一下 ReporterConfig 的定義。 ```go type ReporterConfig struct { QueueSize int `yaml:"queueSize"` BufferFlushInterval time.Duration LogSpans bool `yaml:"logSpans"` LocalAgentHostPort string `yaml:"localAgentHostPort"` DisableAttemptReconnecting bool `yaml:"disableAttemptReconnecting"` AttemptReconnectInterval time.Duration CollectorEndpoint string `yaml:"collectorEndpoint"` User string `yaml:"user"` Password string `yaml:"password"` HTTPHeaders map[string]string `yaml:"http_headers"` } ``` Reporter 配置客戶端如何上報追蹤資訊的,所有欄位都是可選的。 這裡我們介紹幾個常用的配置欄位。 * QUEUESIZE,設定佇列大小,儲存取樣的 span 資訊,佇列滿了後一次性發送到 jaeger 後端;defaultQueueSize 預設為 100; * BufferFlushInterval 強制清空、推送佇列時間,對於流量不高的程式,佇列可能長時間不能滿,那麼設定這個時間,超時可以自動推送一次。對於高併發的情況,一般佇列很快就會滿的,滿了後也會自動推送。預設為1秒。 * LogSpans 是否把 Log 也推送,span 中可以攜帶一些日誌資訊。 * LocalAgentHostPort 要推送到的 Jaeger agent,預設埠 6831,是 Jaeger 接收壓縮格式的 thrift 協議的資料埠。 * CollectorEndpoint 要推送到的 Jaeger Collector,用 Collector 就不用 agent 了。 例如通過 http 上傳 trace: ``` Reporter: &jaegercfg.ReporterConfig{ LogSpans: true, CollectorEndpoint: "http://127.0.0.1:14268/api/traces", }, ``` 據黑洞大佬的提示,HTTP 走的就是 thrift,而 gRPC 是 .NET 特供,所以 reporter 格式只有一種,而且填寫 CollectorEndpoint,我們注意要填寫完整的資訊。 完整程式碼測試: ```go import ( "bufio" "github.com/opentracing/opentracing-go" "github.com/uber/jaeger-client-go" jaegercfg "github.com/uber/jaeger-client-go/config" jaegerlog "github.com/uber/jaeger-client-go/log" "os" ) func main() { var cfg = jaegercfg.Configuration{ ServiceName: "client test", // 對其發起請求的的呼叫鏈,叫什麼服務 Sampler: &jaegercfg.SamplerConfig{ Type: jaeger.SamplerTypeConst, Param: 1, }, Reporter: &jaegercfg.ReporterConfig{ LogSpans: true, CollectorEndpoint: "http://127.0.0.1:14268/api/traces", }, } jLogger := jaegerlog.StdLogger tracer, closer, _ := cfg.NewTracer( jaegercfg.Logger(jLogger), ) // 建立第一個 span A parentSpan := tracer.StartSpan("A") // 呼叫其它服務 B(tracer, parentSpan) // 結束 A parentSpan.Finish() // 結束當前 tracer closer.Close() reader := bufio.NewReader(os.Stdin) _, _ = reader.ReadByte() } func B(tracer opentracing.Tracer, parentSpan opentracing.Span) { // 繼承上下文關係,建立子 span childSpan := tracer.StartSpan( "B", opentracing.ChildOf(parentSpan.Context()), ) defer childSpan.Finish() } ``` 執行後輸出結果: ``` 2021/03/30 15:04:15 Initializing logging reporter 2021/03/30 15:04:15 Reporting span 715e0af47c7d9acb:7dc9a6b568951e4f:715e0af47c7d9acb:1 2021/03/30 15:04:15 Reporting span 715e0af47c7d9acb:715e0af47c7d9acb:0000000000000000:1 2021/03/30 15:04:15 DEBUG: closing tracer 2021/03/30 15:04:15 DEBUG: closing reporter 2021/03/30 15:04:15 DEBUG: flushed 1 spans 2021/03/30 15:04:15 DEBUG: flushed 1 spans ``` 開啟 Jaeger UI,可以看到已經推送完畢(http://127.0.0.1:16686)。 ![上傳的trace](https://img2020.cnblogs.com/blog/1315495/202103/1315495-20210330180024156-1752473610.png) 這時,我們可以抽象程式碼程式碼示例: ```go func CreateTracer(servieName string) (opentracing.Tracer, io.Closer, error) { var cfg = jaegercfg.Configuration{ ServiceName: servieName, Sampler: &jaegercfg.SamplerConfig{ Type: jaeger.SamplerTypeConst, Param: 1, }, Reporter: &jaegercfg.ReporterConfig{ LogSpans: true, // 按實際情況替換你的 ip CollectorEndpoint: "http://127.0.0.1:14268/api/traces", }, } jLogger := jaegerlog.StdLogger tracer, closer, err := cfg.NewTracer( jaegercfg.Logger(jLogger), ) return tracer, closer, err } ``` 這樣可以複用程式碼,呼叫函式建立一個新的 tracer。這個記下來,後面要用。 ### 分散式系統與span 前面介紹瞭如何配置 tracer 、推送資料到 Jaeger Collector,接下來我們聊一下 Span。請看圖。 下圖是一個由使用者 X 請求發起的,穿過多個服務的分散式系統,A、B、C、D、E 表示不同的子系統或處理過程。 在這個圖中, A 是前端,B、C 是中間層、D、E 是 C 的後端。這些子系統通過 rpc 協議連線,例如 gRPC。 一個簡單實用的分散式鏈路追蹤系統的實現,就是對伺服器上每一次請求以及響應收集跟蹤識別符號(message identifiers)和時間戳(timestamped events)。 這裡,我們只需要記住,從 A 開始,A 需要依賴多個服務才能完成任務,每個服務可能是一個程序,也可能是一個程序中的另一個函式。這個要看你程式碼是怎麼寫的。後面會詳細說一下如何定義這種關係,現在大概瞭解一下即可。 ![span呼叫鏈](https://img2020.cnblogs.com/blog/1315495/202101/1315495-20210124092812074-1348764625.png) ### 怎麼調、怎麼傳 如果有了解過 Jaeger 或讀過 [分散式鏈路追蹤框架的基本實現原理](https://www.cnblogs.com/whuanle/p/14321107.html) ,那麼已經大概瞭解的 Jaeger 的工作原理。 jaeger 是分散式鏈路追蹤工具,如果不用在跨程序上,那麼 Jaeger 就失去了意義。而微服務中跨程序呼叫,一般有 HTTP 和 gRPC 兩種,下面將來講解如何在 HTTP、gPRC 呼叫中傳遞 Jaeger 的 上下文。 ### HTTP,跨程序追蹤 A、B 兩個程序,A 通過 HTTP 呼叫 B 時,通過 Http Header 攜帶 trace 資訊(稱為上下文),然後 B 程序接收後,解析出來,在建立 trace 時跟傳遞而來的 上下文關聯起來。 一般使用中介軟體來處理別的程序傳遞而來的上下文。`inject` 函式打包上下文到 Header 中,而 `extract` 函式則將其解析出來。 ![](https://img2020.cnblogs.com/blog/1315495/202101/1315495-20210124151204778-2127450807.png) 這裡我們分為兩步,第一步從 A 程序中傳遞上下文資訊到 B 程序,為了方便演示已經實踐,我們使用 client-webserver 的形式,編寫程式碼。 #### 客戶端 在 A 程序新建一個方法: ```go // 請求遠端服務,獲得使用者資訊 func GetUserInfo(tracer opentracing.Tracer, parentSpan opentracing.Span) { // 繼承上下文關係,建立子 span childSpan := tracer.StartSpan( "B", opentracing.ChildOf(parentSpan.Context()), ) url := "http://127.0.0.1:8081/Get?username=痴者工良" req,_ := http.NewRequest("GET", url, nil) // 設定 tag,這個 tag 我們後面講 ext.SpanKindRPCClient.Set(childSpan) ext.HTTPUrl.Set(childSpan, url) ext.HTTPMethod.Set(childSpan, "GET") tracer.Inject(childSpan.Context(), opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(req.Header)) resp, _ := http.DefaultClient.Do(req) _ = resp // 丟掉 defer childSpan.Finish() } ``` 然後複用前面提到的 `CreateTracer` 函式。 main 函式改成: ```go func main() { tracer, closer, _ := CreateTracer("UserinfoService") // 建立第一個 span A parentSpan := tracer.StartSpan("A") // 呼叫其它服務 GetUserInfo(tracer, parentSpan) // 結束 A parentSpan.Finish() // 結束當前 tracer closer.Close() reader := bufio.NewReader(os.Stdin) _, _ = reader.ReadByte() } ``` 完整程式碼可參考:[https://github.com/whuanle/DistributedTracingGo/issues/1](https://github.com/whuanle/DistributedTracingGo/issues/1) #### Web 服務端 服務端我們使用 gin 來搭建。 新建一個 go 專案,在 main.go 目錄中,執行 `go get -u github.com/gin-gonic/gin`。 建立一個函式,該函式可以從建立一個 tracer,並且繼承其它程序傳遞過來的上下文資訊。 ```go // 從上下文中解析並建立一個新的 trace,獲得傳播的 上下文(SpanContext) func CreateTracer(serviceName string, header http.Header) (opentracing.Tracer,opentracing.SpanContext, io.Closer, error) { var cfg = jaegercfg.Configuration{ ServiceName: serviceName, Sampler: &jaegercfg.SamplerConfig{ Type: jaeger.SamplerTypeConst, Param: 1, }, Reporter: &jaegercfg.ReporterConfig{ LogSpans: true, // 按實際情況替換你的 ip CollectorEndpoint: "http://127.0.0.1:14268/api/traces", }, } jLogger := jaegerlog.StdLogger tracer, closer, err := cfg.NewTracer( jaegercfg.Logger(jLogger), ) // 繼承別的程序傳遞過來的上下文 spanContext, _ := tracer.Extract(opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(header)) return tracer, spanContext, closer, err } ``` 為了解析 HTTP 傳遞而來的 span 上下文,我們需要通過中介軟體來解析了處理一些細節。 ```go func UseOpenTracing() gin.HandlerFunc { handler := func(c *gin.Context) { // 使用 opentracing.GlobalTracer() 獲取全域性 Tracer tracer,spanContext, closer, _ := CreateTracer("userInfoWebService", c.Request.Header) defer closer.Close() // 生成依賴關係,並新建一個 span、 // 這裡很重要,因為生成了 References []SpanReference 依賴關係 startSpan:= tracer.StartSpan(c.Request.URL.Path,ext.RPCServerOption(spanContext)) defer startSpan.Finish() // 記錄 tag // 記錄請求 Url ext.HTTPUrl.Set(startSpan, c.Request.URL.Path) // Http Method ext.HTTPMethod.Set(startSpan, c.Request.Method) // 記錄元件名稱 ext.Component.Set(startSpan, "Gin-Http") // 在 header 中加上當前程序的上下文資訊 c.Request=c.Request.WithContext(opentracing.ContextWithSpan(c.Request.Context(),startSpan)) // 傳遞給下一個中介軟體 c.Next() // 繼續設定 tag ext.HTTPStatusCode.Set(startSpan, uint16(c.Writer.Status())) } return handler } ``` 別忘記了 API 服務: ```go func GetUserInfo(ctx *gin.Context) { userName := ctx.Param("username") fmt.Println("收到請求,使用者名稱稱為:", userName) ctx.String(http.StatusOK, "他的部落格是 https://whuanle.cn") } ``` 然後是 main 方法: ```go func main() { r := gin.Default() // 插入中介軟體處理 r.Use(UseOpenTracing()) r.GET("/Get",GetUserInfo) r.Run("0.0.0.0:8081") // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080") } ``` 完整程式碼可參考:[https://github.com/whuanle/DistributedTracingGo/issues/2](https://github.com/whuanle/DistributedTracingGo/issues/2) 分別啟動 webserver、client,會發現列印日誌。並且開啟 jaerger ui 介面,會出現相關的追蹤資訊。 ![Jaeger追蹤記錄](https://img2020.cnblogs.com/blog/1315495/202103/1315495-20210330175815690-596228485.gif) ### Tag 、 Log 和 Ref Jaeger 的鏈路追蹤中,可以攜帶 Tag 和 Log,他們都是鍵值對的形式: ```json { "key": "http.method", "type": "string", "value": "GET" }, ``` Tag 設定方法是 `ext.xxxx`,例如 : ``` ext.HTTPUrl.Set(startSpan, c.Request.URL.Path) ``` 因為 opentracing 已經規定了所有的 Tag 型別,所以我們只需要呼叫 `ext.xxx.Set()` 設定即可。 前面寫示例的時候忘記把日誌也加一下了。。。日誌其實很簡單的,通過 span 物件呼叫函式即可設定。 示例(在中介軟體裡面加一下): ```go startSpan.LogFields( log.String("event", "soft error"), log.String("type", "cache timeout"), log.Int("waited.millis", 1500)) ``` ![TAG_LOG](https://img2020.cnblogs.com/blog/1315495/202103/1315495-20210330175843277-694478110.png) ref 就是多個 span 之間的關係。span 可以是跨程序的,也可以是一個程序內的不同函式中的。 其中 span 的依賴關係表示示例: ```json "references": [ { "refType": "CHILD_OF", "traceID": "33ba35e7cc40172c", "spanID": "1c7826fa185d1107" }] ``` spanID 為其依賴的父 span。 可以看下面這張圖。 一個程序中的 tracer 可以包裝一些程式碼和操作,為多個 span 生成一些資訊,或建立父子關係。 而 遠端請求中傳遞的是 SpanContext,傳遞後,遠端服務也建立新的 tracer,然後從 SpanContext 生成 span 依賴關係。 子 span 中,其 reference 列表中,會帶有 父 span 的 span id。 ![span傳播](https://img2020.cnblogs.com/blog/1315495/202103/1315495-20210330175910722-576735678.png) 關於 Jaeger Client Go 的文章到此完畢,轉 Go 沒多久,大家可以互相交流喲。