go-zero 是如何追蹤你的請求鏈路的

go-zero 是如何追蹤你的請求鏈路

微服務架構中,調用鏈可能很漫長,從 httprpc ,又從 rpchttp 。而開發者想了解每一個環節的調用狀況及性能,最佳方案就是 全鏈路跟蹤git

追蹤的方法就是在一個請求開始時生成一個本身的 spanID ,隨着整個請求鏈路傳下去。咱們則經過這個 spanID 查看整個鏈路的狀況和性能問題。github

下面來看看 go-zero 的鏈路實現。數據庫

代碼結構

  • spancontext:保存鏈路的上下文信息「traceid,spanid,或者是其餘想要傳遞的內容」
  • span:鏈路中的一個操做,存儲時間和某些信息
  • propagatortrace 傳播下游的操做「抽取,注入」
  • noop:實現了空的 tracer 實現

image.png

概念

SpanContext

在介紹 span 以前,先引入 context 。SpanContext 保存了分佈式追蹤的上下文信息,包括 Trace id,Span id 以及其它須要傳遞到下游的內容。OpenTracing 的實現須要將 SpanContext 經過某種協議 進行傳遞,以將不一樣進程中的 Span 關聯到同一個 Trace 上。對於 HTTP 請求來講,SpanContext 通常是採用 HTTP header 進行傳遞的。架構

下面是 go-zero 默認實現的 spanContextapp

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中已經做爲內置中間件集成。咱們以 httprpc 中,看看 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等信息oop

  2. 開啓一個新的 span,並把「traceId,spanId」封裝在context中

  3. 從上述的 carrier「也就是header」獲取traceId,spanId。

    • 看header中是否設置
    • 若是沒有設置,則隨機生成返回
  4. request 中產生新的ctx,並將相應的信息封裝在 ctx 中,返回

  5. 從上述的 context,拷貝一份到當前的 request

image.png

這樣就實現了 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 等鏈路追蹤工具。

參考

項目地址:
https://github.com/tal-tech/go-zero

相關文章
相關標籤/搜索