Jaeger Client Go 鏈路追蹤|入門詳解
從何說起
之前參加檸檬大佬的訓練營(免費白嫖),在大佬的指導下我們技術蒸蒸日上,然後作業我們需要實現一個 Jaeger 後端,筆者採用 .NET + MongoDB 來實現(大佬說用C#寫的扣10分,嗚嗚嗚…),C# 版本的實現項目地址//github.com/whuanle/DistributedTracing,項目支援 Jaeger Collector、Query 等。
現在筆者開始轉 Go 語言,所以開始 Go 重新實現一次,下一篇文章將完整介紹如何實現一個 Jaeger Collector。在這篇文章,我們可以先學習 Jaeger client Go 的使用方法,以及 Jaeger Go 的一些概念。
在此之前,建議讀者稍微看一下 分散式鏈路追蹤框架的基本實現原理 這篇文章,需要了解 Dapper 論文和一些 Jaeger 的概念。
接下來我們將一步步學習 Go 中的一些技術,後面慢慢展開 Jaeger Client。
Jaeger
OpenTracing 是開放式分散式追蹤規範,OpenTracing API 是一致,可表達,與供應商無關的API,用於分散式跟蹤和上下文傳播。
OpenTracing 的客戶端庫以及規範,可以到 Github 中查看://github.com/opentracing/
Jaeger 是 Uber 開源的分散式跟蹤系統,詳細的介紹可以自行查閱資料。
部署 Jaeger
這裡我們需要部署一個 Jaeger 實例,以供微服務以及後面學習需要。
使用 Docker 部署很簡單,只需要執行下面一條命令即可:
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
}
創建一個 client1 的項目,然後引入 Jaeger client 包。
go get -u github.com/uber/jaeger-client-go/
然後引入包
import (
"github.com/uber/jaeger-client-go"
)
了解 trace、span
鏈路追蹤中的一個進程使用一個 trace 實例標識,每個服務或函數使用一個 span 標識,jaeger 包中有個函數可以創建空的 trace:
tracer := opentracing.GlobalTracer() // 生產中不要使用
然後就是調用鏈中,生成父子關係的 Span:
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,配置服務名稱等。
// 引入 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 可以配置如何上報,後面獨立小節聊一下這個配置。
傳遞上下文的時候,我們可以列印一些日誌:
jLogger := jaegerlog.StdLogger
配置完畢後就可以創建 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", // 對其發起請求的的調用鏈,叫什麼服務
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 配置程式碼示例:
Sampler: &jaegercfg.SamplerConfig{
Type: jaeger.SamplerTypeConst,
Param: 1,
}
這個 sampler 可以使用 jaegercfg.SamplerConfig
,通過 type
、param
兩個欄位來配置取樣器。
為什麼要配置取樣器?因為服務中的請求千千萬萬,如果每個請求都要記錄追蹤資訊並發送到 Jaeger 後端,那麼面對高並發時,記錄鏈路追蹤以及推送追蹤資訊消耗的性能就不可忽視,會對系統帶來較大的影響。當我們配置 sampler 後,jaeger 會根據當前配置的取樣策略做出取樣行為。
詳細可以參考://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 的定義。
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: "//127.0.0.1:14268/api/traces",
},
據黑洞大佬的提示,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", // 對其發起請求的的調用鏈,叫什麼服務
Sampler: &jaegercfg.SamplerConfig{
Type: jaeger.SamplerTypeConst,
Param: 1,
},
Reporter: &jaegercfg.ReporterConfig{
LogSpans: true,
CollectorEndpoint: "//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,可以看到已經推送完畢(//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: "//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 需要依賴多個服務才能完成任務,每個服務可能是一個進程,也可能是一個進程中的另一個函數。這個要看你程式碼是怎麼寫的。後面會詳細說一下如何定義這種關係,現在大概了解一下即可。
怎麼調、怎麼傳
如果有了解過 Jaeger 或讀過 分散式鏈路追蹤框架的基本實現原理 ,那麼已經大概了解的 Jaeger 的工作原理。
jaeger 是分散式鏈路追蹤工具,如果不用在跨進程上,那麼 Jaeger 就失去了意義。而微服務中跨進程調用,一般有 HTTP 和 gRPC 兩種,下面將來講解如何在 HTTP、gPRC 調用中傳遞 Jaeger 的 上下文。
HTTP,跨進程追蹤
A、B 兩個進程,A 通過 HTTP 調用 B 時,通過 Http Header 攜帶 trace 資訊(稱為上下文),然後 B 進程接收後,解析出來,在創建 trace 時跟傳遞而來的 上下文關聯起來。
一般使用中間件來處理別的進程傳遞而來的上下文。inject
函數打包上下文到 Header 中,而 extract
函數則將其解析出來。
這裡我們分為兩步,第一步從 A 進程中傳遞上下文資訊到 B 進程,為了方便演示已經實踐,我們使用 client-webserver 的形式,編寫程式碼。
客戶端
在 A 進程新建一個方法:
// 請求遠程服務,獲得用戶資訊
func GetUserInfo(tracer opentracing.Tracer, parentSpan opentracing.Span) {
// 繼承上下文關係,創建子 span
childSpan := tracer.StartSpan(
"B",
opentracing.ChildOf(parentSpan.Context()),
)
url := "//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 函數改成:
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()
}
完整程式碼可參考://github.com/whuanle/DistributedTracingGo/issues/1
Web 服務端
服務端我們使用 gin 來搭建。
新建一個 go 項目,在 main.go 目錄中,執行 go get -u github.com/gin-gonic/gin
。
創建一個函數,該函數可以從創建一個 tracer,並且繼承其它進程傳遞過來的上下文資訊。
// 從上下文中解析並創建一個新的 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: "//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 上下文,我們需要通過中間件來解析了處理一些細節。
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 服務:
func GetUserInfo(ctx *gin.Context) {
userName := ctx.Param("username")
fmt.Println("收到請求,用戶名稱為:", userName)
ctx.String(http.StatusOK, "他的部落格是 //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")
}
完整程式碼可參考://github.com/whuanle/DistributedTracingGo/issues/2
分別啟動 webserver、client,會發現列印日誌。並且打開 jaerger ui 介面,會出現相關的追蹤資訊。
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 已經規定了所有的 Tag 類型,所以我們只需要調用 ext.xxx.Set()
設置即可。
前面寫示例的時候忘記把日誌也加一下了。。。日誌其實很簡單的,通過 span 對象調用函數即可設置。
示例(在中間件裡面加一下):
startSpan.LogFields(
log.String("event", "soft error"),
log.String("type", "cache timeout"),
log.Int("waited.millis", 1500))
ref 就是多個 span 之間的關係。span 可以是跨進程的,也可以是一個進程內的不同函數中的。
其中 span 的依賴關係表示示例:
"references": [
{
"refType": "CHILD_OF",
"traceID": "33ba35e7cc40172c",
"spanID": "1c7826fa185d1107"
}]
spanID 為其依賴的父 span。
可以看下面這張圖。
一個進程中的 tracer 可以包裝一些程式碼和操作,為多個 span 生成一些資訊,或創建父子關係。
而 遠程請求中傳遞的是 SpanContext,傳遞後,遠程服務也創建新的 tracer,然後從 SpanContext 生成 span 依賴關係。
子 span 中,其 reference 列表中,會帶有 父 span 的 span id。
關於 Jaeger Client Go 的文章到此完畢,轉 Go 沒多久,大家可以互相交流喲。