乐趣区

关于golang:gozero-是如何追踪你的请求链路的

go-zero 是如何追踪你的申请链路

微服务架构中,调用链可能很漫长,从 httprpc,又从 rpchttp。而开发者想理解每个环节的调用状况及性能,最佳计划就是 全链路跟踪

追踪的办法就是在一个申请开始时生成一个本人的 spanID,随着整个申请链路传下去。咱们则通过这个 spanID 查看整个链路的状况和性能问题。

上面来看看 go-zero 的链路实现。

代码构造

  • spancontext:保留链路的上下文信息「traceid,spanid,或者是其余想要传递的内容」
  • span:链路中的一个操作,存储工夫和某些信息
  • propagator:trace 流传上游的操作「抽取,注入」
  • noop:实现了空的 tracer 实现

概念

SpanContext

在介绍 span 之前,先引入 context。SpanContext 保留了分布式追踪的上下文信息,包含 Trace id,Span id 以及其它须要传递到上游的内容。OpenTracing 的实现须要将 SpanContext 通过某种协定 进行传递,以将不同过程中的 Span 关联到同一个 Trace 上。对于 HTTP 申请来说,SpanContext 个别是采纳 HTTP header 进行传递的。

上面是 go-zero 默认实现的 spanContext

type spanContext struct {
    traceId string      // TraceID 示意 tracer 的全局惟一 ID
    spanId  string      // SpanId 标示单个 trace 中某一个 span 的惟一 ID,在 trace 中惟一
}

同时开发者也能够实现 SpanContext 提供的接口办法,实现本人的上下文信息传递:

type SpanContext interface {TraceId() string                        // get TraceId
    SpanId() string                         // get SpanId
    Visit(fn func(key, val string) bool)    // 自定义操作 TraceId,SpanId
}

Span

一个 REST 调用或者数据库操作等,都能够作为一个 spanspan 是分布式追踪的最小跟踪单位,一个 Trace 由多段 Span 组成。追踪信息蕴含如下信息:

type Span struct {
    ctx           spanContext       // 传递的上下文
    serviceName   string            // 服务名 
    operationName string            // 操作
    startTime     time.Time         // 开始工夫戳
    flag          string            // 标记开启 trace 是 server 还是 client
    children      int               // 本 span fork 进去的 childsnums
}

span 的定义构造来看:在微服务中,这就是一个残缺的子调用过程,有调用开始 startTime,有标记本人惟一属性的上下文构造 spanContext 以及 fork 的子节点数。

实例利用

go-zero 中 http,rpc 中曾经作为内置中间件集成。咱们以 http,rpc 中,看看 tracing 是怎么应用的:

HTTP

func TracingHandler(next http.Handler) http.Handler {return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // **1**
        carrier, err := trace.Extract(trace.HttpFormat, r.Header)
        // ErrInvalidCarrier means no trace id was set in http header
        if err != nil && err != trace.ErrInvalidCarrier {logx.Error(err)
        }

        // **2**
        ctx, span := trace.StartServerSpan(r.Context(), carrier, sysx.Hostname(), r.RequestURI)
        defer span.Finish()
        // **5**
        r = r.WithContext(ctx)

        next.ServeHTTP(w, r)
    })
}

func StartServerSpan(ctx context.Context, carrier Carrier, serviceName, operationName string) (context.Context, tracespec.Trace) {span := newServerSpan(carrier, serviceName, operationName)
    // **4**
    return context.WithValue(ctx, tracespec.TracingKey, span), span
}

func newServerSpan(carrier Carrier, serviceName, operationName string) tracespec.Trace {
    // **3**
    traceId := stringx.TakeWithPriority(func() string {
        if carrier != nil {return carrier.Get(traceIdKey)
        }
        return ""
    }, func() string {return stringx.RandId()
    })
    spanId := stringx.TakeWithPriority(func() string {
        if carrier != nil {return carrier.Get(spanIdKey)
        }
        return ""
    }, func() string {return initSpanId})

    return &Span{
        ctx: spanContext{
            traceId: traceId,
            spanId:  spanId,
        },
        serviceName:   serviceName,
        operationName: operationName,
        startTime:     timex.Time(),
        // 标记为 server
        flag:          serverFlag,
    }
}
  1. 将 header -> carrier,获取 header 中的 traceId 等信息
  2. 开启一个新的 span,并把 「traceId,spanId」 封装在 context 中
  3. 从上述的 carrier「也就是 header」获取 traceId,spanId。
    • 看 header 中是否设置
    • 如果没有设置,则随机生成返回
  4. request 中产生新的 ctx,并将相应的信息封装在 ctx 中,返回
  5. 从上述的 context,拷贝一份到以后的 request

这样就实现了 span 的信息随着 request 传递到上游服务。

RPC

在 rpc 中存在 client, server,所以从 tracing 上也有 clientTracing, serverTracingserveTracing 的逻辑根本与 http 的统一,来看看 clientTracing 是怎么应用的?

func TracingInterceptor(ctx context.Context, method string, req, reply interface{},
    cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
    // open clientSpan
    ctx, span := trace.StartClientSpan(ctx, cc.Target(), method)
    defer span.Finish()

    var pairs []string
    span.Visit(func(key, val string) bool {pairs = append(pairs, key, val)
        return true
    })
    // **3** 将 pair 中的 data 以 map 的模式退出 ctx
    ctx = metadata.AppendToOutgoingContext(ctx, pairs...)

    return invoker(ctx, method, req, reply, cc, opts...)
}

func StartClientSpan(ctx context.Context, serviceName, operationName string) (context.Context, tracespec.Trace) {
    // **1**
    if span, ok := ctx.Value(tracespec.TracingKey).(*Span); ok {
        // **2**
        return span.Fork(ctx, serviceName, operationName)
    }

    return ctx, emptyNoopSpan
}
  1. 获取上游带下来的 span 上下文信息
  2. 从获取的 span 中创立新的 ctx,span「继承父 span 的 traceId」
  3. 将生成 span 的 data 退出 ctx,传递到下一个中间件,流至上游

总结

go-zero 通过拦挡申请获取链路 traceID,而后在中间件函数入口会调配一个根 Span,而后在后续操作中会决裂出子 Span,每个 span 都有本人的具体的标识,Finsh 之后就会会集在链路追踪零碎中。

开发者能够通过 ELK 工具追踪 traceID,看到整个调用链。同时 go-zero 并没有提供整套 trace 链路计划,开发者能够封装 go-zero 已有的 span 构造,做本人的上报零碎,接入 jaeger, zipkin 等链路追踪工具。

参考

  • go-zero trace
  • 凋谢分布式追踪(OpenTracing)入门与 Jaeger 实现

我的项目地址:
https://github.com/tal-tech/go-zero

退出移动版