go-zero 是如何追蹤你的請求鏈路
微服務架構中,呼叫鏈可能很漫長,從 http
到 rpc
,又從 rpc
到 http
。而開發者想了解每個環節的呼叫情況及效能,最佳方案就是 全鏈路跟蹤。
追蹤的方法就是在一個請求開始時生成一個自己的 spanID
,隨著整個請求鏈路傳下去。我們則通過這個 spanID
檢視整個鏈路的情況和效能問題。
下面來看看 go-zero
的鏈路實現。
程式碼結構
- spancontext:儲存鏈路的上下文資訊「traceid,spanid,或者是其他想要傳遞的內容」
- span:鏈路中的一個操作,儲存時間和某些資訊
- propagator:
trace
傳播下游的操作「抽取,注入」 - noop:實現了空的
tracer
實現
概念
SpanContext
在介紹 span
之前,先引入 context
。SpanContext 儲存了分散式追蹤的上下文資訊,包括 Trace id,Span id 以及其它需要傳遞到下游的內容。OpenTracing 的實現需要將 SpanContext 通過某種協議 進行傳遞,以將不同程式中的 Span 關聯到同一個 Trace 上。對於 HTTP 請求來說,SpanContext 一般是採用 HTTP header 進行傳遞的。
下面是 go-zero
預設實現的 spanContext
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
}
Span
一個 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
是怎麼使用的:
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(),
// 標記為server
flag: serverFlag,
}
}
-
將 header -> carrier,獲取 header 中的traceId等資訊
-
開啟一個新的 span,並把「traceId,spanId」封裝在context中
-
從上述的 carrier「也就是header」獲取traceId,spanId。
-
- 看header中是否設定
-
- 如果沒有設定,則隨機生成返回
-
從
request
中產生新的ctx,並將相應的資訊封裝在 ctx 中,返回 -
從上述的 context,拷貝一份到當前的
request
這樣就實現了 span
的資訊隨著 request
傳遞到下游服務。
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 中建立新的 ctx,span「繼承父span的traceId」
- 將生成 span 的data加入ctx,傳遞到下一個中介軟體,流至下游
總結
go-zero
通過攔截請求獲取鏈路traceID,然後在中介軟體函式入口會分配一個根Span,然後在後續操作中會分裂出子Span,每個span都有自己的具體的標識,Finsh之後就會彙集在鏈路追蹤系統中。
開發者可以通過 ELK
工具追蹤 traceID
,看到整個呼叫鏈。同時 go-zero
並沒有提供整套 trace
鏈路方案,開發者可以封裝 go-zero
已有的 span
結構,做自己的上報系統,接入 jaeger, zipkin
等鏈路追蹤工具。