如何在 Go 服務中做鏈路追蹤
在 Java 中解決這個問題比較簡單,可以使用 MDC,在一個進程內共享一個請求的 RequestId。
下面的代碼基於 gin 框架來實現。
- 使用全局 map 來實現
使用 map 方案需要在全局維護一個 map,在一個請求進來的時候,會爲每一個請求生成 RequestId,然後在每次在打印日誌的時候,從這個 Map 中通過 goid 獲取到 RequestId,打印到日誌中。
代碼的實現很簡單:
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) // 打印日誌
}
這樣的實現很簡單,但是問題也很多。
第一個問題就是,在 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 也不是一種常規的做法,一般要通過 hack 的方式來獲取,這種做法已經不推薦了。而且這個全局的 map 爲了併發安全,在實際的使用中,可以還需要用到鎖,在高併發的情況下必然會影響性能。
在每個請求結束的時候,還需要手動的把 requestId 從 map 中刪除,否則就會造成內存泄漏。
總的來說,使用 map 這種方式來實現並不是很好。
- 使用 Context 來實現
在傳遞 RequestId 的場景中,同樣也可以使用 Context 來實現,使用 Context 好處很明顯,Context 生命週期與請求相同,不需要手動銷燬。而且 Context 是每個請求獨享的,也不用擔心併發安全的問題,Context 還可以在 goroutine 之間傳遞。
使用 Context 實現的代碼如下:
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,而且不用擔心內存泄漏和併發安全。
但是使用 Context 也有個問題就是需要每次傳遞 Context,很多人還不習慣使用這種方式。其實 Go 官方早就推薦使用 Context 了,通常會把 Context 作爲函數的第一個參數。如果函數使用結構體作爲參數,也可以直接把 Context 作爲結構體的一個字段。
Context 除了使用可以同來傳遞 RequestId 之外,還可以用來控制 goroutine 的生命週期,這些內容在之前的 Context 文章中詳細說明了,感興趣的可以去看看。
- 小結
獲取 goroutine id 這種方式應該被拋棄,而是應該使用 Context, Go 官方也早就推薦使用這種方式,在上文中,我們使用 Context 來傳遞 RequestId,除此之外還可以用來傳遞單個請求範圍的值,比如認證的 token 之類的,應該習慣在代碼中使用 Context。
[1] https://blog.golang.org/context
文 / Rayjun
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/q77CX4s08FfL0XQx2BPC8Q