Jaeger Client Go 鏈路追蹤|入門詳解
阿新 • • 發佈:2021-03-30
[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 沒多久,大家可以互相交流喲。