在微服務架構的系統中,請求在各服務之間流轉,調用鏈錯綜復雜,一旦出現了問題和異常,很難追查定位,這個時候就需要鏈路追蹤來幫忙了。鏈路追蹤系統能追蹤並記錄請求在系統中的調用順序,調用時間等一系列關鍵信息,從而幫助我們定位異常服務和發現性能瓶頸。
Opentracing
Opentracing是分布式鏈路追蹤的一種規范標准,是CNCF(雲原生計算基金會)下的項目之一。和一般的規范標准不同,Opentracing不是傳輸協議,消息格式層面上的規范標准,而是一種語言層面上的API標准。以Go語言為例,只要某鏈路追蹤系統實現了Opentracing規定的接口(interface),符合Opentracing定義的表現行為,那么就可以說該應用符合Opentracing標准。這意味着開發者只需修改少量的配置代碼,就可以在符合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定義了兩種引用關系:ChildOf
和FollowFrom
。
ChildOf: 父span的執行依賴子span的執行結果時,此時子span對父span的引用關系是ChildOf
。比如對於一次RPC調用,服務端的span(子span)與客戶端調用的span(父span)是ChildOf
關系。
FollowFrom:父span的執不依賴子span執行結果時,此時子span對父span的引用關系是FollowFrom
。FollowFrom
常用於異步調用的表示,例如消息隊列中consumer
span與producer
span之間的關系。
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.LogFields
和span.SetTag
將錯誤記錄在追蹤鏈中。 通過上面的例子可以發現,如果要確保追蹤鏈在程序中不斷開,需要將函數的第一個參數設置為context.Context
,通過opentracing.ContextWithSpan
將保存到context
中,通過opentracing.StartSpanFromContext
開始一個新的子span。
效果查看
執行完上面的程序后,打開Jaeger UI: http://localhost:16686/search,可以看到鏈路追蹤的結果:
點擊詳情可以查看具體信息:
通過鏈路追蹤系統,我們可以方便的掌握鏈路中各span的調用順序,調用關系,執行時間軸,以及記錄一些tag和log信息,極大的方便我們定位系統中的異常和發現性能瓶頸。