基于opentracing + jaeger 實現全鏈路追蹤

[TOC]

鏈路追蹤

當代互聯網服務,通常都是用復雜,大規模分布式集群來實現,微服務化,這些軟件模塊分布在不同的機器,不同的數據中心,由不同團隊,語言開發而成。因此,需要工具幫助理解,分析這些系統、定位問題,做到追蹤每一個請求的完整調用鏈路,收集性能數據,反饋到服務治理中,鏈路追蹤系統應運而生。

現有大部分 APM(Application Performance Management) 理論模型大多借鑒 google dapper 論文,Twitter的zipkin,Uber的 jaeger,淘寶的鷹眼,大眾的cat,京東的Hydra等。

微服務問題:

  1. 故障定位難
  2. 鏈路梳理難
  3. 容量預估難

舉個例子,一個場景下,一個請求進來,入口服務是 serviceA, serviceA 接到請求后訪問數據庫讀取用戶數據,然后向 serviceB 發起 rpc,serviceB 收到 rpc 請求時同時向后端服務 serviceC 和 serviceD 發起請求,等待請求回復后再返回 serviceA 的 rpc 調用。如果我們發現發起的請求失敗,或者請求的時延很大,我們該如何去定位呢?

基于這個需求,我們將服務介入追蹤系統。

分布式追蹤系統發展很快,種類繁多,但核心步驟一般有三個:代碼埋點,數據存儲、查詢展示

在數據采集過程,需要侵入用戶代碼做埋點,不同系統的API不兼容會導致切換追蹤系統需要做很大的改動。為了解決這個問題,誕生了opentracing 規范。

   +-------------+  +---------+  +----------+  +------------+
   | Application |  | Library |  |   OSS    |  |  RPC/IPC   |
   |    Code     |  |  Code   |  | Services |  | Frameworks |
   +-------------+  +---------+  +----------+  +------------+
          |              |             |             |
          |              |             |             |
          v              v             v             v
     +-----------------------------------------------------+
     | · · · · · · · · · · OpenTracing · · · · · · · · · · |
     +-----------------------------------------------------+
       |               |                |               |
       |               |                |               |
       v               v                v               v
 +-----------+  +-------------+  +-------------+  +-----------+
 |  Tracing  |  |   Logging   |  |   Metrics   |  |  Tracing  |
 | System A  |  | Framework B |  | Framework C |  | System D  |
 +-----------+  +-------------+  +-------------+  +-----------+

OpenTracing

opentracing (中文)是一套分布式追蹤協議,與平臺,語言無關,統一接口,方便開發接入不同的分布式追蹤系統。

  • 語義規范 : 描述定義的數據模型 Tracer,Sapn 和 SpanContext 等;

  • 語義慣例 : 羅列出 tag 和 logging 操作時,標準的key值;

Trace 和 sapn

opentracing 中的 Trace(調用鏈)通過歸屬此鏈的 Span 來隱性定義。一條 Trace 可以認為一個有多個 Span 組成的有向無環圖(DAG圖),Span 是一個邏輯執行單元,Span 與 Span 的因果關系命名為 References。

opentracing 定義兩種關系:

  • Childof:如下例子中, SpanC 是 childof SpanA
  • FollowsFrom:如下例子中,SpanG 是 followsFrom SpanF

例子 Trace 包含 8個 Span,

 [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)

通過時間軸顯示一個 Tracer 更加直觀,

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

 [Span A···················································]
   [Span B··············································]
      [Span D··········································]
    [Span C········································]
         [Span E·······]        [Span F··] [Span G··] [Span H··]

每個Span封裝了如下狀態:

  • 操作名稱
  • 開始時間戳
  • 結束時間戳
  • 一組零或多個鍵:值結構的 Span標簽 (Tags)。鍵必須是字符串。值可以是字符串,布爾或數值類型.
  • 一組零或多個 Span日志 (Logs),其中每個都是一個鍵:值映射并與一個時間戳配對。鍵必須是字符串,值可以是任何類型。 并非所有的 OpenTracing 實現都必須支持每種值類型。
  • 一個 SpanContext (見下文)
  • 零或多個因果相關的 Span 間的 References (通過那些相關的 SpanSpanContext )

每個 SpanContext 封裝了如下狀態:

  • 任何需要跟跨進程 Span 關聯的,依賴于 OpenTracing 實現的狀態(例如 Trace 和 Span 的 id)

  • 鍵:值結構的跨進程的 Baggage Items(區別于 span tag,baggage 是全局范圍,在 span 間保持傳遞,而tag 是 span 內部,不會被子 span 繼承使用。)

InjectExtract 操作

跨進程,機器通訊,通過傳遞 Spancontext 來提供足夠的信息建立 span 間的關系。SpanContext 通過 Inject 操作向 Carrier 中增加,傳遞后通過 ExtractedCarrier 中取出。

關于inject 和 extract

Sampling,采樣

OpenTracing API 不強調采樣的概念,但是大多數追蹤系統通過不同方式實現采樣。有些情況下,應用系統需要通知追蹤程序,這條特定的調用需要被記錄,即使根據默認采樣規則,它不需要被記錄。sampling.priority tag 提供這樣的方式。追蹤系統不保證一定采納這個參數,但是會盡可能的保留這條調用。
sampling.priority - integer

  • 如果大于 0, 追蹤系統盡可能保存這條調用鏈

  • 等于 0, 追蹤系統不保存這條調用鏈

  • 如果此tag沒有提供,追蹤系統使用自己的默認采樣規則

OpenTracing 多語言支持

提供不同語言的 API,用于在自己的應用程序中執行鏈路記錄。

Jaeger

Jaeger (?yā-g?r) 是Uber開發的一套分布式追蹤系統,受啟發于 dapper 和 OpenZipkin,兼容 OpenTracing 標準,CNCF的開源項目。

系統框架

image.png
  • Jaeger Client - 為不同語言實現了符合 OpenTracing 標準的 SDK。應用程序通過 API 寫入數據,client library 把 trace 信息按照應用程序指定的采樣策略傳遞給 jaeger-agent。
  • Agent - 是一個監聽在 UDP 端口上接收 span 數據的網絡守護進程,它會將數據批量發送給 collector。它被設計成一個基礎組件,推薦部署到所有的宿主機上。Agent 將 client library 和 collector 解耦,為 client library 屏蔽了路由和發現 collector 的細節。
  • Collector - 接收 jaeger-agent 發送來的數據,然后將數據寫入后端存儲。Collector 被設計成無狀態的組件,因此您可以同時運行任意數量的 jaeger-collector。
  • Data Store - 后端存儲被設計成一個可插拔的組件,支持將數據寫入 cassandra、elastic search。
  • Query - 接收查詢請求,然后從后端存儲系統中檢索 trace 并通過 UI 進行展示。Query 是無狀態的,您可以啟動多個實例,把它們部署在 nginx 這樣的負載均衡器后面。

官方釋放部署的鏡像到 dockerhub,所以部署 jaeger 非常方便,如果是本地測試,可以直接用 jaeger 提供的 all-in-one 鏡像部署。

快速搭建,all-in-one

執行一下命令,可以在本機拉起一個 jaeger 環境,上報的鏈路數據保存在本地內存,所以只能用于測試。

$ docker run -d --name jaeger \
  -e COLLECTOR_ZIPKIN_HTTP_PORT=9411 \
  -p 5775:5775/udp \kaixiao
  -p 6831:6831/udp \
  -p 6832:6832/udp \
  -p 5778:5778 \
  -p 16686:16686 \
  -p 14268:14268 \
  -p 9411:9411 \
  jaegertracing/all-in-one:latest

通過 http://localhost:16686 可以在瀏覽器查看 Jaeger UI

官方提供的一個例子: HotROD

采樣速率

生產環境系統性能很重要,所以對于所有的請求都開啟 Trace 顯然會帶來比較大的壓力,另外,大量的數據也會帶來很大存儲壓力。為此,jaeger 支持設置采樣速率,根據系統實際情況設置合適的采樣頻率。

Jaeger 官方提供了多種采集策略,使用者可以按需選擇使用

  1. const,全量采集,采樣率設置0,1 分別對應打開和關閉
  2. probabilistic ,概率采集,默認萬份之一,0~1之間取值,
  3. rateLimiting ,限速采集,每秒只能采集一定量的數據
  4. remote ,一種動態采集策略,根據當前系統的訪問量調節采集策略

追蹤實踐 - go

go 程序中集成鏈路追蹤并上報到 jaeger 需要用到一下兩個包 opentracing go api 和 jaeger go 客戶端。

一個簡單的 trace

以下代碼上報一個包含一個 span 的 trace,程序在初始化階段通過環境變量獲取 jaeger 的配置并初始化全局 tracer。之后便可以通過這個 tracer 開啟 span(root span) 記錄程序鏈路。

package main
import (
    "fmt"
    "io"
    "time"

    opentracing "github.com/opentracing/opentracing-go"
    jaeger "github.com/uber/jaeger-client-go"
    jaegercfg "github.com/uber/jaeger-client-go/config"
)
// InitJaeger ...
func InitJaeger(service string) (opentracing.Tracer, io.Closer) {
    cfg, err := jaegercfg.FromEnv()
    /*
        cfg.Sampler.Type = "const"
        cfg.Sampler.Param = 1
        cfg.Reporter.LocalAgentHostPort = "127.0.0.1:6831"
        cfg.Reporter.LogSpans = true
    */
    tracer, closer, err := cfg.New(service, jaegercfg.Logger(jaeger.StdLogger))
    if err != nil {
        panic(fmt.Sprintf("ERROR: cannot init Jaeger: %v\n", err))
    }
    return tracer, closer
}

func main() {
    tracer, closer := InitJaeger("hello-world")
    defer closer.Close()
    opentracing.InitGlobalTracer(tracer)

    helloStr := "hello jaeger"
    span := tracer.StartSpan("say-hello")
    time.Sleep(time.Duration(2) * time.Millisecond)
    println(helloStr)
    span.Finish() 
}

然后通過 jaeger ui 可以看到本次上報的 trace。

$ export JAEGER_DISABLED=false
$ export JAEGER_SAMPLER_TYPE="const"
$ export JAEGER_SAMPLER_PARAM=1
$ export JAEGER_REPORTER_LOG_SPANS=true
$ export JAEGER_AGENT_HOST="127.0.0.1"
$ export JAEGER_AGENT_PORT=6831
$ go run ./test.go
2019/06/09 23:01:31 Initializing logging reporter
hello jaeger
2019/06/09 23:01:31 Reporting span 2813d696ced4431:2813d696ced4431:0:1

在開啟 span 記錄一個過程時,還可以通過 api 進行 tag,logs等操作 ,并能在 UI 看到相應設置的鍵z值

span.SetTag("value", helloStr)
span.LogFields(
    log.String("event", "sayhello"),
    log.String("value", helloStr),
)
//span.LogKV("event", "sayhello") // 單一設置

tag 和 logs 在opentarcing中提到一些推薦命名:語義慣例

使用 tag 是用于描述 span 中的特性,是對整個過程而言,而 log 是用于記錄 span 這個過程中的一個時間,因為記錄 log 時會攜帶一個發生的時間戳,是有先后之分的。

baggage

相比 tag,log 限制在 span 中, baggage 同樣提供保存鍵值對設置,但是 baggage 數據有效是全 trace 的,所以使用的時候避免設置不必要的值,導致傳遞開銷。

// set
span.SetBaggageItem("greeting", greeting)
// get
greeting := span.BaggageItem("greeting")

使用上下文傳遞 span

當我們提到調用鏈,一般涉及多個函數,多個進程甚至多個機器上運行的過程,用 tracer 開啟 root span 后,需要向其他過程傳遞以保持他們之間的關聯性,我們通過上下文來存儲 span 并傳遞。

// 存儲到 context 中
ctx := context.Background()
ctx = opentracing.ContextWithSpan(ctx, span)
//....

// 其他過程獲取并開始子 span
span, ctx := opentracing.StartSpanFromContext(ctx, "newspan")
defer span.Finish()
// StartSpanFromContext 會將新span保存到ctx中更新

或者先取出 parent span,然后在以 childof 開啟span,需要手動寫入新 span 到 ctx中。

//獲取上一級 span
parent := opentracing.SpanFromContext(ctx) 
span1 := opentracing.StartSpan("from-sayhello-1", opentracing.ChildOf(span2.Context()))
...
span1.Finish()
ctx = opentracing.ContextWithSpan(ctx, span2) //更新ctx

span2 := opentracing.StartSpan("from-sayhello-2", opentracing.ChildOf(span2.Context()))
...
span2.Finish()
ctx = opentracing.ContextWithSpan(ctx, span2) //更新ctx

tracing grpc 調用

由于 grpc 調用和服務端都聲明了 UnaryInterceptor 和 StreamInterceptor 兩回調函數,因此只需要重寫這兩個函數,在函數中調用 opentracing 的借口進行鏈路追蹤,并初始化客戶端或者服務端時候注冊進去就可以。

相應的函數已經有現成的包 grpc-opentracing

使用如下:

var tracer opentracing.Tracer = ...
//client
conn, err := grpc.Dial(
    address,
    ... // other options
    grpc.WithUnaryInterceptor(
        otgrpc.OpenTracingClientInterceptor(tracer)),
    grpc.WithStreamInterceptor(
        otgrpc.OpenTracingStreamClientInterceptor(tracer)))


// server
s := grpc.NewServer(
    ... // other options
    grpc.UnaryInterceptor(
        otgrpc.OpenTracingServerInterceptor(tracer)),
    grpc.StreamInterceptor(
        otgrpc.OpenTracingStreamServerInterceptor(tracer)))

參考

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 在各大廠分布式鏈路跟蹤系統架構對比中已經介紹了幾大框架的對比,如果想用免費的可以用zipkin和pinpoint還...
    歡醉閱讀 1,903評論 2 2
  • 分布式鏈路追蹤(Distributed Tracing),也叫 分布式鏈路跟蹤,分布式跟蹤,分布式追蹤 等等。 本...
    Daniel_adu閱讀 33,761評論 0 20
  • 28歲的第一天,很平靜。仿佛是再平常不過的一天。 從凌晨開始,收到了許多朋友的祝福,生在這樣一個特別的日子,最大的...
    Yvonne不是我閱讀 358評論 0 0
  • 今天中午剛打掃完衛生就接到孩子班主任的電話問我接孩子了嗎?我說沒,然后老師說你現在有時間來一趟嗎?我在學校門口等你...
    新的一天從早上開始閱讀 191評論 0 1
  • 十月不遠——給大海,請埋葬我的愛情 十月不遠 遇見你不遠 愛情不遠——上帝盤旋的褲衩下 海水腥咸 因此大海不遠 浪...
    九松子閱讀 275評論 1 1