1.概述
存在這樣一種場景,當我們進行微服務拆分後,一個請求將會經過多個服務處理之後再返回,這時,如果在請求的鏈路上某個服務出現故障時,排查故障將會比較困難. 我們可能需要將請求經過的服務,挨個檢視日誌進行分析,當服務有幾十上百個例項時,這無疑是可怕的.因此為了解決這種問題,呼叫鏈追蹤應運而生.
2.opentracing
1.1 opentracing作用
呼叫鏈追蹤最先由googel在Dapper這篇論文中提出,OpenTracing主要定義了相關的協議以及介面,這樣各個語言只要按照Opentracing的介面以及協議實現資料上報,那麼呼叫資訊就能統一被收集.
如上圖所示,介面可能首先經過web框架,然後呼叫auth服務,通過呼叫鏈,將請求經過的服務進行編號,統一收集起來,形成邏輯上的鏈路,這樣,我們就可以看到請求經過了哪些服務,從而形成服務依賴的拓撲.
如上,總鏈路由每段鏈路組成,每段鏈路均代表經過的服務,耗時可用於分析系統瓶頸,當某個請求返回較慢時,可以通過排查某一段鏈路的耗時情況,從而分析是哪個服務出現延時較高,今個到具體的服務中分析具體的問題.
1.2 opentraing關鍵術語
-
Traces(呼叫鏈)
一次呼叫的鏈路,由TraceID唯一標誌,如一次請求則通常為一個trace,trace由所有途徑的span組成. -
Spans(呼叫跨度)
沒進過一個服務則將span,同樣每個span由spanID唯一標誌. -
Span Tags(跨度標籤)
span的標籤,如一段span是呼叫redis的,而可以設定redis的標籤,這樣通過搜尋redis關鍵字,我們就可以查詢出所有相關的span以及trace. -
Baggage Item(附帶資料)
附加的資料,由key:value組成,通過附加資料,可以給呼叫鏈更多的描述資訊,不過考慮到傳輸問題,附加資料應該儘可能少.
1.3 jaeger & zipkin
- zipkin zipkin主要由java編寫,通過各個語言的上報庫實現將資料上報到collector,collector再將資料儲存,並通過API提供給前段UI展示.
- jaeger jaeger由go實現,由uber開發,目前是cloud native專案,流程與zipkin類似,增加jager-agent這樣個元件,這個元件官方建議是每個機器都部署一個,通過這個元件再將資料上報到collector儲存展示,另外,裡面做了對zipkin的適配,其實一開始他們用的也是zipkin,為毛後面要自己造輪子?見他們的解釋. 連結
總的來說兩者都能基本滿足opentracing的功能,具體的選擇可以結合自身技術棧和癖好.
2. grpc整合opentracing
grpc整合opentracing並不難,因為grpc服務端以及呼叫端分別宣告瞭UnaryClientInterceptor以及UnaryServerInterceptor兩個回撥函式,因此只需要重寫這兩個回撥函式,並在重寫的回撥函式中呼叫opentracing介面進行上報即可.
初始化時傳入重寫後的回撥函式,同時二選一初始化jager或者zipkin,然後你就可以開啟分散式呼叫鏈追蹤之旅了.
完整的程式碼見grpc-wrapper
2.1 client端
func OpenTracingClientInterceptor(tracer opentracing.Tracer) grpc.UnaryClientInterceptor {
return func(
ctx context.Context,
method string,
req, resp interface{},
cc *grpc.ClientConn,
invoker grpc.UnaryInvoker,
opts ...grpc.CallOption,
) error {
//從context中獲取spanContext,如果上層沒有開啟追蹤,則這裡新建一個
//追蹤,如果上層已經有了,測建立子span.
var parentCtx opentracing.SpanContext
if parent := opentracing.SpanFromContext(ctx); parent != nil {
parentCtx = parent.Context()
}
cliSpan := tracer.StartSpan(
method,
opentracing.ChildOf(parentCtx),
wrapper.TracingComponentTag,
ext.SpanKindRPCClient,
)
defer cliSpan.Finish()
//將之前放入context中的metadata資料取出,如果沒有則新建一個metadata
md, ok := metadata.FromOutgoingContext(ctx)
if !ok {
md = metadata.New(nil)
} else {
md = md.Copy()
}
mdWriter := MDReaderWriter{md}
//將追蹤資料注入到metadata中
err := tracer.Inject(cliSpan.Context(), opentracing.TextMap, mdWriter)
if err != nil {
grpclog.Errorf("inject to metadata err %v", err)
}
//將metadata資料裝入context中
ctx = metadata.NewOutgoingContext(ctx, md)
//使用帶有追蹤資料的context進行grpc呼叫.
err = invoker(ctx, method, req, resp, cc, opts...)
if err != nil {
cliSpan.LogFields(log.String("err", err.Error()))
}
return err
}
}
複製程式碼
2.2 server端
func OpentracingServerInterceptor(tracer opentracing.Tracer) grpc.UnaryServerInterceptor {
return func(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (resp interface{}, err error) {
//從context中取出metadata
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
md = metadata.New(nil)
}
//從metadata中取出最終資料,並建立出span物件
spanContext, err := tracer.Extract(opentracing.TextMap, MDReaderWriter{md})
if err != nil && err != opentracing.ErrSpanContextNotFound {
grpclog.Errorf("extract from metadata err %v", err)
}
//初始化server 端的span
serverSpan := tracer.StartSpan(
info.FullMethod,
ext.RPCServerOption(spanContext),
wrapper.TracingComponentTag,
ext.SpanKindRPCServer,
)
defer serverSpan.Finish()
ctx = opentracing.ContextWithSpan(ctx, serverSpan)
//將帶有追蹤的context傳入應用程式碼中進行呼叫
return handler(ctx, req)
}
}
複製程式碼
由於opentracing定義了相關的介面,而jaeger以及zipkin進行了相應的實現,因此這裡可以使用jaeger的也可以使用zipkin進行上報.
3.效果
jaeger服務主頁資訊
每條呼叫鏈資訊