在微服務架構中,呼叫鏈是漫長而復雜的,要了解其中的每個環節及其性能,你需要全鏈路跟蹤, 它的原理很簡單,你可以在每個請求開始時生成一個唯一的ID,并將其傳遞到整個呼叫鏈, 該ID稱為CorrelationID1,你可以用它來跟蹤整個請求并獲得各個呼叫環節的性能指標,簡單來說有兩個問題需要解決,第一,如何在應用程式內部傳遞ID; 第二,當你需要呼叫另一個微服務時,如何通過網路傳遞ID,
什么是OpenTracing?
現在有許多開源的分布式跟蹤庫可供選擇,其中最受歡迎的庫可能是Zipkin2和Jaeger3, 選擇哪個是一個令人頭疼的問題,因為你現在可以選擇最受歡迎的一個,但是如果以后有一個更好的出現呢?OpenTracing?可以幫你解決這個問題,它建立了一套跟蹤庫的通用介面,這樣你的程式只需要呼叫這些介面而不被具體的跟蹤庫系結,將來可以切換到不同的跟蹤庫而無需更改代碼,Zipkin和Jaeger都支持OpenTracing,
如何跟蹤服務器端點(server endpoints)?
在下面的程式中我使用“Zipkin”作為跟蹤庫,用“OpenTracing”作為通用跟蹤介面, 跟蹤系統中通常有四個組件,下面我用Zipkin作為示例:
-
recorder(記錄器):記錄跟蹤資料
-
Reporter (or collecting agent)(報告器或收集代理):從記錄器收集資料并將資料發送到UI程式
-
Tracer:生成跟蹤資料
-
UI:負責在圖形UI中顯示跟蹤資料

上面是Zipkin的組件圖,你可以在Zipkin Architecture中找到它,
有兩種不同型別的跟蹤,一種是行程內跟蹤(in-process),另一種是跨行程跟蹤(cross-process), 我們將首先討論跨行程跟蹤,
客戶端程式:
我們將用一個簡單的gRPC程式作為示例,它分成客戶端和服務器端代碼, 我們想跟蹤一個完整的服務請求,它從客戶端到服務端并從服務端回傳, 以下是在客戶端創建新跟蹤器的代碼,它首先創建“HTTP Collector”(the agent)用來收集跟蹤資料并將其發送到“Zipkin” UI, “endpointUrl”是“Zipkin” UI的URL, 其次,它創建了一個記錄器(recorder)來記錄端點上的資訊,“hostUrl”是gRPC(客戶端)呼叫的URL,第三,它用我們新建的記錄器創建了一個新的跟蹤器(tracer), 最后,它為“OpenTracing”設定了“GlobalTracer”,這樣你可以在程式中的任何地方訪問它,
const (
endpoint_url = "http://localhost:9411/api/v1/spans"
host_url = "localhost:5051"
service_name_cache_client = "cache service client"
service_name_call_get = "callGet"
)
func newTracer () (opentracing.Tracer, zipkintracer.Collector, error) {
collector, err := openzipkin.NewHTTPCollector(endpoint_url)
if err != nil {
return nil, nil, err
}
recorder :=openzipkin.NewRecorder(collector, true, host_url, service_name_cache_client)
tracer, err := openzipkin.NewTracer(
recorder,
openzipkin.ClientServerSameSpan(true))
if err != nil {
return nil,nil,err
}
opentracing.SetGlobalTracer(tracer)
return tracer,collector, nil
}
以下是gRPC客戶端代碼, 它首先呼叫上面提到的函式“newTrace()”來創建跟蹤器,然后,它創建一個包含跟蹤器的gRPC呼叫連接,接下來,它使用新建的gRPC連接創建快取服務(Cache service)的gRPC客戶端, 最后,它通過gRPC客戶端來呼叫快取服務的“Get”函式,
key:="123"
tracer, collector, err :=newTracer()
if err != nil {
panic(err)
}
defer collector.Close()
connection, err := grpc.Dial(host_url,
grpc.WithInsecure(), grpc.WithUnaryInterceptor(otgrpc.OpenTracingClientInterceptor(tracer, otgrpc.LogPayloads())),
)
if err != nil {
panic(err)
}
defer connection.Close()
client := pb.NewCacheServiceClient(connection)
value, err := callGet(key, client)
Trace 和 Span:
在OpenTracing中,一個重要的概念是“trace”,它表示從頭到尾的一個請求的呼叫鏈,它的識別符號是“traceID”, 一個“trace”包含有許多跨度(span),每個跨度捕獲呼叫鏈內的一個作業單元,并由“spanId”標識, 每個跨度具有一個父跨度,并且一個“trace”的所有跨度形成有向無環圖(DAG), 以下是跨度之間的關系圖, 你可以從The OpenTracing Semantic Specification中找到它,

以下是函式“callGet”的代碼,它呼叫了gRPC服務端的“Get"函式, 在函式的開頭,OpenTracing為這個函式呼叫開啟了一個新的span,整個函式結束后,它也結束了這個span,
const service_name_call_get = "callGet"
func callGet(key string, c pb.CacheServiceClient) ( []byte, error) {
span := opentracing.StartSpan(service_name_call_get)
defer span.Finish()
time.Sleep(5*time.Millisecond)
// Put root span in context so it will be used in our calls to the client.
ctx := opentracing.ContextWithSpan(context.Background(), span)
//ctx := context.Background()
getReq:=&pb.GetReq{Key:key}
getResp, err :=c.Get(ctx, getReq )
value := getResp.Value
return value, err
}
服務端代碼:
下面是服務端代碼,它與客戶端代碼類似,它呼叫了“newTracer()”(與客戶端“newTracer()”函式幾乎相同)來創建跟蹤器,然后,它創建了一個“OpenTracingServerInterceptor”,其中包含跟蹤器, 最后,它使用我們剛創建的攔截器(Interceptor)創建了gRPC服務器,
connection, err := net.Listen(network, host_url)
if err != nil {
panic(err)
}
tracer,err := newTracer()
if err != nil {
panic(err)
}
opts := []grpc.ServerOption{
grpc.UnaryInterceptor(
otgrpc.OpenTracingServerInterceptor(tracer,otgrpc.LogPayloads()),
),
}
srv := grpc.NewServer(opts...)
cs := initCache()
pb.RegisterCacheServiceServer(srv, cs)
err = srv.Serve(connection)
if err != nil {
panic(err)
} else {
fmt.Println("server listening on port 5051")
}
以下是運行上述代碼后在Zipkin中看到的跟蹤和跨度的圖片, 在服務器端,我們不需要在函式內部撰寫任何代碼來生成span,我們需要做的就是創建跟蹤器(tracer),服務器攔截器自動為我們生成span,

怎樣跟蹤函式內部?
上面的圖片沒有告訴我們函式內部的跟蹤細節, 我們需要撰寫一些代碼來獲得它,
以下是服務器端“get”函式,我們在其中添加了跟蹤代碼, 它首先從背景關系獲取跨度(span),然后創建一個新的子跨度并使用我們剛付訓得的跨度作為父跨度, 接下來,它執行一些操作(例如資料庫查詢),然后結束(mysqlSpan.Finish())子跨度,
const service_name_db_query_user = "db query user"
func (c *CacheService) Get(ctx context.Context, req *pb.GetReq) (*pb.GetResp, error) {
time.Sleep(5*time.Millisecond)
if parent := opentracing.SpanFromContext(ctx); parent != nil {
pctx := parent.Context()
if tracer := opentracing.GlobalTracer(); tracer != nil {
mysqlSpan := tracer.StartSpan(service_name_db_query_user, opentracing.ChildOf(pctx))
defer mysqlSpan.Finish()
//do some operations
time.Sleep(time.Millisecond * 10)
}
}
key := req.GetKey()
value := c.storage[key]
fmt.Println("get called with return of value: ", value)
resp := &pb.GetResp{Value: value}
return resp, nil
}
以下是它運行后的圖片, 現在它在服務器端有一個新的跨度“db query user”,

以下是zipkin中的跟蹤資料, 你可以看到客戶端從8.016ms開始,服務端也在同一時間啟動, 服務器端完成需要大約16ms,

怎樣跟蹤資料庫?
怎樣才能跟蹤資料庫內部的操作?首先,資料庫驅動程式需要支持跟蹤,另外你需要將跟蹤器(tracer)傳遞到資料庫函式中,如果資料庫驅動程式不支持跟蹤怎么辦?現在已經有幾個開源驅動程式封裝器(Wrapper),它們可以封裝任何資料庫驅動程式并使其支持跟蹤,其中一個是instrumentedsql?(另外兩個是luna-duclos/instrumentedsql?和ocsql/driver.go?),我簡要地看了一下他們的代碼,他們的原理基本相同,它們都為底層資料庫的每個函式創建了一個封裝(Wrapper),并在每個資料庫操作之前啟動一個新的跨度,并在操作完成后結束跨度,但是所有這些都只封裝了“database/sql”介面,這就意味著NoSQL資料庫沒有辦法使用他們,如果你找不到支持你需要的NoSQL資料庫(例如MongoDB)的OpenTracing的驅動程式,你可能需要自己撰寫一個封裝(Wrapper),它并不困難,
一個問題是“如果我使用OpenTracing和Zipkin而資料庫驅動程式使用Openeracing和Jaeger,那會有問題嗎?"這其實不會發生,我上面提到的大部分封裝都支持OpenTracing,在使用封裝時,你需要注冊封裝了的SQL驅動程式,其中包含跟蹤器,在SQL驅動程式內部,所有跟蹤函式都只呼叫了OpenTracing的介面,因此它們甚至不知道底層實作是Zipkin還是Jaeger,現在使用OpenTarcing的好處終于體現出來了,在應用程式中創建全域跟蹤器時(Global tracer),你需要決定是使用Zipkin還是Jaeger,但這之后,應用程式或第三方庫中的每個函式都只呼叫OpenTracing介面,已經與具體的跟蹤庫(Zipkin或Jaeger)沒關系了,
怎樣跟蹤服務呼叫?
假設我們需要在gRPC服務中呼叫另外一個微服務(例如RESTFul服務),該如何跟蹤?
簡單來說就是使用HTTP頭作為媒介(Carrier)來傳遞跟蹤資訊(traceID),無論微服務是gRPC還是RESTFul,它們都使用HTTP協議,如果是訊息佇列(Message Queue),則將跟蹤資訊(traceID)放入訊息報頭中,(Zipkin B3-propogation有“single header”和“multiple header”有兩種不同型別的跟蹤資訊,但JMS僅支持“single header”)
一個重要的概念是“跟蹤背景關系(trace context)”,它定義了傳播跟蹤所需的所有資訊,例如traceID,parentId(父spanId)等,有關詳細資訊,請閱讀跟蹤背景關系(trace context)1?,
OpenTracing提供了兩個處理“跟蹤背景關系(trace context)”的函式:“extract(format,carrier)”和“inject(SpanContext,format,carrier)”, “extarct()”從媒介(通常是HTTP頭)獲取跟蹤背景關系, “inject”將跟蹤背景關系放入媒介,來保證跟蹤鏈的連續性,以下是我從Zipkin獲取的b3-propagation圖,

但是為什么我們沒有在上面的例子中呼叫這些函式呢?讓我們再來回顧一下代碼,在客戶端,在創建gRPC客戶端連接時,我們呼叫了一個為“OpenTracingClientInterceptor”的函式, 以下是“OpenTracingClientInterceptor”的部分代碼,我從otgrpc11包中的“client.go”中得到了它,它已經從Go context12獲取了跟蹤背景關系并將其注入HTTP頭,因此我們不再需要再次呼叫“inject”函式,
func OpenTracingClientInterceptor(tracer opentracing.Tracer, optFuncs ...Option)
grpc.UnaryClientInterceptor {
...
ctx = injectSpanContext(ctx, tracer, clientSpan)
...
}
func injectSpanContext(ctx context.Context, tracer opentracing.Tracer, clientSpan opentracing.Span)
context.Context {
md, ok := metadata.FromOutgoingContext(ctx)
if !ok {
md = metadata.New(nil)
} else {
md = md.Copy()
}
mdWriter := metadataReaderWriter{md}
err := tracer.Inject(clientSpan.Context(), opentracing.HTTPHeaders, mdWriter)
// We have no better place to record an error than the Span itself :-/
if err != nil {
clientSpan.LogFields(log.String("event", "Tracer.Inject() failed"), log.Error(err))
}
return metadata.NewOutgoingContext(ctx, md)
}
在服務器端,我們還呼叫了一個函式“otgrpc.OpenTracingServerInterceptor”,其代碼類似于客戶端的“OpenTracingClientInterceptor”,它不是呼叫“inject”寫入跟蹤背景關系,而是從HTTP頭中提取(extract)跟蹤背景關系并將其放入Go背景關系(Go context)中, 這就是我們不需要再次手動呼叫“extract()”的原因, 我們可以直接從Go背景關系中提取跟蹤背景關系(opentracing.SpanFromContext(ctx)), 但對于其他基于HTTP的服務(如RESTFul服務), 情況就并非如此,因此我們需要寫代碼從服務器端的HTTP頭中提取跟蹤背景關系, 當然,您也可以使用攔截器或過濾器,
跟蹤庫之間的互兼容性
你也許會問“如果我的程式使用Zipkin和OpenTracing而需要呼叫的第三方微服務使用OpenTracing與Jaeger,它們會兼容嗎?"它看起來于我們之前詢問的資料庫問題類似,但實際上很不相同,對于資料庫,因為應用程式和資料庫在同一個行程中,它們可以共享相同的全域跟蹤器,因此更容易解決,對于微服務,這種方式將不兼容,因為OpenTracing只標準化了跟蹤介面,它沒有標準化跟蹤背景關系,萬維網聯盟(W3C)正在制定跟蹤背景關系(trace context)1?的標準,并于2019-08-09年發布了候選推薦標準,OpenTracing沒有規定跟蹤背景關系的格式,而是把決定權留給了實作它的跟蹤庫,結果每個庫都選擇了自己獨有的的格式,例如,Zipkin使用“X-B3-TraceId”作為跟蹤ID,Jaeger使用“uber-trace-id”,因此使用OpenTracing并不意味著不同的跟蹤庫可以進行跨網互操作, 對于“Jaeger”來說有一個好處是你可以選擇使用“Zipkin兼容性功能"13來生成Zipkin跟蹤背景關系, 這樣就可以與Zipkin相互兼容了,對于其他情況,你需要自己進行手動格式轉換(在“inject”和“extract”之間),
全鏈路跟蹤設計
盡量少寫代碼
一個好的全鏈路跟蹤系統不需要用戶撰寫很多跟蹤代碼,最理想的情況是你不需要任何代碼,讓框架或庫負責處理它,當然這比較困難, 全鏈路跟蹤分成三個跟蹤級別:
-
跨行程跟蹤 (cross-process)(呼叫另一個微服務)
-
資料庫跟蹤
-
行程內部的跟蹤 (in-process)(在一個函式內部的跟蹤)
跨行程跟蹤是最簡單的,你可以撰寫攔截器或過濾器來跟蹤每個請求,它只需要撰寫極少的編碼,資料庫跟蹤也比較簡單,如果使用我們上面討論過的封裝器(Wrapper),你只需要注冊SQL驅動程式封裝器(Wrapper)并將go-context(里面有跟蹤背景關系) 傳入資料庫函式,你可以使用依賴注入(Dependency Injection)這樣就可以用比較少的代碼來完成此操作,
行程內跟蹤是最困難的,因為你必須為每個單獨的函式撰寫跟蹤代碼,現在還沒有一個很好的方法,可以撰寫一個通用的函式來跟蹤應用程式中的每個函式(攔截器不是一個好選擇,因為它需要每個函式的引數和回傳都必須是一個泛型型別(interface {})),幸運的是,對于大多數人來說,前兩個級別的跟蹤應該已經足夠了,
有些人可能會使用服務網格(service mesh)來實作分布式跟蹤,例如Istio或Linkerd,它確實是一個好主意,跟蹤最好由基礎架構實作,而不是將業務邏輯代碼與跟蹤代碼混在一起,不過你將遇到我們剛才談到的同樣問題,服務網格只負責跨行程跟蹤,函式內部或資料庫跟蹤任然需要你來撰寫代碼,不過一些服務網格可以通過提供與流行跟蹤庫的集成,來簡化不同跟蹤庫跨網跟蹤時的的背景關系格式轉換,
跟蹤設計:
精心設計的跨度(span),服務名稱(service name),標簽(tag)能充分發揮全鏈路跟蹤的作用,并使之簡單易用,有關資訊請閱讀語意約定(Semantic Conventions)1?,
將Trace ID記錄到日志
將跟蹤與日志記錄集成是一個常見的需求,最重要的是將跟蹤ID記錄到整個呼叫鏈的日志訊息中, 目前OpenTracing不提供訪問traceID的方法, 你可以將“OpenTracing.SpanContext”轉換為特定跟蹤庫的“SpanContext”(Zipkin和Jaeger都可以通過“SpanContext”訪問traceID)或將“OpenTracing.SpanContext”轉換為字串并決議它以獲取traceID,轉換為字串更好,因為它不會破壞程式的依賴關系, 幸運的是不久的將來你就不需要它了,因為OpenTracing將提供訪問traceID的方法,請閱讀這里,
OpenTracing 和 OpenCensus
OpenCensus1?不是另一個通用跟蹤介面,它是一組庫,可以用來與其他跟蹤庫集成以完成跟蹤功能,因此它經常與OpenTracing進行比較, 那么它與OpenTracing兼容嗎?答案是否定的, 因此,在選擇跟蹤介面時(不論是OpenTracing還是OpenCensus)需要小心,以確保你需要呼叫的其他庫支持它, 一個好訊息是,你不需要在將來做出選擇,因為它們會將專案合并為一個1?,
結論:
全鏈路跟蹤包括不同的場景,例如在函式內部跟蹤,資料庫跟蹤和跨行程跟蹤, 每個場景都有不同的問題和解決方案,如果你想設計更好的跟蹤解決方案或為你的應用選擇最適合的跟蹤工具或庫,那你需要對每種情況都有清晰的了解,
原始碼:
完整原始碼的github鏈接
索引:
[1]Correlation IDs for microservices architectures
https://hilton.org.uk/blog/microservices-correlation-id
[2]Zipkin
https://zipkin.io
[3]Jaeger: open source, end-to-end distributed tracing
https://www.jaegertracing.io
[4]OpenTracing
https://opentracing.io/docs/getting-started
[5]Zipkin Architecture
https://zipkin.io/pages/architecture.html
[6]The OpenTracing Semantic Specification
https://opentracing.io/specification/
[7]instrumentedsql
https://github.com/ExpansiveWorlds/instrumentedsql
[8]luna-duclos/instrumentedsql
https://github.com/luna-duclos/instrumentedsql
[9]ocsql/driver.go
https://github.com/opencensus-integrations/ocsql/blob/master/driver.go
[10]Trace Context
https://www.w3.org/TR/trace-context/
[11]otgrpc
http://github.com/grpc-ecosystem/grpc-opentracing/go/otgrpc
[12]Go Concurrency Patterns: Context
https://blog.golang.org/context
[13]Zipkin compatibility features
https://github.com/jaegertracing/jaeger-client-go/tree/master/zipkin
[14]Semantic Conventions
https://github.com/opentracing/specification/blob/master/semantic_conventions.md
[15]OpenCensus
https://opencensus.io/
[16]merge the project into one
https://medium.com/opentracing/merging-opentracing-and-opencensus-f0fe9c7ca6f0
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/71577.html
標籤:Go
上一篇:Golang error 的突圍
下一篇:GoLang中面向物件的三大特性
