首页 > golang > golang分布式链路追踪系统-jaeger安装与简单使用
2022
01-15

golang分布式链路追踪系统-jaeger安装与简单使用

简介

jaeger是一个比较有名的分布式链路追踪系统,底层用golang实现,兼容opentracing标准。

    文档地址:docs

    github地址:github

    官网:website

    blog:blog

部署

我们用docker部署,集成整套环境all-in-one,docker地址:https://hub.docker.com/r/jaegertracing/all-in-one

注意: 在 all in one 模式下,jaeger 存储数据使用的是内存,因此重启 dockre 后就看不到之前的数据了。所以,该模式仅用于前期的 demo 或者测试验证,不可在生产环境中使用这种模式部署

直接运行docker命令安装:

docker run -d --name jaeger 
  -e COLLECTOR_ZIPKIN_HTTP_PORT=9411 
  -p 5775:5775/udp 
  -p 6831:6831/udp 
  -p 6832:6832/udp 
  -p 5778:5778 
  -p 16686:16686 
  -p 14268:14268 
  -p 9411:9411 
  jaegertracing/all-in-one:latest

执行完成后,用
命令:docker ps
看看运行起来没,这里看结果已经运行了:

访问jaeger的web界面:
localhost:16686
如果你是远程,这里的localhost可以换成你的服务器ip,或者你配置的域名。

在单体应用中实现Tracing

package main

import (
   "context"
   "fmt"
   "github.com/opentracing/opentracing-go"
   "github.com/uber/jaeger-client-go"
   jaegercfg "github.com/uber/jaeger-client-go/config"
   "time"
)

func main() {
   initJaeger("jager-test-demo")

}

func initJaeger(serviceName string) {
   cfg := jaegercfg.Configuration{
      Sampler: &jaegercfg.SamplerConfig{
         Type:  jaeger.SamplerTypeConst,
         Param: 1,
      },
      Reporter: &jaegercfg.ReporterConfig{
         LogSpans:           true,
         LocalAgentHostPort: "122.51.156.172:6831", // 替换 122.51.156.172
      },
   }

   closer, err := cfg.InitGlobalTracer(
      serviceName,
   )
   if err != nil {
      fmt.Printf("Could not initialize jaeger tracer: %s", err.Error())
      return
   }

   var ctx = context.TODO()
   span1, ctx := opentracing.StartSpanFromContext(ctx, "span_1")
   time.Sleep(time.Second / 2)

   span11, _ := opentracing.StartSpanFromContext(ctx, "span_1-1")
   time.Sleep(time.Second / 2)
   span11.Finish()

   span1.Finish()

   defer closer.Close()
}


go run test.go

然后在去jaeger UI上刷新查看,会出现记录:

可以发现有分层,时间耗时也明显,接口先后调用也很清晰。

image.png

点击上面的 jager-test-demo 进去,可以看到下面的这种调用情况:

image.png

通过Grpc中间件使用

在单体程序中, 父子Span通过context关联, 而context是在内存中的, 显而易见这样的方法在垮应用的场景下是行不通的

垮应用通讯使用的方式通常是"序列化", 在jaeger-client-go库中也是通过类似的操作去传递信息, 它们叫:Tracer.Inject() 与 Tracer.Extract()

其中inject方法支持将span系列化成几种格式:

    Binary: 二进制

    TextMap: key=>value

    HTTPHeaders: Http头, 其实也是key=>value

正好grpc支持传递metadata也是string的key=>value形式, 所以我们就能通过metadata实现在不同应用间传递Span了

这段代码在github上有人实现了: https://github.com/grpc-ecosystem/go-grpc-middleware

题外话:上面的库使用到了grpc的Interceptor, 但grpc不支持多个Interceptor, 
所以当你又使用到了其他中间件(如grpc_retry)的话就能导致冲突. 
同样也可以使用这个库grpc_middleware.ChainUnaryClient解决这个问题.


在grpc服务端的中间件代码如下(已省略错误处理)

package main

import (
   "github.com/uber/jaeger-client-go"
   jaegercfg "github.com/uber/jaeger-client-go/config"
   "google.golang.org/grpc"
)

func main() {
   jcfg := jaegercfg.Configuration{
      Sampler: &jaegercfg.SamplerConfig{
         Type:  "const",
         Param: 1,
      },
      ServiceName: "serviceName",
   }

   report := jaegercfg.ReporterConfig{
      LogSpans:           true,
      LocalAgentHostPort: "locahost:6831",
   }

   reporter, _ := report.NewReporter(serviceName, jaeger.NewNullMetrics(), jaeger.NullLogger)
   tracer, closer, _ = jcfg.NewTracer(
      jaegercfg.Reporter(reporter),
   )

   server := grpc.NewServer(grpc.UnaryInterceptor(grpc_opentracing.UnaryServerInterceptor(grpc_opentracing.WithTracer(tracer))))
}

在grpc客户端的中间件代码如下

conn, err := grpc.Dial(addr, grpc.WithUnaryInterceptor(grpc_opentracing.UnaryClientInterceptor(
   grpc_opentracing.WithTracer(tracer),
)))

现在服务端和客户端之间的调用情况就能被jaeger收集到了.

在业务代码中使用

有时候只监控一个"api"是不够的,还需要监控到程序中的代码片段(如方法),可以这样封装一个方法

package main

import (
   "context"
   "github.com/opentracing/opentracing-go"
   "github.com/opentracing/opentracing-go/ext"
)

func main() {
   //使用demo
   newCtx, finish := tracer.Start("DoSomeThing", ctx)
   err := DoSomeThing(newCtx)
   finish(tracer.SpanWithError(err))
   if err != nil {
      //
   }
}

type SpanOption func(span opentracing.Span)

func SpanWithError(err error) SpanOption {
   return func(span opentracing.Span) {
      if err != nil {
         ext.Error.Set(span, true)
         span.LogFields(tlog.String("event", "error"), tlog.String("msg", err.Error()))
      }
   }
}

// example:
// SpanWithLog(
//    "event", "soft error",
//    "type", "cache timeout",
//    "waited.millis", 1500)
func SpanWithLog(arg ...interface{}) SpanOption {
   return func(span opentracing.Span) {
      span.LogKV(arg...)
   }
}

func Start(tracer opentracing.Tracer, spanName string, ctx context.Context) (newCtx context.Context, finish func(...SpanOption)) {
   if ctx == nil {
      ctx = context.TODO()
   }
   span, newCtx := opentracing.StartSpanFromContextWithTracer(ctx, tracer, spanName,
      opentracing.Tag{Key: string(ext.Component), Value: "func"},
   )

   finish = func(ops ...SpanOption) {
      for _, o := range ops {
         o(span)
      }
      span.Finish()
   }

   return
}


最后能得到一个像这样的结果

image.png

可以看到在服务的调用过程中各个span的时间,这个span可以是一个微服务之间的调用也可以是某个方法的调用。

点开某个span也能看到额外的log信息。

通过Gin中间件中使用

在我的项目中使用http服务作为网关提供给前端使用,那么这个http服务层就是root span而不用关心父span了,编写代码就要简单一些。

封装一个gin中间件就能实现

jaeger.go

package middleware

import (
   "github.com/gin-gonic/gin"
   "github.com/opentracing/opentracing-go"
   "github.com/opentracing/opentracing-go/ext"
   "github.com/uber/jaeger-client-go"
   jaegercfg "github.com/uber/jaeger-client-go/config"
)

const defaultComponentName = "net/http"
const JaegerOpen = 1
const AppName = "gin-api"
const JaegerHostPort = "122.51.156.172:6831"

func OpenTracing(serviceName string) gin.HandlerFunc {
   return func(c *gin.Context) {
      if JaegerOpen == 1 {
         var parentSpan opentracing.Span
         jcfg := jaegercfg.Configuration{
            Sampler: &jaegercfg.SamplerConfig{
               Type:  "const",
               Param: 1,
            },
            ServiceName: AppName,
         }

         report := jaegercfg.ReporterConfig{
            LogSpans:           true,
            LocalAgentHostPort: JaegerHostPort,
            QueueSize:          1000, //发送的Spans个数大于了QueueSize, 多余QueueSize的Spans可能会被丢弃, 可以通过配置 QueueSize:
         }

         reporter, _ := report.NewReporter(serviceName, jaeger.NewNullMetrics(), jaeger.NullLogger)
         tracer, closer, _ := jcfg.NewTracer(
            jaegercfg.Reporter(reporter),
         )

         defer closer.Close()

         spCtx, err := opentracing.GlobalTracer().Extract(opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(c.Request.Header))
         if err != nil {
            parentSpan = tracer.StartSpan(c.Request.URL.Path)
            defer parentSpan.Finish()
         } else {
            parentSpan = opentracing.StartSpan(
               c.Request.URL.Path,
               opentracing.ChildOf(spCtx),
               opentracing.Tag{Key: string(ext.Component), Value: "HTTP"},
               ext.SpanKindRPCServer,
            )
            defer parentSpan.Finish()
         }
         c.Set("Tracer", tracer)
         c.Set("ParentSpanContext", parentSpan.Context())
      }
      c.Next()
   }
}

router.go

Router.Use(middleware.OpenTracing("gin-api"))

如果需要向下层传递context则这样获取context

func Api(gtx *gin.Context) {
   ctx = gtx.Get("ctx").(context.Context)
}


总结

使用trace会入侵部分代码,特别是追踪一个方法,但这是不可避免的。
甚至需要每个方法都需要添加上ctx, 关于这点有兴趣的朋友可以读一下这篇文章: Golang Context 是好的设计吗?
(原文找不到了, 将就看一下)
但其实并不是整个系统的服务都需要追踪,可只针对于重要或者有性能问题的地方进行追踪。


参考

    https://www.jaegertracing.io/docs/1.18/

    https://medium.com/jaegertracing/

    https://wu-sheng.gitbooks.io/opentracing-io/content/

    https://blog.csdn.net/liyunlong41/article/details/87932953


本文》有 0 条评论

留下一个回复