如何在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":{}}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章