首頁 > 軟體

Jaeger Client Go入門並實現鏈路追蹤

2022-03-31 13:03:06

Jaeger

OpenTracing 是開放式分散式追蹤規範,OpenTracing API 是一致,可表達,與供應商無關的API,用於分散式跟蹤和上下文傳播。

OpenTracing 的使用者端庫以及規範,可以到 Github 中檢視:https://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,通過 typeparam 兩個欄位來設定取樣器。

為什麼要設定取樣器?因為服務中的請求千千萬萬,如果每個請求都要記錄追蹤資訊並行送到 Jaeger 後端,那麼面對高並行時,記錄鏈路追蹤以及推播追蹤資訊消耗的效能就不可忽視,會對系統帶來較大的影響。當我們設定 sampler 後,jaeger 會根據當前設定的取樣策略做出取樣行為。

詳細可以參考:https://www.jaegertracing.io/docs/1.22/sampling/

jaegercfg.SamplerConfig 結構體中的欄位 Param 是設定取樣率或速率,要根據 Type 而定。

下面對其關係進行說明:

TypeParam說明
"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: "http://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: "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)。

這時,我們可以抽象程式碼程式碼範例:

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 需要依賴多個服務才能完成任務,每個服務可能是一個程序,也可能是一個程序中的另一個函數。這個要看你程式碼是怎麼寫的。後面會詳細說一下如何定義這種關係,現在大概瞭解一下即可。

怎麼調、怎麼傳

如果有了解過 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 := "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 函數改成:

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

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: "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 上下文,我們需要通過中介軟體來解析了處理一些細節。

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, "他的部落格是 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,會發現列印紀錄檔。並且開啟 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入門並實現鏈路追蹤的文章就介紹到這了。希望對大家的學習有所幫助,也希望大家多多支援it145.com。


IT145.com E-mail:sddin#qq.com