詳解如何在Go服務(wù)中做鏈路追蹤
使用 Go 語言開發(fā)微服務(wù)的時(shí)候,需要追蹤每一個(gè)請(qǐng)求的訪問鏈路,這塊在 Go 中目前沒有很好的解決方案。
在 Java 中解決這個(gè)問題比較簡(jiǎn)單,可以使用 MDC,在一個(gè)進(jìn)程內(nèi)共享一個(gè)請(qǐng)求的 RequestId。
在 Go 中實(shí)現(xiàn)鏈路追蹤有兩種思路:一種是在項(xiàng)目中使用一個(gè)全局的 map, key 是 goroutine 的唯一 Id,value 是 RequestId,另一種思路可以使用 context.Context 來實(shí)現(xiàn)。
下面的代碼基于 gin 框架來實(shí)現(xiàn)。
1. 使用全局 map 來實(shí)現(xiàn)
使用 map 方案需要在全局維護(hù)一個(gè) map,在一個(gè)請(qǐng)求進(jìn)來的時(shí)候,會(huì)為每一個(gè)請(qǐng)求生成 RequestId,然后在每次在打印日志的時(shí)候,從這個(gè) Map 中通過 goid 獲取到 RequestId,打印到日志中。
代碼的實(shí)現(xiàn)很簡(jiǎn)單:
var requestIdMap = make(map[int64]string) // 全局的 Map
func main() {
r := gin.Default()
r.Use(Logger()) // 使用中間件
r.GET("/index", func(c *gin.Context) {
Info("main goroutine") // 打印日志
c.JSON(200, gin.H{
"message": "index",
})
})
r.Run()
}
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
requestIdMap[goid.Get()] = uuid.New().String() // 在日志中間件中為每個(gè)請(qǐng)求設(shè)定
c.Next()
}
}
func Info(msg string) {
now := time.Now()
nowStr := now.Format("2006-01-02 15:04:05")
fmt.Printf("%s [%s] %s\n", nowStr, requestIdMap[goid.Get()], msg) // 打印日志
}
這樣的實(shí)現(xiàn)很簡(jiǎn)單,但是問題也很多。
第一個(gè)問題就是,在 Go 程序中,一次請(qǐng)求可能會(huì)涉及到多個(gè) goroutine,用這種方式很難在多個(gè) gotoutine 之間傳遞 RequestId。
在下面的代碼中,如果新啟動(dòng)了一個(gè) goroutine,就會(huì)導(dǎo)致日志中獲取不到 RequestId:
func main() {
r := gin.Default()
r.Use(Logger())
r.GET("/index", func(c *gin.Context) {
Info("main goroutine")
go func() { // 這里新啟動(dòng)了一個(gè)一個(gè) goroutine
Info("goroutine1")
}()
c.JSON(200, gin.H{
"message": "index",
})
})
r.Run()
}
獲取 goroutine id 也不是一種常規(guī)的做法,一般要通過 hack 的方式來獲取,這種做法已經(jīng)不推薦了。而且這個(gè)全局的 map 為了并發(fā)安全,在實(shí)際的使用中,可以還需要用到鎖,在高并發(fā)的情況下必然會(huì)影響性能。
在每個(gè)請(qǐng)求結(jié)束的時(shí)候,還需要手動(dòng)的把 requestId 從 map 中刪除,否則就會(huì)造成內(nèi)存泄漏。
總的來說,使用 map 這種方式來實(shí)現(xiàn)并不是很好。
2. 使用 Context 來實(shí)現(xiàn)
在上面的代碼中,我們使用一個(gè) hack 的方式去獲取 goroutine id,這種方式早就不推薦使用,更推薦使用 Context,關(guān)于 Context 內(nèi)容,可以去看我之前的文章,在這里就不多說了。
在傳遞 RequestId 的場(chǎng)景中,同樣也可以使用 Context 來實(shí)現(xiàn),使用 Context 好處很明顯,Context 生命周期與請(qǐng)求相同,不需要手動(dòng)銷毀。而且Context 是每個(gè)請(qǐng)求獨(dú)享的,也不用擔(dān)心并發(fā)安全的問題,Context 還可以在 goroutine 之間傳遞。
使用 Context 實(shí)現(xiàn)的代碼如下:
func main() {
r := gin.Default()
r.Use(Logger())
r.GET("/index", func(c *gin.Context) {
ctx, _ := c.Get("ctx")
Info(ctx.(context.Context) , "main goroutine")
go func() {
Info(ctx.(context.Context), "goroutine1")
}()
c.JSON(200, gin.H{
"message": "index",
})
})
r.Run()
}
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
valueCtx := context.WithValue(c.Request.Context(), "RequestId", uuid.New().String())
c.Set("ctx", valueCtx)
c.Next()
}
}
func Info(ctx context.Context, msg string) {
now := time.Now()
nowStr := now.Format("2006-01-02 15:04:05")
fmt.Printf("%s [%s] %s\n", nowStr, ctx.Value("RequestId"), msg)
}
這樣在一個(gè)請(qǐng)求中,所有的 gotroutine 都可以獲取到同一個(gè) RequestId,而且不用擔(dān)心內(nèi)存泄漏和并發(fā)安全。
但是使用 Context 也有個(gè)問題就是需要每次傳遞 Context,很多人還不習(xí)慣使用這種方式。其實(shí) Go 官方早就推薦使用 Context了,通常會(huì)把 Context 作為函數(shù)的第一個(gè)參數(shù)。如果函數(shù)使用結(jié)構(gòu)體作為參數(shù),也可以直接把 Context 作為結(jié)構(gòu)體的一個(gè)字段。
Context 除了使用可以同來傳遞 RequestId 之外,還可以用來控制 goroutine 的生命周期,這些內(nèi)容在之前的 Context 文章中詳細(xì)說明了,感興趣的可以去看看。
3. 小結(jié)
獲取 goroutine id 這種方式應(yīng)該被拋棄,而是應(yīng)該使用 Context, Go 官方也早就推薦使用這種方式,在上文中,我們使用 Context 來傳遞 RequestId,除此之外還可以用來傳遞單個(gè)請(qǐng)求范圍的值,比如認(rèn)證的 token 之類的,應(yīng)該習(xí)慣在代碼中使用 Context。
[1] https://blog.golang.org/context
到此這篇關(guān)于詳解如何在Go 服務(wù)中做鏈路追蹤的文章就介紹到這了,更多相關(guān)Go 服務(wù)中做鏈路追蹤內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Go語言類型內(nèi)嵌和結(jié)構(gòu)體內(nèi)嵌的具體使用
本文主要介紹了Go語言類型內(nèi)嵌和結(jié)構(gòu)體內(nèi)嵌的具體使用,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-04-04
Go語言標(biāo)準(zhǔn)輸入輸出庫的基本使用教程
輸入輸出在任何一門語言中都必須提供的一個(gè)功能,下面這篇文章主要給大家介紹了關(guān)于Go語言標(biāo)準(zhǔn)輸入輸出庫的基本使用,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-02-02
Golang中空的切片轉(zhuǎn)化成 JSON 后變?yōu)?nbsp;null 問題的解決方案
在 Golang 中,經(jīng)常需要將其他類型(例如 slice、map、struct 等類型)的數(shù)據(jù)轉(zhuǎn)化為 JSON 格式,有時(shí)候轉(zhuǎn)化的結(jié)果并不是預(yù)期中的,例如將一個(gè)空的切片轉(zhuǎn)化為 JSON 時(shí),會(huì)變成"null",所以本文將給大家介紹一下解決方法,需要的朋友可以參考下2023-09-09
idea搭建go環(huán)境實(shí)現(xiàn)go語言開發(fā)
這篇文章主要給大家介紹了關(guān)于idea搭建go環(huán)境實(shí)現(xiàn)go語言開發(fā)的相關(guān)資料,文中通過圖文介紹以及代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用go具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2024-01-01

