go-zero 是如何追蹤你的請求鏈路?
go-zero 是一個整合了各種工程實踐的 web 和 rpc 框架。通過彈性設計保障了大併發服務端的穩定性,經受了充分的實戰檢驗。
序言
微服務架構中,呼叫鏈可能很漫長,從 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
並加入我們,https://github.com/tal-tech/go-zero
- 加微信實戰群請加微信(註明:實戰群):gocnio
相關文章
- go-zero 是如何追蹤你的請求鏈路的Go
- go-zero的全鏈路追蹤與超時Go
- go的鏈路追蹤Go
- 使用 Solon Cloud 的 Jaeger 做請求鏈路跟蹤Cloud
- Spring Cloud 鏈路追蹤SpringCloud
- skywalking鏈路追蹤
- gRPC的請求追蹤神器 go tool traceRPCGo
- 分散式鏈路追蹤的利器——Zipkin分散式
- go-zero docker-compose 搭建課件服務(八):整合jaeger鏈路追蹤GoDocker
- 分散式鏈路追蹤技術分散式
- Spring Cloud Sleuth 鏈路追蹤SpringCloud
- Go 鏈路追蹤入門 OpentelemetryGo
- 網路廣告商的畫素追蹤是如何工作的?
- golang 接入鏈路追蹤(opentracing)Golang
- zipkin分散式鏈路追蹤介紹分散式
- AsyncLocal<T>在鏈路追蹤中的應用
- Go - 實現專案內鏈路追蹤Go
- BOS分散式鏈路追蹤產品揭秘分散式
- 微服務鏈路追蹤元件 SkyWalking微服務元件
- 鏈路追蹤技術的應用及實踐
- Rust 語言的全鏈路追蹤庫 tracingRust
- Jaeger鏈路追蹤在專案中的應用
- 分散式鏈路追蹤框架的基本實現原理分散式框架
- 如何透過鏈路追蹤進行定時任務診斷
- go-kit微服務:服務鏈路追蹤Go微服務
- Dubbo日誌鏈路追蹤TraceId選型
- Jaeger Client Go 鏈路追蹤|入門詳解clientGo
- Go - 實現專案內鏈路追蹤(二)Go
- 【剖析 | SOFARPC 框架】系列之鏈路追蹤剖析RPC框架
- net core 微服務框架 Viper 呼叫鏈路追蹤微服務框架
- 日誌收集和鏈路追蹤:skywalking
- (16)go-micro微服務jaeger鏈路追蹤Go微服務
- 分散式系統中的分散式鏈路追蹤與分散式呼叫鏈路分散式
- .NET Core 中的日誌與分散式鏈路追蹤分散式
- 利用Spring Boot實現微服務的鏈路追蹤Spring Boot微服務
- 帶你十天輕鬆搞定 Go 微服務系列(九、鏈路追蹤)Go微服務
- 微服務 Zipkin 鏈路追蹤原理(圖文詳解)微服務
- Asp.Net Core&Jaeger實現鏈路追蹤ASP.NET