go-zero 鏈路追蹤

2022-04-21 11:39 更新

序言

微服務(wù)架構(gòu)中,調(diào)用鏈可能很漫長,從 http 到 rpc ,又從 rpc 到 http 。而開發(fā)者想了解每個(gè)環(huán)節(jié)的調(diào)用情況及性能,最佳方案就是 全鏈路跟蹤。

追蹤的方法就是在一個(gè)請求開始時(shí)生成一個(gè)自己的 spanID ,隨著整個(gè)請求鏈路傳下去。我們則通過這個(gè) spanID 查看整個(gè)鏈路的情況和性能問題。

下面來看看 go-zero 的鏈路實(shí)現(xiàn)。

代碼結(jié)構(gòu)

  • spancontext :保存鏈路的上下文信息「traceid,spanid,或者是其他想要傳遞的內(nèi)容」
  • span :鏈路中的一個(gè)操作,存儲時(shí)間和某些信息
  • propagator : trace 傳播下游的操作「抽取,注入」
  • noop :實(shí)現(xiàn)了空的 tracer 實(shí)現(xiàn)


概念

SpanContext

在介紹 span 之前,先引入 context 。SpanContext 保存了分布式追蹤的上下文信息,包括 Trace id,Span id 以及其它需要傳遞到下游的內(nèi)容。OpenTracing 的實(shí)現(xiàn)需要將 SpanContext 通過某種協(xié)議 進(jìn)行傳遞,以將不同進(jìn)程中的 Span 關(guān)聯(lián)到同一個(gè) Trace 上。對于 HTTP 請求來說,SpanContext 一般是采用 HTTP header 進(jìn)行傳遞的。

下面是 go-zero 默認(rèn)實(shí)現(xiàn)的 spanContext

type spanContext struct {
    traceId string      // TraceID 表示tracer的全局唯一ID
    spanId  string      // SpanId 標(biāo)示單個(gè)trace中某一個(gè)span的唯一ID,在trace中唯一
}

同時(shí)開發(fā)者也可以實(shí)現(xiàn) SpanContext 提供的接口方法,實(shí)現(xiàn)自己的上下文信息傳遞:

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

Span

一個(gè) REST 調(diào)用或者數(shù)據(jù)庫操作等,都可以作為一個(gè) span 。 span 是分布式追蹤的最小跟蹤單位,一個(gè) Trace 由多段 Span 組成。追蹤信息包含如下信息:

type Span struct {
    ctx           spanContext       // 傳遞的上下文
    serviceName   string            // 服務(wù)名 
    operationName string            // 操作
    startTime     time.Time         // 開始時(shí)間戳
    flag          string            // 標(biāo)記開啟trace是 server 還是 client
    children      int               // 本 span fork出來的 childsnums
}

從 span 的定義結(jié)構(gòu)來看:在微服務(wù)中, 這就是一個(gè)完整的子調(diào)用過程,有調(diào)用開始 startTime ,有標(biāo)記自己唯一屬性的上下文結(jié)構(gòu) spanContext 以及 fork 的子節(jié)點(diǎn)數(shù)。

實(shí)例應(yīng)用

在 go-zero 中http,rpc中已經(jīng)作為內(nèi)置中間件集成。我們以 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(),
        // 標(biāo)記為server
        flag:          serverFlag,
    }
}
  1. 將 header -> carrier,獲取 header 中的traceId等信息
  2. 開啟一個(gè)新的 span,并把「traceId,spanId」封裝在context中
  3. 從上述的 carrier「也就是header」獲取traceId,spanId
    • 看header中是否設(shè)置
    • 如果沒有設(shè)置,則隨機(jī)生成返回
  4. 從 request 中產(chǎn)生新的ctx,并將相應(yīng)的信息封裝在 ctx 中,返回
  5. 從上述的 context,拷貝一份到當(dāng)前的 request


這樣就實(shí)現(xiàn)了 span 的信息隨著 request 傳遞到下游服務(wù)。

RPC

在 rpc 中存在 client, server ,所以從 tracing 上也有 clientTracing, serverTracing 。 serveTracing 的邏輯基本與 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
}

  • 獲取上游帶下來的 span 上下文信息
  • 從獲取的 span 中創(chuàng)建新的 ctx,span「繼承父span的traceId」
  • 將生成 span 的data加入ctx,傳遞到下一個(gè)中間件,流至下游

總結(jié)

go-zero 通過攔截請求獲取鏈路traceID,然后在中間件函數(shù)入口會分配一個(gè)根Span,然后在后續(xù)操作中會分裂出子Span,每個(gè)span都有自己的具體的標(biāo)識,F(xiàn)insh之后就會匯集在鏈路追蹤系統(tǒng)中。開發(fā)者可以通過 ELK 工具追蹤 traceID ,看到整個(gè)調(diào)用鏈。

同時(shí) go-zero 并沒有提供整套 trace 鏈路方案,開發(fā)者可以封裝 go-zero 已有的 span 結(jié)構(gòu),做自己的上報(bào)系統(tǒng),接入 jaeger, zipkin 等鏈路追蹤工具。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號