詳解如何在Go服務(wù)中做鏈路追蹤
使用 Go 語言開發(fā)微服務(wù)的時候,需要追蹤每一個請求的訪問鏈路,這塊在 Go 中目前沒有很好的解決方案。
在 Java 中解決這個問題比較簡單,可以使用 MDC,在一個進(jìn)程內(nèi)共享一個請求的 RequestId。
在 Go 中實(shí)現(xiàn)鏈路追蹤有兩種思路:一種是在項(xiàng)目中使用一個全局的 map, key 是 goroutine 的唯一 Id,value 是 RequestId,另一種思路可以使用 context.Context 來實(shí)現(xiàn)。
下面的代碼基于 gin 框架來實(shí)現(xiàn)。
1. 使用全局 map 來實(shí)現(xiàn)
使用 map 方案需要在全局維護(hù)一個 map,在一個請求進(jìn)來的時候,會為每一個請求生成 RequestId,然后在每次在打印日志的時候,從這個 Map 中通過 goid 獲取到 RequestId,打印到日志中。
代碼的實(shí)現(xià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() // 在日志中間件中為每個請求設(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)很簡單,但是問題也很多。
第一個問題就是,在 Go 程序中,一次請求可能會涉及到多個 goroutine,用這種方式很難在多個 gotoutine 之間傳遞 RequestId。
在下面的代碼中,如果新啟動了一個 goroutine,就會導(dǎo)致日志中獲取不到 RequestId:
func main() { r := gin.Default() r.Use(Logger()) r.GET("/index", func(c *gin.Context) { Info("main goroutine") go func() { // 這里新啟動了一個一個 goroutine Info("goroutine1") }() c.JSON(200, gin.H{ "message": "index", }) }) r.Run() }
獲取 goroutine id 也不是一種常規(guī)的做法,一般要通過 hack 的方式來獲取,這種做法已經(jīng)不推薦了。而且這個全局的 map 為了并發(fā)安全,在實(shí)際的使用中,可以還需要用到鎖,在高并發(fā)的情況下必然會影響性能。
在每個請求結(jié)束的時候,還需要手動的把 requestId 從 map 中刪除,否則就會造成內(nèi)存泄漏。
總的來說,使用 map 這種方式來實(shí)現(xiàn)并不是很好。
2. 使用 Context 來實(shí)現(xiàn)
在上面的代碼中,我們使用一個 hack 的方式去獲取 goroutine id,這種方式早就不推薦使用,更推薦使用 Context,關(guān)于 Context 內(nèi)容,可以去看我之前的文章,在這里就不多說了。
在傳遞 RequestId 的場景中,同樣也可以使用 Context 來實(shí)現(xiàn),使用 Context 好處很明顯,Context 生命周期與請求相同,不需要手動銷毀。而且Context 是每個請求獨(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) }
這樣在一個請求中,所有的 gotroutine 都可以獲取到同一個 RequestId,而且不用擔(dān)心內(nèi)存泄漏和并發(fā)安全。
但是使用 Context 也有個問題就是需要每次傳遞 Context,很多人還不習(xí)慣使用這種方式。其實(shí) Go 官方早就推薦使用 Context了,通常會把 Context 作為函數(shù)的第一個參數(shù)。如果函數(shù)使用結(jié)構(gòu)體作為參數(shù),也可以直接把 Context 作為結(jié)構(gòu)體的一個字段。
Context 除了使用可以同來傳遞 RequestId 之外,還可以用來控制 goroutine 的生命周期,這些內(nèi)容在之前的 Context 文章中詳細(xì)說明了,感興趣的可以去看看。
3. 小結(jié)
獲取 goroutine id 這種方式應(yīng)該被拋棄,而是應(yīng)該使用 Context, Go 官方也早就推薦使用這種方式,在上文中,我們使用 Context 來傳遞 RequestId,除此之外還可以用來傳遞單個請求范圍的值,比如認(rèn)證的 token 之類的,應(yīng)該習(xí)慣在代碼中使用 Context。
[1] https://blog.golang.org/context
到此這篇關(guān)于詳解如何在Go 服務(wù)中做鏈路追蹤的文章就介紹到這了,更多相關(guān)Go 服務(wù)中做鏈路追蹤內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
使用go實(shí)現(xiàn)刪除sql里面的注釋和字符串功能(demo)
這篇文章主要介紹了使用go實(shí)現(xiàn)刪除sql里面的注釋和字符串功能,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-11-11Go語言實(shí)現(xiàn)UDP版聊天小工具的示例詳解
這篇文章主要為大家詳細(xì)介紹了如何利用Go語言實(shí)現(xiàn)聊天小工具(UDP版),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-03-03Go語言多人聊天室項(xiàng)目實(shí)戰(zhàn)
這篇文章主要為大家詳細(xì)介紹了Go語言多人聊天室項(xiàng)目實(shí)戰(zhàn),實(shí)現(xiàn)單撩或多撩等多種功能,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-08-08Go語言動態(tài)并發(fā)控制sync.WaitGroup的靈活運(yùn)用示例詳解
本文將講解 sync.WaitGroup 的使用方法、原理以及在實(shí)際項(xiàng)目中的應(yīng)用場景,用清晰的代碼示例和詳細(xì)的注釋,助力讀者掌握并發(fā)編程中等待組的使用技巧2023-11-11