微服務(wù)架構(gòu)中,調(diào)用鏈可能很漫長(zhǎng),從 http 到 rpc ,又從 rpc 到 http 。而開發(fā)者想了解每個(gè)環(huán)節(jié)的調(diào)用情況及性能,最佳方案就是 全鏈路跟蹤。
追蹤的方法就是在一個(gè)請(qǐng)求開始時(shí)生成一個(gè)自己的 spanID ,隨著整個(gè)請(qǐng)求鏈路傳下去。我們則通過(guò)這個(gè) spanID 查看整個(gè)鏈路的情況和性能問(wèn)題。
下面來(lái)看看 go-zero 的鏈路實(shí)現(xiàn)。
在介紹 span 之前,先引入 context 。SpanContext 保存了分布式追蹤的上下文信息,包括 Trace id,Span id 以及其它需要傳遞到下游的內(nèi)容。OpenTracing 的實(shí)現(xiàn)需要將 SpanContext 通過(guò)某種協(xié)議 進(jìn)行傳遞,以將不同進(jìn)程中的 Span 關(guān)聯(lián)到同一個(gè) Trace 上。對(duì)于 HTTP 請(qǐng)求來(lái)說(shuō),SpanContext 一般是采用 HTTP header 進(jìn)行傳遞的。
下面是 go-zero 默認(rèn)實(shí)現(xiàn)的 spanContext
type spanContext struct {
traceId string // TraceID 表示tracer的全局唯一ID
spanId string // SpanId 標(biāo)示單個(gè)trace中某一個(gè)span的唯一ID,在trace中唯一
}
同時(shí)開發(fā)者也可以實(shí)現(xiàn) SpanContext 提供的接口方法,實(shí)現(xiàn)自己的上下文信息傳遞:
type SpanContext interface {
TraceId() string // get TraceId
SpanId() string // get SpanId
Visit(fn func(key, val string) bool) // 自定義操作TraceId,SpanId
}
一個(gè) REST 調(diào)用或者數(shù)據(jù)庫(kù)操作等,都可以作為一個(gè) span 。 span 是分布式追蹤的最小跟蹤單位,一個(gè) Trace 由多段 Span 組成。追蹤信息包含如下信息:
type Span struct {
ctx spanContext // 傳遞的上下文
serviceName string // 服務(wù)名
operationName string // 操作
startTime time.Time // 開始時(shí)間戳
flag string // 標(biāo)記開啟trace是 server 還是 client
children int // 本 span fork出來(lái)的 childsnums
}
從 span 的定義結(jié)構(gòu)來(lái)看:在微服務(wù)中, 這就是一個(gè)完整的子調(diào)用過(guò)程,有調(diào)用開始 startTime ,有標(biāo)記自己唯一屬性的上下文結(jié)構(gòu) spanContext 以及 fork 的子節(jié)點(diǎn)數(shù)。
在 go-zero 中http,rpc中已經(jīng)作為內(nèi)置中間件集成。我們以 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(),
// 標(biāo)記為server
flag: serverFlag,
}
}
這樣就實(shí)現(xiàn)了 span 的信息隨著 request 傳遞到下游服務(wù)。
在 rpc 中存在 client, server ,所以從 tracing 上也有 clientTracing, serverTracing 。 serveTracing 的邏輯基本與 http 的一致,來(lái)看看 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 通過(guò)攔截請(qǐng)求獲取鏈路traceID,然后在中間件函數(shù)入口會(huì)分配一個(gè)根Span,然后在后續(xù)操作中會(huì)分裂出子Span,每個(gè)span都有自己的具體的標(biāo)識(shí),F(xiàn)insh之后就會(huì)匯集在鏈路追蹤系統(tǒng)中。開發(fā)者可以通過(guò) ELK 工具追蹤 traceID ,看到整個(gè)調(diào)用鏈。
同時(shí) go-zero 并沒(méi)有提供整套 trace 鏈路方案,開發(fā)者可以封裝 go-zero 已有的 span 結(jié)構(gòu),做自己的上報(bào)系統(tǒng),接入 jaeger, zipkin 等鏈路追蹤工具。
更多建議: