Opentracing 鏈路追蹤


在微服務架構的系統中,請求在各服務之間流轉,調用鏈錯綜復雜,一旦出現了問題和異常,很難追查定位,這個時候就需要鏈路追蹤來幫忙了。鏈路追蹤系統能追蹤並記錄請求在系統中的調用順序,調用時間等一系列關鍵信息,從而幫助我們定位異常服務和發現性能瓶頸。

Opentracing

Opentracing是分布式鏈路追蹤的一種規范標准,是CNCF(雲原生計算基金會)下的項目之一。和一般的規范標准不同,Opentracing不是傳輸協議,消息格式層面上的規范標准,而是一種語言層面上的API標准。以Go語言為例,只要某鏈路追蹤系統實現了Opentracing規定的接口(interface),符合Opentracing定義的表現行為,那么就可以說該應用符合Opentracing標准。這意味着開發者只需修改少量的配置代碼,就可以在符合Opentracing標准的鏈路追蹤系統之間自由切換。

github.com/opentracing…

Data Model

在使用Opentracing來實現全鏈路追蹤前,有必要先了解一下它所定義的數據模型。

Span

Span是一條追蹤鏈路中的基本組成要素,一個span表示一個獨立的工作單元,比如可以表示一次函數調用,一次http請求等等。span會記錄如下基本要素:

  • 服務名稱(operation name)
  • 服務的開始時間和結束時間
  • K/V形式的Tags
  • K/V形式的Logs
  • SpanContext
  • References:該span對一個或多個span的引用(通過引用SpanContext)。

Tags

Tags以K/V鍵值對的形式保存用戶自定義標簽,主要用於鏈路追蹤結果的查詢過濾。例如: http.method="GET",http.status_code=200。其中key值必須為字符串,value必須是字符串,布爾型或者數值型。 span中的tag僅自己可見,不會隨着 SpanContext傳遞給后續span。 例如:

span.SetTag("http.method","GET")
span.SetTag("http.status_code",200)

Logs

Logs與tags類似,也是K/V鍵值對形式。與tags不同的是,logs還會記錄寫入logs的時間,因此logs主要用於記錄某些事件發生的時間。logs的key值同樣必須為字符串,但對value類型則沒有限制。例如:

span.LogFields(
    log.String("event", "soft error"),
    log.String("type", "cache timeout"),
    log.Int("waited.millis", 1500),
)

Opentracing列舉了一些慣用的Tags和Logs: github.com/opentracing…

SpanContext

SpanContext攜帶着一些用於跨服務通信的(跨進程)數據,主要包含:

  • 足夠在系統中標識該span的信息,比如:span_id,trace_id
  • Baggage Items,為整條追蹤連保存跨服務(跨進程)的K/V格式的用戶自定義數據。
Baggage Items

Baggage Items與tags類似,也是K/V鍵值對。與tags不同的是:

  • 其key跟value都只能是字符串格式
  • Baggage items不僅當前span可見,其會隨着SpanContext傳遞給后續所有的子span。要小心謹慎的使用baggage items——因為在所有的span中傳遞這些K,V會帶來不小的網絡和CPU開銷。

References

Opentracing定義了兩種引用關系:ChildOfFollowFrom

ChildOf: 父span的執行依賴子span的執行結果時,此時子span對父span的引用關系是ChildOf。比如對於一次RPC調用,服務端的span(子span)與客戶端調用的span(父span)是ChildOf關系。

FollowFrom:父span的執不依賴子span執行結果時,此時子span對父span的引用關系是FollowFromFollowFrom常用於異步調用的表示,例如消息隊列中consumerspan與producerspan之間的關系。

Trace

Trace表示一次完整的追蹤鏈路,trace由一個或多個span組成。下圖示例表示了一個由8個span組成的trace:

        [Span A]  ←←←(the root span)
            |
     +------+------+
     |             |
 [Span B]      [Span C] ←←←(Span C is a `ChildOf` Span A)
     |             |
 [Span D]      +---+-------+
               |           |
           [Span E]    [Span F] >>> [Span G] >>> [Span H]
                                       ↑
                                       ↑
                                       ↑
                         (Span G `FollowsFrom` Span F)
復制代碼

時間軸的展現方式會更容易理解:

––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–> time

 [Span A···················································]
   [Span B··············································]
      [Span D··········································]
    [Span C········································]
         [Span E·······]        [Span F··] [Span G··] [Span H··]
復制代碼

示例來源: github.com/opentracing…

示例

對Opentracing的概念有初步了解后,下面使用Jaeger來演示如何在程序中使用實現鏈路追蹤。

更多更詳細的示例可參考: Opentracing Go Tutorial

Jaeger

Jaeger\ˈyā-gər\ 是Uber開源的分布式追蹤系統,是遵循Opentracing的系統之一,也是CNCF項目。本篇將使用Jaeger來演示如何在系統中引入分布式追蹤。

Quick Start

Jaeger提供了all-in-one鏡像,方便我們快速開始測試:

$ docker run -d --name jaeger \
  -e COLLECTOR_ZIPKIN_HTTP_PORT=9411 \
  -p 5775:5775/udp \
  -p 6831:6831/udp \
  -p 6832:6832/udp \
  -p 5778:5778 \
  -p 16686:16686 \
  -p 14268:14268 \
  -p 9411:9411 \
  jaegertracing/all-in-one:1.14

鏡像啟動后,通過http://localhost:16686可以打開Jaeger UI。

下載客戶端library:

go get github.com/jaegertracing/jaeger-client-go

初始化Jaeger tracer:

import (
    "context"
    "errors"
    "fmt"
    "io"
    "time"

    "github.com/opentracing/opentracing-go"
    "github.com/opentracing/opentracing-go/log"
    "github.com/uber/jaeger-client-go"
    jaegercfg "github.com/uber/jaeger-client-go/config"
)
// initJaeger 將jaeger tracer設置為全局tracer
func initJaeger(service string) io.Closer {
    cfg := jaegercfg.Configuration{
        // 將采樣頻率設置為1,每一個span都記錄,方便查看測試結果
        Sampler: &jaegercfg.SamplerConfig{
            Type:  jaeger.SamplerTypeConst,
            Param: 1,
        },
        Reporter: &jaegercfg.ReporterConfig{
            LogSpans: true,
            // 將span發往jaeger-collector的服務地址
            CollectorEndpoint: "http://localhost:14268/api/traces",
        },
    }
    closer, err := cfg.InitGlobalTracer(service, jaegercfg.Logger(jaeger.StdLogger))
    if err != nil {
        panic(fmt.Sprintf("ERROR: cannot init Jaeger: %v\n", err))
    }
    return closer
}

創建tracer,生成root span:

func main() {
    closer := initJaeger("in-process")
    defer closer.Close()
    // 獲取jaeger tracer
    t := opentracing.GlobalTracer()
    // 創建root span
    sp := t.StartSpan("in-process-service")
    // main執行完結束這個span
    defer sp.Finish()
    // 將span傳遞給Foo
    ctx := opentracing.ContextWithSpan(context.Background(), sp)
    Foo(ctx)
}

上述代碼創建了一個root span,並將該span通過context傳遞給Foo方法,以便在Foo方法中將追蹤鏈繼續延續下去:

func Foo(ctx context.Context) {
    // 開始一個span, 設置span的operation_name=Foo
    span, ctx := opentracing.StartSpanFromContext(ctx, "Foo")
    defer span.Finish()
    // 將context傳遞給Bar
    Bar(ctx)
    // 模擬執行耗時
    time.Sleep(1 * time.Second)
}
func Bar(ctx context.Context) {
    // 開始一個span,設置span的operation_name=Bar
    span, ctx := opentracing.StartSpanFromContext(ctx, "Bar")
    defer span.Finish()
    // 模擬執行耗時
    time.Sleep(2 * time.Second)

    // 假設Bar發生了某些錯誤
    err := errors.New("something wrong")
    span.LogFields(
        log.String("event", "error"),
        log.String("message", err.Error()),
    )
    span.SetTag("error", true)
}

Foo方法調用了Bar,假設在Bar中發生了一些錯誤,可以通過span.LogFieldsspan.SetTag將錯誤記錄在追蹤鏈中。 通過上面的例子可以發現,如果要確保追蹤鏈在程序中不斷開,需要將函數的第一個參數設置為context.Context,通過opentracing.ContextWithSpan將保存到context中,通過opentracing.StartSpanFromContext開始一個新的子span。

效果查看

執行完上面的程序后,打開Jaeger UI: http://localhost:16686/search,可以看到鏈路追蹤的結果:

點擊詳情可以查看具體信息:

通過鏈路追蹤系統,我們可以方便的掌握鏈路中各span的調用順序,調用關系,執行時間軸,以及記錄一些tag和log信息,極大的方便我們定位系統中的異常和發現性能瓶頸。

其他參考

Opentracing Go Tutorial
Opentracing語義習慣
Opentracing規范文檔v1.1


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM