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