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 等鏈路追蹤工具,
參考
- go-zero trace
- 開放分布式追蹤(OpenTracing)入門與 Jaeger 實作
專案地址:
https://github.com/tal-tech/go-zero
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/201278.html
標籤:Go
