微服務架構中,調用鏈可能很漫長,從 http
到 rpc
,又從 rpc
到 http
。而開發者想了解每一個環節的調用狀況及性能,最佳方案就是 全鏈路跟蹤。git
追蹤的方法就是在一個請求開始時生成一個本身的 spanID
,隨着整個請求鏈路傳下去。咱們則經過這個 spanID
查看整個鏈路的狀況和性能問題。github
下面來看看 go-zero
的鏈路實現。數據庫
trace
傳播下游的操做「抽取,注入」tracer
實現在介紹 span
以前,先引入 context
。SpanContext 保存了分佈式追蹤的上下文信息,包括 Trace id,Span id 以及其它須要傳遞到下游的內容。OpenTracing 的實現須要將 SpanContext 經過某種協議 進行傳遞,以將不一樣進程中的 Span 關聯到同一個 Trace 上。對於 HTTP 請求來講,SpanContext 通常是採用 HTTP header 進行傳遞的。架構
下面是 go-zero
默認實現的 spanContext
app
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 }
一個 REST 調用或者數據庫操做等,均可以做爲一個 span
。 span
是分佈式追蹤的最小跟蹤單位,一個 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中已經做爲內置中間件集成。咱們以 http,rpc 中,看看 tracing
是怎麼使用的:工具
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, } }
將 header -> carrier,獲取 header 中的traceId等信息oop
開啓一個新的 span,並把「traceId,spanId」封裝在context中
從上述的 carrier「也就是header」獲取traceId,spanId。
從 request
中產生新的ctx,並將相應的信息封裝在 ctx 中,返回
從上述的 context,拷貝一份到當前的 request
這樣就實現了 span
的信息隨着 request
傳遞到下游服務。
在 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 }
go-zero
經過攔截請求獲取鏈路traceID,而後在中間件函數入口會分配一個根Span,而後在後續操做中會分裂出子Span,每一個span都有本身的具體的標識,Finsh以後就會聚集在鏈路追蹤系統中。
開發者能夠經過 ELK
工具追蹤 traceID
,看到整個調用鏈。同時 go-zero
並無提供整套 trace
鏈路方案,開發者能夠封裝 go-zero
已有的 span
結構,作本身的上報系統,接入 jaeger, zipkin
等鏈路追蹤工具。