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

hxl發表於2020-11-02

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

追蹤的方法就是在一個請求開始時生成一個自己的 spanID ,隨著整個請求鏈路傳下去。我們則通過這個 spanID 檢視整個鏈路的情況和效能問題。

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

程式碼結構

  • spancontext:儲存鏈路的上下文資訊「traceid,spanid,或者是其他想要傳遞的內容」
  • span:鏈路中的一個操作,儲存時間和某些資訊
  • propagatortrace 傳播下游的操作「抽取,注入」
  • 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 中已經作為內建中介軟體整合。我們以 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 等資訊
  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 並加入我們,專案地址

更多原創文章乾貨分享,請關注公眾號
  • go-zero 是如何追蹤你的請求鏈路?
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章