> 文章列表 > Go项目(链路追踪)

Go项目(链路追踪)

Go项目(链路追踪)

文章目录

  • 简介
  • jaeger
    • OpenTracing
    • Demo
  • 集成

简介

  • 分布式链路追踪(Distributed Tracing)技术是为了快速定位分布式微服务系统内的问题而诞生的
  • 因为各微服务之间的调用链可能很复杂冗长,如果出了问题只能通过看日志的方式逐级排查,是非常低效的
  • 如果系统某个地方响应很慢需要优化,也只能逐级测试响应时间,对开发人员很不友好
  • 总之,关乎到的不仅仅是开发、运维,还有测试,DBA,以及老板的工作量,链路追踪技术就可以解决这些问题
  • 常见的链路追踪技术实现
    Go项目(链路追踪)
  • 综合考虑,这里选择使用 jaeger,但是他们都兼容 OpenTracing,切换很方便
    • 注:Opentracing 是一套标准接口,而不是具体实现;大家都兼容了这个接口,就可以在切换技术实现时减少代码改动
    • 后面会详细介绍

jaeger

  • 安装和配置
  • 还是 docker 安装,访问:http://192.168.109.128:16686 (UI)
    docker run \\--rm \\--name jaeger \\-p6831:6831/udp \\-p16686:16686 \\jaegertracing/all-in-one:latest
    
  • 架构图及文档
    Go项目(链路追踪)
  • 概念
    • OpenTelemetry SDK - 为不同语言实现的符合 OpenTracing 标准的 SDK;应用程序通过 API 写入数据,client library 把 trace 信息按照应用程序指定的采样策略传递给 jaeger-agent
    • Jaeger Agent - 它是一个监听在 UDP 端口上接收 span 数据的网络守护进程,它会将数据批量发送给 collector;它被设计成一个基础组件,部署到所有的宿主机上;Agent 将 client library 和 collector 解耦,为 client library 屏蔽了路由和发现 collector 的细节;(埋点)
    • Collector - 接收 jaeger-agent 发送来的数据,然后将数据写入后端存储;Collector 被设计成无状态的组件,因此可以同时运行任意数量的 jaeger-collector
    • Data Store - 后端存储被设计成一个可插拔的组件,支持将数据写入 cassandra、elastic search
    • Query - 接收查询请求,然后从后端存储系统中检索 trace 并通过 UI 进行展示;Query 是无状态的,可以启动多个实例,把它们部署在 nginx 这样的负载均衡器后面
  • 理论部分先了解即可,往往需要在熟练使用的基础上看源码才能深入理解,而且它在使用上屏蔽了很多细节,现在只能是窥豹一斑,不必在此纠结

OpenTracing

  • 因为链路追踪是刚需,所以相关组件会层出不穷,定义一个统一接口是很有必要的
  • 参考文档
  • 主要概念,参考
    • Span:可以被理解为一次方法调用,一个程序块的调用,或者一次RPC/数据库访问;只要是一个具有完整时间周期的程序访问(有请求有响应),都可以被认为是一个 span;可以看出,真正做事的就是这个东西
    • SpanContext:上个 span,下个 span
  • 如图所示,这条调用链
    Go项目(链路追踪)

Demo

  • 根据官方的这段说明,目前已经不推荐使用 jaeger-client,而是使用 OpenTelemetry 作为 client
    Jaeger project recommends OpenTelemetry SDKs for instrumentation, instead of Jaeger's native SDKs that are now deprecated.
    The OpenTracing and OpenCensus projects have merged into a new CNCF project called OpenTelemetry. 
    The Jaeger and OpenTelemetry projects have different goals. OpenTelemetry aims to provide APIs and SDKs in multiple languages to allow applications to export various telemetry data out of the process, to any number of metrics and tracing backends. 
    The Jaeger project is primarily the tracing backend that receives tracing telemetry data and provides processing, aggregation, data mining, and visualizations of that data. The Jaeger client libraries do overlap with OpenTelemetry in functionality. 
    OpenTelemetry natively supports Jaeger as a tracing backend and makes Jaeger native clients unnecessary. For more information please refer to a blog post Jaeger and OpenTelemetry.
    
  • 但是从文档来看目前支持的系统还不完善,Unix 系统中只支持 Ubuntu,还是先用 jaeger-client
    "github.com/opentracing/opentracing-go"
    "github.com/uber/jaeger-client-go"
    jaegercfg "github.com/uber/jaeger-client-go/config"
    
  • 注意在创建容器的时候就不能指定 latest 了
  • 新建追踪连,定义 span
    package mainimport ("github.com/uber/jaeger-client-go"jaegercfg "github.com/uber/jaeger-client-go/config""time"
    )func main() {cfg := jaegercfg.Configuration{Sampler: &jaegercfg.SamplerConfig{Type:  jaeger.SamplerTypeConst,Param: 1,},Reporter: &jaegercfg.ReporterConfig{LogSpans:           true,LocalAgentHostPort: "192.168.109.128:6831", // UI 16686},ServiceName: "vshop",}// 生成一条跟踪连tracer, closer, err := cfg.NewTracer(jaegercfg.Logger(jaeger.StdLogger))if err != nil {panic(err)}defer closer.Close()// 创建一个 spanspan := tracer.StartSpan("go-grpc-order-web")// 业务代码time.Sleep(time.Second)// 结束 spandefer span.Finish()
    }
    
  • 执行成功,可以在 UI 上看到;4433a5c 是 tracerID
    Go项目(链路追踪)
  • 创建嵌套 span,其实就是调用链上各函数之间的 span,有顺序,但也可以说成是嵌套关系
    func main() {cfg := jaegercfg.Configuration{Sampler: &jaegercfg.SamplerConfig{Type:  jaeger.SamplerTypeConst,Param: 1,},Reporter: &jaegercfg.ReporterConfig{LogSpans:           true,LocalAgentHostPort: "192.168.109.128:6831", // UI 16686},ServiceName: "shop",}// 生成一条跟踪连tracer, closer, err := cfg.NewTracer(jaegercfg.Logger(jaeger.StdLogger))if err != nil {panic(err)}defer closer.Close()span_a := tracer.StartSpan("funcA")time.Sleep(time.Second)defer span_a.Finish()span_b := tracer.StartSpan("funcB")time.Sleep(2 * time.Second)defer span_b.Finish()
    }
    

    Go项目(链路追踪)

  • 上面有两个问题,两个 span 不是嵌套关系呀,traceID 不是一个,funcA 用时 3s,也就是包含了 B 的时间
  • 解决方案如下,创建父级 span
    parent := tracer.StartSpan("father")
    span_a := tracer.StartSpan("funcA", opentracing.ChildOf(parent.Context()))
    time.Sleep(time.Second)
    // 不能用 defer,当然,条件是函数逻辑到这里确实执行完毕了;否则A会包含B执行的时间
    span_a.Finish()
    // defer 后的代码会在什么时机执行呢?
    span_b := tracer.StartSpan("funcB", opentracing.ChildOf(parent.Context()))
    time.Sleep(2 * time.Second)
    span_b.Finish()
    // 关闭
    parent.Finish()
    

    Go项目(链路追踪)

  • 父 span 是一条完整的时间线,如果各同级 span 之间有没被包裹的地方,会间隔开
  • 追踪 grpc 调用
    • 使用这个工具,只需要里面的 otgrpc 部分,利用拦截器实现,可以指定 tracer 和 parentSpan,一般放在拨号中
    • 看上面的例子,我们需要用 span 夹住被调用的函数
    • Client 端代码如下
      package mainimport ("context""fmt""github.com/opentracing/opentracing-go""github.com/uber/jaeger-client-go"jaegercfg "github.com/uber/jaeger-client-go/config""google.golang.org/grpc""shop_srvs/order_srv/tests/jaeger/otgrpc""shop_srvs/order_srv/tests/jaeger/proto"
      )func main() {cfg := jaegercfg.Configuration{Sampler: &jaegercfg.SamplerConfig{Type:  jaeger.SamplerTypeConst,Param: 1,},Reporter: &jaegercfg.ReporterConfig{LogSpans:           true,LocalAgentHostPort: "192.168.109.128:6831", // UI 16686},ServiceName: "roy-shop",}// 生成一条跟踪连tracer, closer, err := cfg.NewTracer(jaegercfg.Logger(jaeger.StdLogger))if err != nil {panic(err)}defer closer.Close()// 用opentracing设置为全局 tracer,方便全局使用// 这个包是go语言版的接口标准的定义opentracing.SetGlobalTracer(tracer)defer closer.Close()// 使用拦截器实现traceconn, err := grpc.Dial("127.0.0.1:50051", grpc.WithInsecure(), grpc.WithUnaryInterceptor(otgrpc.OpenTracingClientInterceptor(opentracing.GlobalTracer())))if err != nil {panic(err)}defer conn.Close()c := proto.NewGreeterClient(conn)r, err := c.SayHello(context.Background(), &proto.HelloRequest{Name: "Roy"})if err != nil {panic(err)}fmt.Println(r.Message)
      }
      

      Go项目(链路追踪)
      Go项目(链路追踪)

    • 服务端也要加,现在只能看到 client 端调用的总时间,这包含了网络传输时间

集成

  • 将 jaeger 集成到 web 层,先从商品服务开始
    • 还是放在 go-other-web branch,完整代码分支
    • 放到 go-goods-web 的话需要变基最新代码到 other branch 才能启动项目
  • 步骤很简单
    • 定义追踪 middlewares
    • 更改配置文件(连接 jaeger host)
    • 在 router 里使用
      GoodsRouter := Router.Group("goods").Use(middlewares.Trace())
      
  • 但一般情况下,我们需要知道这条 tracer 上调用的每一个接口所耗费的时间,因为没有设置 span,上述步骤完成后只能看到总时间
    • 我们可以在接口调用前后创建 span,比如这里
      r, err := global.GoodsSrvClient.GoodsList(context.WithValue(context.Background(), "ginContext", ctx), request)
      
    • 但是还有一个问题,我们之前配置了负载均衡 initialize/srv_conn.go,在这里拨号的;那能否这样做呢:加这么一句(把otgrpc也拿过来)
      grpc.WithUnaryInterceptor(otgrpc.OpenTracingClientInterceptor(opentracing.GlobalTracer())),
      
    • 这样做有个问题,链路追踪一定要考虑 span 之间的层级关系,但是在这里拿不到中间件生成的 parentSpan (这里在初始化时就执行了)
    • 而且,对于每个请求,都应该有一个 tracer 和 parentSpan,这里如果在拨号的时候就用 opentracing 设置全局 tracer,不同请求之间是会相互影响的
    • 需要看源码解决了,要在调用接口的时候获取 gin.Context 设置的 tracer 和 parentSpan
      ctx.Set("tracer", tracer)
      ctx.Set("parentSpan", startSpan)
      
    • 每个 grpc 调用都做如下修改,也就是给当前 context 增加一个键值对,把我们的 ginContext 传递进去
      r, err := global.GoodsSrvClient.GoodsList(context.WithValue(context.Background(), "ginContext", ctx), request)
      
    • 然后在 otgrpc/client.go 的 OpenTracingClientInterceptor 加这样一段代码,获取每次接口被请求时中间件生成的 tracer 和 parentSpan,作为本次的 tracer 和父span(覆盖掉拨号时的GlobalTracer)
      ginContext := ctx.Value("ginContext")
      switch ginContext.(type) {
      case *gin.Context:if itracer, ok := ginContext.(*gin.Context).Get("tracer");ok{tracer = itracer.(opentracing.Tracer)}if parentSpan, ok := ginContext.(*gin.Context).Get("parentSpan");ok{parentCtx = parentSpan.(*jaegerClient.Span).Context()}
      }
      
    • 这样就 OK 了,也不需要调用?当然需要,在 srv_conn.go
      grpc.WithUnaryInterceptor(otgrpc.OpenTracingClientInterceptor(opentracing.GlobalTracer())),
      
    • goods-web/api 下面的其他部分也要改哦!当然,如果你想追踪的话;TODO
    • 其他都是商品服务的本地代码,也可以自己从 ctx 把 tracer 拿出来生成 span 追踪
  • 订单服务
    • 把商品服务里的 otgrpc/、写的中间件拿过来,并修改 router,添加 config 字段,修改 nacos 配置
    • 修改 grpc 调用的参数(可以只改你想追踪的方法)
  • 现在看到的这些接口中,基本上都只调用了一个 srv 层的接口,我们在 UI 看到的时间只是总时间
    • 比如新建订单调了 global.OrderSrvClient.CreateOrder,看到的只是 web 接口运行的时间
      Go项目(链路追踪)
    • 想看到在这个父 span 下的 srv 层接口具体执行了多久,它又调了哪些 srv 服务,先看源码理解一下再操作
    • 注:一个 web 接口可能调了多个 srv 接口,这几个 srv 的父span 是相同的,都用的是 middlewares 为这个 web 接口生成的 parentSpan,看到的应该是这样
      Go项目(链路追踪)

源码解析

  • otgrpc 是如何跟踪远程调用的方法的呢?看源码
  • 要想 span 追踪 srv 层的方法,就需要把 web 层准备好的 Span 优雅的放到服务端去,而不改变服务端的代码,这就要用 metadata 的机制(?)
  • OpenTracingClientInterceptor 中可以看到这么一行
    ctx = injectSpanContext(ctx, tracer, clientSpan)
    
  • 源码如下
    func injectSpanContext(ctx context.Context, tracer opentracing.Tracer, clientSpan opentracing.Span) context.Context {md, ok := metadata.FromOutgoingContext(ctx)if !ok {md = metadata.New(nil)} else {md = md.Copy()}mdWriter := metadataReaderWriter{md}err := tracer.Inject(clientSpan.Context(), opentracing.HTTPHeaders, mdWriter)// We have no better place to record an error than the Span itself :-/if err != nil {clientSpan.LogFields(log.String("event", "Tracer.Inject() failed"), log.Error(err))}return metadata.NewOutgoingContext(ctx, md)
    }
    
  • Inject 方法将 clientSpan 注入,接下来就要到服务端操作了

server

  • 服务端集成 otgrpc
  • 订单服务(新建订单),main.go,初始化 jaeger,重点是 NewServer 的时候传递 grpc.UnaryInterceptor
    tracer, closer, err := cfg.NewTracer(jaegercfg.Logger(jaeger.StdLogger))
    if err != nil {panic(err)
    }
    opentracing.SetGlobalTracer(tracer)
    server := grpc.NewServer(grpc.UnaryInterceptor(otgrpc.OpenTracingServerInterceptor(tracer)))
    
  • 看源码,底层调用了 OpenTracingServerInterceptor,其中又调用了 spanContext, err := extractSpanContext(ctx, tracer)
    func extractSpanContext(ctx context.Context, tracer opentracing.Tracer) (opentracing.SpanContext, error) {md, ok := metadata.FromIncomingContext(ctx)if !ok {md = metadata.New(nil)}return tracer.Extract(opentracing.HTTPHeaders, metadataReaderWriter{md})
    }
    
  • 这里的 Extract 和 client 端的 Inject 对应,提取出父span怎么获取的呢?
    serverSpan := tracer.StartSpan(info.FullMethod,ext.RPCServerOption(spanContext),gRPCComponentTag,
    )
    defer serverSpan.Finish()ctx = opentracing.ContextWithSpan(ctx, serverSpan)
    
  • ContextWithSpan 这个方法给 activeSpanKey 设置了 value,就是提取出的 parentSpan
  • 回到新建订单接口,OrderListener 中新增 Ctx context.Context,本地事务中
    parentSpan := opentracing.SpanFromContext(o.Ctx)
    
  • SpanFromContext 这个方法从 ctx 获取上面那个 key 的 value,也就拿到了 parentSpan
    func SpanFromContext(ctx context.Context) Span {val := ctx.Value(activeSpanKey)if sp, ok := val.(Span); ok {return sp}return nil
    }
    
  • OK,接下来就是 server 端本地操作了,在想追踪的地方新建 child span 夹住,例如:
    shopCartSpan := opentracing.GlobalTracer().StartSpan("select_shopcart", opentracing.ChildOf(parentSpan.Context()))
    // local logic
    shopCartSpan.Finish() // 不使用 defer
    
  • 但是运行发现有很多 health check 的 span,从 info.FullMethod 获取它的 span name,在源码中加个判断
  • OK,启动服务访问新建订单接口,可以看到如下结果
    Go项目(链路追踪)
  • 这里 client 的 /Order/CreateOrder 和 server 的 /Order/CreateOrder 重复了,可以不用 server 创建的 serverSpan
  • 空缺部分可能是网络传输或者未追踪部分的执行时间
  • 以上就是 server 端使用 otgrpc 的一个例子,其他都可以模仿实现
    • 和 proto 文件类似,C 和 S 都要用,才能实现web -> srv 的完整追踪
  • 接下来进入可用性相关的阶段:熔断降级