如何在Go 服務中做鏈路追蹤

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"使用 Go 語言開發微服務的時候,需要追蹤每一個請求的訪問鏈路,這塊在 Go 中目前沒有很好的解決方案。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 Java 中解決這個問題比較簡單,可以使用 MDC,在一個進程內共享一個請求的 RequestId。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 Go 中實現鏈路追蹤有兩種思路:一種是在項目中使用一個全局的 map, key 是 goroutine 的唯一 Id,value 是 RequestId,另一種思路可以使用 context.Context 來實現。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面的代碼基於 gin 框架來實現。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"1. 使用全局 map 來實現","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"使用 map 方案需要在全局維護一個 map,在一個請求進來的時候,會爲每一個請求生成 RequestId,然後在每次在打印日誌的時候,從這個 Map 中通過 goid 獲取到 RequestId,打印到日誌中。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"代碼的實現很簡單:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"var requestIdMap = make(map[int64]string) // 全局的 Map\n\nfunc main() {\n r := gin.Default()\n r.Use(Logger()) // 使用中間件\n\n r.GET(\"/index\", func(c *gin.Context) {\n Info(\"main goroutine\") // 打印日誌\n\n c.JSON(200, gin.H{\n \"message\": \"index\",\n })\n })\n r.Run()\n}\n\nfunc Logger() gin.HandlerFunc {\n return func(c *gin.Context) {\n requestIdMap[goid.Get()] = uuid.New().String() // 在日誌中間件中爲每個請求設定\n c.Next()\n }\n}\n\nfunc Info(msg string) {\n now := time.Now()\n nowStr := now.Format(\"2006-01-02 15:04:05\")\n fmt.Printf(\"%s [%s] %s\\n\", nowStr, requestIdMap[goid.Get()], msg) // 打印日誌\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這樣的實現很簡單,但是問題也很多。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第一個問題就是,在 Go 程序中,一次請求可能會涉及到多個 goroutine,用這種方式很難在多個 gotoutine 之間傳遞 RequestId。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在下面的代碼中,如果新啓動了一個 goroutine,就會導致日誌中獲取不到 RequestId:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"func main() {\n r := gin.Default()\n r.Use(Logger())\n\n r.GET(\"/index\", func(c *gin.Context) {\n Info(\"main goroutine\")\n\n go func() { // 這裏新啓動了一個一個 goroutine\n Info(\"goroutine1\")\n }()\n\n c.JSON(200, gin.H{\n \"message\": \"index\",\n })\n })\n r.Run()\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"獲取 goroutine id 也不是一種常規的做法,一般要通過 hack 的方式來獲取,這種做法已經不推薦了。而且這個全局的 map 爲了併發安全,在實際的使用中,可以還需要用到鎖,在高併發的情況下必然會影響性能。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在每個請求結束的時候,還需要手動的把 requestId 從 map 中刪除,否則就會造成內存泄漏。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"總的來說,使用 map 這種方式來實現並不是很好。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"2. 使用 Context 來實現","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在上面的代碼中,我們使用一個 hack 的方式去獲取 goroutine id,這種方式早就不推薦使用,更推薦使用 Context,關於 Context 內容,可以去看我之前的文章,在這裏就不多說了。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在傳遞 RequestId 的場景中,同樣也可以使用 Context 來實現,使用 Context 好處很明顯,Context 生命週期與請求相同,不需要手動銷燬。而且Context 是每個請求獨享的,也不用擔心併發安全的問題,Context 還可以在 goroutine 之間傳遞。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"使用 Context 實現的代碼如下:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"func main() {\n r := gin.Default()\n r.Use(Logger())\n\n r.GET(\"/index\", func(c *gin.Context) {\n\n ctx, _ := c.Get(\"ctx\")\n\n Info(ctx.(context.Context) , \"main goroutine\")\n\n go func() {\n Info(ctx.(context.Context), \"goroutine1\")\n }()\n\n c.JSON(200, gin.H{\n \"message\": \"index\",\n })\n })\n r.Run()\n}\n\nfunc Logger() gin.HandlerFunc {\n return func(c *gin.Context) {\n valueCtx := context.WithValue(c.Request.Context(), \"RequestId\", uuid.New().String())\n c.Set(\"ctx\", valueCtx)\n c.Next()\n }\n}\n\nfunc Info(ctx context.Context, msg string) {\n now := time.Now()\n nowStr := now.Format(\"2006-01-02 15:04:05\")\n fmt.Printf(\"%s [%s] %s\\n\", nowStr, ctx.Value(\"RequestId\"), msg)\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這樣在一個請求中,所有的 gotroutine 都可以獲取到同一個 RequestId,而且不用擔心內存泄漏和併發安全。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但是使用 Context 也有個問題就是需要每次傳遞 Context,很多人還不習慣使用這種方式。其實 Go 官方早就推薦使用 Context了,通常會把 Context 作爲函數的第一個參數。如果函數使用結構體作爲參數,也可以直接把 Context 作爲結構體的一個字段。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Context 除了使用可以同來傳遞 RequestId 之外,還可以用來控制 goroutine 的生命週期,這些內容在之前的 Context 文章中詳細說明了,感興趣的可以去看看。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"3. 小結","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"獲取 goroutine id 這種方式應該被拋棄,而是應該使用 Context, Go 官方也早就推薦使用這種方式,在上文中,我們使用 Context 來傳遞 RequestId,除此之外還可以用來傳遞單個請求範圍的值,比如認證的 token 之類的,應該習慣在代碼中使用 Context。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"[1] ","attrs":{}},{"type":"link","attrs":{"href":"https://blog.golang.org/context","title":"","type":null},"content":[{"type":"text","text":"https://blog.golang.org/context","attrs":{}}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#FF7021","name":"orange"}}],"text":"文 / Rayjun","attrs":{}}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章