详解如何在Go服务中做链路追踪
使用Go语言开发微服务的时候,需要追踪每一个请求的访问链路,这块在Go中目前没有很好的解决方案。
在Java中解决这个问题比较简单,可以使用MDC,在一个进程内共享一个请求的RequestId。
在Go中实现链路追踪有两种思路:一种是在项目中使用一个全局的map,key是goroutine的唯一Id,value是RequestId,另一种思路可以使用context.Context来实现。
下面的代码基于gin框架来实现。
使用map方案需要在全局维护一个map,在一个请求进来的时候,会为每一个请求生成RequestId,然后在每次在打印日志的时候,从这个Map中通过goid获取到RequestId,打印到日志中。
代码的实现很简单:
varrequestIdMap=make(map[int64]string)//全局的Mapfuncmain(){r:=gin.Default()r.Use(Logger())//使用中间件r.GET("/index",func(c*gin.Context){Info("maingoroutine")//打印日志c.JSON(200,gin.H{"message":"index",})})r.Run()}funcLogger()gin.HandlerFunc{returnfunc(c*gin.Context){requestIdMap[goid.Get()]=uuid.New().String()//在日志中间件中为每个请求设定c.Next()}}funcInfo(msgstring){now:=time.Now()nowStr:=now.Format("2006-01-0215:04:05")fmt.Printf("%s[%s]%s\n",nowStr,requestIdMap[goid.Get()],msg)//打印日志}
这样的实现很简单,但是问题也很多。
第一个问题就是,在Go程序中,一次请求可能会涉及到多个goroutine,用这种方式很难在多个gotoutine之间传递RequestId。
在下面的代码中,如果新启动了一个goroutine,就会导致日志中获取不到RequestId:
funcmain(){r:=gin.Default()r.Use(Logger())r.GET("/index",func(c*gin.Context){Info("maingoroutine")gofunc(){//这里新启动了一个一个goroutineInfo("goroutine1")}()c.JSON(200,gin.H{"message":"index",})})r.Run()}
获取goroutineid也不是一种常规的做法,一般要通过hack的方式来获取,这种做法已经不推荐了。而且这个全局的map为了并发安全,在实际的使用中,可以还需要用到锁,在高并发的情况下必然会影响性能。
在每个请求结束的时候,还需要手动的把requestId从map中删除,否则就会造成内存泄漏。
总的来说,使用map这种方式来实现并不是很好。
在上面的代码中,我们使用一个hack的方式去获取goroutineid,这种方式早就不推荐使用,更推荐使用Context,关于Context内容,可以去看我之前的文章,在这里就不多说了。
在传递RequestId的场景中,同样也可以使用Context来实现,使用Context好处很明显,Context生命周期与请求相同,不需要手动销毁。而且Context是每个请求独享的,也不用担心并发安全的问题,Context还可以在goroutine之间传递。
使用Context实现的代码如下:
funcmain(){r:=gin.Default()r.Use(Logger())r.GET("/index",func(c*gin.Context){ctx,_:=c.Get("ctx")Info(ctx.(context.Context),"maingoroutine")gofunc(){Info(ctx.(context.Context),"goroutine1")}()c.JSON(200,gin.H{"message":"index",})})r.Run()}funcLogger()gin.HandlerFunc{returnfunc(c*gin.Context){valueCtx:=context.WithValue(c.Request.Context(),"RequestId",uuid.New().String())c.Set("ctx",valueCtx)c.Next()}}funcInfo(ctxcontext.Context,msgstring){now:=time.Now()nowStr:=now.Format("2006-01-0215: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文章中详细说明了,感兴趣的可以去看看。
获取goroutineid这种方式应该被抛弃,而是应该使用Context,Go官方也早就推荐使用这种方式,在上文中,我们使用Context来传递RequestId,除此之外还可以用来传递单个请求范围的值,比如认证的token之类的,应该习惯在代码中使用Context。
到此这篇关于详解如何在Go服务中做链路追踪的文章就介绍到这了,更多相关服务中做链路追踪内容请搜索完美下载以前的文章或继续浏览下面的相关文章希望大家以后多多支持完美下载!