Go集成Opentracing(分佈式鏈路追蹤)

在微服務架構的系統中,請求在各服務之間流轉,調用鏈錯綜複雜,一旦出現了問題和異常,很難追查定位,這個時候就須要鏈路追蹤來幫忙了。鏈路追蹤系統能追蹤並記錄請求在系統中的調用順序,調用時間等一系列關鍵信息,從而幫助咱們定位異常服務和發現性能瓶頸。git

Opentracing

Opentracing是分佈式鏈路追蹤的一種規範標準,是CNCF(雲原生計算基金會)下的項目之一。和通常的規範標準不一樣,Opentracing不是傳輸協議,消息格式層面上的規範標準,而是一種語言層面上的API標準。以Go語言爲例,只要某鏈路追蹤系統實現了Opentracing規定的接口(interface),符合Opentracing定義的表現行爲,那麼就能夠說該應用符合Opentracing標準。這意味着開發者只需修改少許的配置代碼,就能夠在符合Opentracing標準的鏈路追蹤系統之間自由切換。github

github.com/opentracing…docker

Data Model

在使用Opentracing來實現全鏈路追蹤前,有必要先了解一下它所定義的數據模型。api

Span

Span是一條追蹤鏈路中的基本組成要素,一個span表示一個獨立的工做單元,好比能夠表示一次函數調用,一次http請求等等。span會記錄以下基本要素:bash

  • 服務名稱(operation name)
  • 服務的開始時間和結束時間
  • K/V形式的Tags
  • K/V形式的Logs
  • SpanContext
  • References:該span對一個或多個span的引用(經過引用SpanContext)。

Tags

Tags以K/V鍵值對的形式保存用戶自定義標籤,主要用於鏈路追蹤結果的查詢過濾。例如: http.method="GET",http.status_code=200。其中key值必須爲字符串,value必須是字符串,布爾型或者數值型。 span中的tag僅本身可見,不會隨着 SpanContext傳遞給後續span。 例如:網絡

span.SetTag("http.method","GET")
span.SetTag("http.status_code",200)
複製代碼

Logs

Logs與tags相似,也是K/V鍵值對形式。與tags不一樣的是,logs還會記錄寫入logs的時間,所以logs主要用於記錄某些事件發生的時間。logs的key值一樣必須爲字符串,但對value類型則沒有限制。例如:架構

span.LogFields(
	log.String("event", "soft error"),
	log.String("type", "cache timeout"),
	log.Int("waited.millis", 1500),
)
複製代碼

Opentracing列舉了一些慣用的Tags和Logs: github.com/opentracing…異步

SpanContext

SpanContext攜帶着一些用於跨服務通訊的(跨進程)數據,主要包含:分佈式

  • 足夠在系統中標識該span的信息,好比:span_id,trace_id
  • Baggage Items,爲整條追蹤連保存跨服務(跨進程)的K/V格式的用戶自定義數據。
Baggage Items

Baggage Items與tags相似,也是K/V鍵值對。與tags不一樣的是:函數

  • 其key跟value都只能是字符串格式
  • Baggage items不只當前span可見,其會隨着SpanContext傳遞給後續全部的子span。要當心謹慎的使用baggage items——由於在全部的span中傳遞這些K,V會帶來不小的網絡和CPU開銷。

References

Opentracing定義了兩種引用關係:ChildOfFollowFrom

ChildOf: 父span的執行依賴子span的執行結果時,此時子span對父span的引用關係是ChildOf。好比對於一次RPC調用,服務端的span(子span)與客戶端調用的span(父span)是ChildOf關係。

FollowFrom:父span的執不依賴子span執行結果時,此時子span對父span的引用關係是FollowFromFollowFrom經常使用於異步調用的表示,例如消息隊列中consumerspan與producerspan之間的關係。

Trace

Trace表示一次完整的追蹤鏈路,trace由一個或多個span組成。下圖示例表示了一個由8個span組成的trace:

[Span A]  ←←←(the root span)
            |
     +------+------+
     |             |
 [Span B]      [Span C] ←←←(Span C is a `ChildOf` Span A)
     |             |
 [Span D]      +---+-------+
               |           |
           [Span E]    [Span F] >>> [Span G] >>> [Span H]
                                       ↑
                                       ↑
                                       ↑
                         (Span G `FollowsFrom` Span F)
複製代碼

時間軸的展示方式會更容易理解:

––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–> time

 [Span A···················································]
   [Span B··············································]
      [Span D··········································]
    [Span C········································]
         [Span E·······]        [Span F··] [Span G··] [Span H··]
複製代碼

示例來源: github.com/opentracing…

示例

對Opentracing的概念有初步瞭解後,下面使用Jaeger來演示如何在程序中使用實現鏈路追蹤。

更多更詳細的示例可參考: Opentracing Go Tutorial

Jaeger

Jaeger\ˈyā-gər\ 是Uber開源的分佈式追蹤系統,是遵循Opentracing的系統之一,也是CNCF項目。本篇將使用Jaeger來演示如何在系統中引入分佈式追蹤。

Quick Start

Jaeger提供了all-in-one鏡像,方便咱們快速開始測試:

$ docker run -d --name jaeger \
  -e COLLECTOR_ZIPKIN_HTTP_PORT=9411 \
  -p 5775:5775/udp \
  -p 6831:6831/udp \
  -p 6832:6832/udp \
  -p 5778:5778 \
  -p 16686:16686 \
  -p 14268:14268 \
  -p 9411:9411 \
  jaegertracing/all-in-one:1.14
複製代碼

鏡像啓動後,經過http://localhost:16686能夠打開Jaeger UI。

下載客戶端library:

go get github.com/jaegertracing/jaeger-client-go
複製代碼

初始化Jaeger tracer:

import (
	"context"
	"errors"
	"fmt"
	"io"
	"time"

	"github.com/opentracing/opentracing-go"
	"github.com/opentracing/opentracing-go/log"
	"github.com/uber/jaeger-client-go"
	jaegercfg "github.com/uber/jaeger-client-go/config"
)
// initJaeger 將jaeger tracer設置爲全局tracer
func initJaeger(service string) io.Closer {
	cfg := jaegercfg.Configuration{
		// 將採樣頻率設置爲1,每個span都記錄,方便查看測試結果
		Sampler: &jaegercfg.SamplerConfig{
			Type:  jaeger.SamplerTypeConst,
			Param: 1,
		},
		Reporter: &jaegercfg.ReporterConfig{
			LogSpans: true,
			// 將span發往jaeger-collector的服務地址
			CollectorEndpoint: "http://localhost:14268/api/traces",
		},
	}
	closer, err := cfg.InitGlobalTracer(service, jaegercfg.Logger(jaeger.StdLogger))
	if err != nil {
		panic(fmt.Sprintf("ERROR: cannot init Jaeger: %v\n", err))
	}
	return closer
}

複製代碼

建立tracer,生成root span:

func main() {
	closer := initJaeger("in-process")
	defer closer.Close()
	// 獲取jaeger tracer
	t := opentracing.GlobalTracer()
	// 建立root span
	sp := t.StartSpan("in-process-service")
	// main執行完結束這個span
	defer sp.Finish()
	// 將span傳遞給Foo
	ctx := opentracing.ContextWithSpan(context.Background(), sp)
	Foo(ctx)
}
複製代碼

上述代碼建立了一個root span,並將該span經過context傳遞給Foo方法,以便在Foo方法中將追蹤鏈繼續延續下去:

func Foo(ctx context.Context) {
    // 開始一個span, 設置span的operation_name=Foo
	span, ctx := opentracing.StartSpanFromContext(ctx, "Foo")
	defer span.Finish()
	// 將context傳遞給Bar
	Bar(ctx)
	// 模擬執行耗時
	time.Sleep(1 * time.Second)
}
func Bar(ctx context.Context) {
    // 開始一個span,設置span的operation_name=Bar
	span, ctx := opentracing.StartSpanFromContext(ctx, "Bar")
	defer span.Finish()
	// 模擬執行耗時
	time.Sleep(2 * time.Second)

	// 假設Bar發生了某些錯誤
	err := errors.New("something wrong")
	span.LogFields(
		log.String("event", "error"),
		log.String("message", err.Error()),
	)
	span.SetTag("error", true)
}

複製代碼

Foo方法調用了Bar,假設在Bar中發生了一些錯誤,能夠經過span.LogFieldsspan.SetTag將錯誤記錄在追蹤鏈中。 經過上面的例子能夠發現,若是要確保追蹤鏈在程序中不斷開,須要將函數的第一個參數設置爲context.Context,經過opentracing.ContextWithSpan將保存到context中,經過opentracing.StartSpanFromContext開始一個新的子span。

效果查看

執行完上面的程序後,打開Jaeger UI: http://localhost:16686/search,能夠看到鏈路追蹤的結果:

點擊詳情能夠查看具體信息:

經過鏈路追蹤系統,咱們能夠方便的掌握鏈路中各span的調用順序,調用關係,執行時間軸,以及記錄一些tag和log信息,極大的方便咱們定位系統中的異常和發現性能瓶頸。

其餘參考

Opentracing Go Tutorial
Opentracing語義習慣
Opentracing規範文檔v1.1

相關文章
相關標籤/搜索