Jaeger?Client?Go入門并實現(xiàn)鏈路追蹤
Jaeger
OpenTracing 是開放式分布式追蹤規(guī)范,OpenTracing API 是一致,可表達,與供應商無關的API,用于分布式跟蹤和上下文傳播。
OpenTracing 的客戶端庫以及規(guī)范,可以到 Github 中查看:https://github.com/opentracing/
Jaeger 是 Uber 開源的分布式跟蹤系統(tǒng),詳細的介紹可以自行查閱資料。
部署 Jaeger
這里我們需要部署一個 Jaeger 實例,以供微服務以及后面學習需要。
使用 Docker 部署很簡單,只需要執(zhí)行下面一條命令即可:
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 查詢這些追蹤信息。
從示例了解 Jaeger Client Go
這里,我們主要了解一些 Jaeger Client 的接口和結構體,了解一些代碼的使用。
為了讓讀者方便了解 Trace、Span 等,可以看一下這個 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 }
創(chuàng)建一個 client1 的項目,然后引入 Jaeger client 包。
go get -u github.com/uber/jaeger-client-go/
然后引入包
import ( "github.com/uber/jaeger-client-go" )
了解 trace、span
鏈路追蹤中的一個進程使用一個 trace 實例標識,每個服務或函數(shù)使用一個 span 標識,jaeger 包中有個函數(shù)可以創(chuàng)建空的 trace:
tracer := opentracing.GlobalTracer() // 生產(chǎn)中不要使用
然后就是調用鏈中,生成父子關系的 Span:
func main() { tracer := opentracing.GlobalTracer() // 創(chuàng)建第一個 span A parentSpan := tracer.StartSpan("A") defer parentSpan.Finish() // 可手動調用 Finish() } func B(tracer opentracing.Tracer,parentSpan opentracing.Span){ // 繼承上下文關系,創(chuàng)建子 span childSpan := tracer.StartSpan( "B", opentracing.ChildOf(parentSpan.Context()), ) defer childSpan.Finish() // 可手動調用 Finish() }
每個 span 表示調用鏈中的一個結點,每個結點都需要明確父 span。
現(xiàn)在,我們知道了,如何生成 trace{span1,span2}
,且 span1 -> span2
即 span1 調用 span2,或 span1 依賴于 span2。
tracer 配置
由于服務之間的調用是跨進程的,每個進程都有一些特點的標記,為了標識這些進程,我們需要在上下文間、span 攜帶一些信息。
例如,我們在發(fā)起請求的第一個進程中,配置 trace,配置服務名稱等。
// 引入 jaegercfg "github.com/uber/jaeger-client-go/config" cfg := jaegercfg.Configuration{ ServiceName: "client test", // 對其發(fā)起請求的的調用鏈,叫什么服務 Sampler: &jaegercfg.SamplerConfig{ Type: jaeger.SamplerTypeConst, Param: 1, }, Reporter: &jaegercfg.ReporterConfig{ LogSpans: true, }, }
Sampler 是客戶端采樣率配置,可以通過 sampler.type
和 sampler.param
屬性選擇采樣類型,后面詳細聊一下。
Reporter 可以配置如何上報,后面獨立小節(jié)聊一下這個配置。
傳遞上下文的時候,我們可以打印一些日志:
jLogger := jaegerlog.StdLogger
配置完畢后就可以創(chuàng)建 tracer 對象了:
tracer, closer, err := cfg.NewTracer( jaegercfg.Logger(jLogger), ) defer closer.Close() if err != nil { }
完整代碼如下:
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", // 對其發(fā)起請求的的調用鏈,叫什么服務 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 { } // 創(chuàng)建第一個 span A parentSpan := tracer.StartSpan("A") defer parentSpan.Finish() B(tracer,parentSpan) } func B(tracer opentracing.Tracer, parentSpan opentracing.Span) { // 繼承上下文關系,創(chuàng)建子 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 配置代碼示例:
Sampler: &jaegercfg.SamplerConfig{ Type: jaeger.SamplerTypeConst, Param: 1, }
這個 sampler 可以使用 jaegercfg.SamplerConfig
,通過 type
、param
兩個字段來配置采樣器。
為什么要配置采樣器?因為服務中的請求千千萬萬,如果每個請求都要記錄追蹤信息并發(fā)送到 Jaeger 后端,那么面對高并發(fā)時,記錄鏈路追蹤以及推送追蹤信息消耗的性能就不可忽視,會對系統(tǒng)帶來較大的影響。當我們配置 sampler 后,jaeger 會根據(jù)當前配置的采樣策略做出采樣行為。
詳細可以參考:https://www.jaegertracing.io/docs/1.22/sampling/
jaegercfg.SamplerConfig 結構體中的字段 Param 是設置采樣率或速率,要根據(jù) 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 后端中央配置甚至動態(tài)地控制服務中的采樣策略。
Reporter 配置
看一下 ReporterConfig 的定義。
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 信息,隊列滿了后一次性發(fā)送到 jaeger 后端;defaultQueueSize 默認為 100;
BufferFlushInterval 強制清空、推送隊列時間,對于流量不高的程序,隊列可能長時間不能滿,那么設置這個時間,超時可以自動推送一次。對于高并發(fā)的情況,一般隊列很快就會滿的,滿了后也會自動推送。默認為1秒。
LogSpans 是否把 Log 也推送,span 中可以攜帶一些日志信息。
LocalAgentHostPort 要推送到的 Jaeger agent,默認端口 6831,是 Jaeger 接收壓縮格式的 thrift 協(xié)議的數(shù)據(jù)端口。
CollectorEndpoint 要推送到的 Jaeger Collector,用 Collector 就不用 agent 了。
例如通過 http 上傳 trace:
Reporter: &jaegercfg.ReporterConfig{ LogSpans: true, CollectorEndpoint: "http://127.0.0.1:14268/api/traces", },
據(jù)黑洞大佬的提示,HTTP 走的就是 thrift,而 gRPC 是 .NET 特供,所以 reporter 格式只有一種,而且填寫 CollectorEndpoint,我們注意要填寫完整的信息。
完整代碼測試:
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", // 對其發(fā)起請求的的調用鏈,叫什么服務 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), ) // 創(chuàng)建第一個 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) { // 繼承上下文關系,創(chuàng)建子 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,可以看到已經(jīng)推送完畢(http://127.0.0.1:16686)。
這時,我們可以抽象代碼代碼示例:
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 }
這樣可以復用代碼,調用函數(shù)創(chuàng)建一個新的 tracer。這個記下來,后面要用。
分布式系統(tǒng)與span
前面介紹了如何配置 tracer 、推送數(shù)據(jù)到 Jaeger Collector,接下來我們聊一下 Span。請看圖。
下圖是一個由用戶 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)。
這里,我們只需要記住,從 A 開始,A 需要依賴多個服務才能完成任務,每個服務可能是一個進程,也可能是一個進程中的另一個函數(shù)。這個要看你代碼是怎么寫的。后面會詳細說一下如何定義這種關系,現(xiàn)在大概了解一下即可。
怎么調、怎么傳
如果有了解過 Jaeger 或讀過 分布式鏈路追蹤框架的基本實現(xiàn)原理 ,那么已經(jīng)大概了解的 Jaeger 的工作原理。
jaeger 是分布式鏈路追蹤工具,如果不用在跨進程上,那么 Jaeger 就失去了意義。而微服務中跨進程調用,一般有 HTTP 和 gRPC 兩種,下面將來講解如何在 HTTP、gPRC 調用中傳遞 Jaeger 的 上下文。
HTTP,跨進程追蹤
A、B 兩個進程,A 通過 HTTP 調用 B 時,通過 Http Header 攜帶 trace 信息(稱為上下文),然后 B 進程接收后,解析出來,在創(chuàng)建 trace 時跟傳遞而來的 上下文關聯(lián)起來。
一般使用中間件來處理別的進程傳遞而來的上下文。inject
函數(shù)打包上下文到 Header 中,而 extract
函數(shù)則將其解析出來。
這里我們分為兩步,第一步從 A 進程中傳遞上下文信息到 B 進程,為了方便演示已經(jīng)實踐,我們使用 client-webserver 的形式,編寫代碼。
客戶端
在 A 進程新建一個方法:
// 請求遠程服務,獲得用戶信息 func GetUserInfo(tracer opentracing.Tracer, parentSpan opentracing.Span) { // 繼承上下文關系,創(chuàng)建子 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
函數(shù)。
main 函數(shù)改成:
func main() { tracer, closer, _ := CreateTracer("UserinfoService") // 創(chuàng)建第一個 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
Web 服務端
服務端我們使用 gin 來搭建。
新建一個 go 項目,在 main.go 目錄中,執(zhí)行 go get -u github.com/gin-gonic/gin
。
創(chuàng)建一個函數(shù),該函數(shù)可以從創(chuàng)建一個 tracer,并且繼承其它進程傳遞過來的上下文信息。
// 從上下文中解析并創(chuàng)建一個新的 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 上下文,我們需要通過中間件來解析了處理一些細節(jié)。
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() // 繼續(xù)設置 tag ext.HTTPStatusCode.Set(startSpan, uint16(c.Writer.Status())) } return handler }
別忘記了 API 服務:
func GetUserInfo(ctx *gin.Context) { userName := ctx.Param("username") fmt.Println("收到請求,用戶名稱為:", userName) ctx.String(http.StatusOK, "他的博客是 https://whuanle.cn") }
然后是 main 方法:
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
分別啟動 webserver、client,會發(fā)現(xiàn)打印日志。并且打開 jaerger ui 界面,會出現(xiàn)相關的追蹤信息。
Tag 、 Log 和 Ref
Jaeger 的鏈路追蹤中,可以攜帶 Tag 和 Log,他們都是鍵值對的形式:
{ "key": "http.method", "type": "string", "value": "GET" },
Tag 設置方法是 ext.xxxx
,例如 :
ext.HTTPUrl.Set(startSpan, c.Request.URL.Path)
因為 opentracing 已經(jīng)規(guī)定了所有的 Tag 類型,所以我們只需要調用 ext.xxx.Set()
設置即可。
前面寫示例的時候忘記把日志也加一下了。。。日志其實很簡單的,通過 span 對象調用函數(shù)即可設置。
示例(在中間件里面加一下):
startSpan.LogFields( log.String("event", "soft error"), log.String("type", "cache timeout"), log.Int("waited.millis", 1500))
ref 就是多個 span 之間的關系。span 可以是跨進程的,也可以是一個進程內的不同函數(shù)中的。
其中 span 的依賴關系表示示例:
"references": [ { "refType": "CHILD_OF", "traceID": "33ba35e7cc40172c", "spanID": "1c7826fa185d1107" }]
spanID 為其依賴的父 span。
可以看下面這張圖。
一個進程中的 tracer 可以包裝一些代碼和操作,為多個 span 生成一些信息,或創(chuàng)建父子關系。
而 遠程請求中傳遞的是 SpanContext,傳遞后,遠程服務也創(chuàng)建新的 tracer,然后從 SpanContext 生成 span 依賴關系。
子 span 中,其 reference 列表中,會帶有 父 span 的 span id。
到此這篇關于Jaeger Client Go入門并實現(xiàn)鏈路追蹤的文章就介紹到這了。希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
相關文章
詳解go-zero如何使用validator進行參數(shù)校驗
這篇文章主要介紹了如何使用validator庫做參數(shù)校驗的一些十分實用的使用技巧,包括翻譯校驗錯誤提示信息、自定義提示信息的字段名稱、自定義校驗方法等,感興趣的可以了解下2024-01-01