一文讀懂 分佈式鏈路追蹤系統 Jaeger

爲什麼需要分佈式鏈路追蹤系統

當代的互聯網的服務,通常都是用複雜的、大規模分佈式集羣來實現的。互聯網應用構建在不同的軟件模塊集上,這些軟件模塊,有可能是由不同的團隊開發、可能使用不同的編程語言來實現、有可能布在了幾千臺服務器,橫跨多個不同的數據中心。因此,就需要一些可以幫助理解系統行爲、用於分析性能問題的工具。

希望解決的問題

  1. 如何快速發現問題?
  2. 如何判斷故障影響範圍?
  3. 如何梳理服務依賴以及依賴的合理性?
  4. 如何分析鏈路性能問題以及實時容量規劃?

分佈式鏈路跟蹤系統原理


分佈式服務的跟蹤系統需要記錄在一次特定的請求後系統中完成的所有工作的信息,爲服務器上每一次你發送和接收動作來收集跟蹤標識符 (message identifiers) 和時間戳(timestamped events)。

OpenTracing 語義標準

OpenTracing 是一個跨編程語言的標準,OpenTracing 中的 Trace(調用鏈)通過歸屬於此調用鏈的 Span 來隱性的定義。特別說明,一條 Trace(調用鏈)可以被認爲是一個由多個 Span 組成的有向無環圖(DAG 圖), Span 與 Span 的關係被命名爲 References。
Span 表示跨度,可以理解成一次方法調用, 一個程序塊的調用或者一次 RPC / 數據庫訪問. 只要是一個具有完整時間週期的程序訪問,都可以被認爲是一個 Span。
Ex: 一條 Trace 由 8 個 Span 組成

Span 因果關係

Span 時間關係

術語

Trace: 一個由多個 Span 組成的有向無環圖(DAG 圖)
Span: 代表系統中具有開始時間和執行時長的邏輯運行單元
References: Span 與 Span 的關係, 關係包括 ChildOf 和 FollowsFrom 兩類
Operation name: Span 的操作名稱
Start Timestamp: Span 的起始時間
Finish Timestamp: Span 的結束時間
Span Tag: 一組鍵值對構成的 Span 標籤集合
Span Log: 一組 span 的日誌集合
SpanContext: Span 上下文對象, 代表跨越進程邊界,傳遞到下級 span 的狀態。
Baggage: 存儲在 SpanContext 中的一個鍵值對 (SpanContext) 集合。它會在一條追蹤鏈路上的所有 span 內全局傳輸
Inject and Extract: SpanContexts 可以通過 Injected 操作向 Carrier 增加,或者通過 Extracted 從 Carrier 中獲取,跨進程通訊數據(例如:HTTP 頭)

現有的系統

什麼是 Jaeger

Jaeger 優勢

  1. 與 Kiali 集成 – 當正確配置時,您可以從 Kiali 控制檯查看 Jaeger 數據
  2. 高可伸縮性 – Jaeger 後端被設計爲沒有單一故障點,並可根據需要進行縮放。
  3. 分佈式上下文發佈 – 允許您通過不同的組件連接數據以創建完整的端到端的 trace。
  4. 與 Zipkin 的後向兼容性 – Jaeger 帶有 API,讓它可以作爲 Zipkin 的直接替代項

Jaeger 基礎組建構成

Jaeger Client:Jaeger client 是 OpenTracing API 的具體語言實現
Jaeger Agent :Jaeger 代理是一個網絡守護進程,它會監聽通過 User Datagram Protocol (UDP) 發送的 Span,併發送到收集程序。
Jaeger Collector :與代理類似,該收集器可以接收 Span,並將其放入內部隊列以便進行處理。這允許收集器立即返回到客戶端 / 代理,而不需要等待 Span 進入存儲。
Storage(Data Store):收集器需要一個持久的存儲後端。可以是 ElasticSearch 或者 Cassandra
Query:Query 是一個從存儲中檢索 trace 的服務

Jaeger 基礎架構圖

Jaeger 與 Go 語言的 gin 框架的結合

組建安裝

ES 安裝

大家可以自行度娘 (略)

Jaeger 安裝

Jaeger 支持 Docker 安裝和二進制安裝。本文以二進制文件安裝爲例

  1. 官網下載二進制文件 https://www.jaegertracing.io/download/
  2. 解壓壓縮包,會看到如下文件
example-hotrod  jaeger-agent  jaeger-all-in-one  jaeger-collector  jaeger-ingester  jaeger-query
  1. 啓動組建
    可以直接啓動 jaeger-all-in-one 完成所有組件的啓動。爲了方便查看各組件配置,我們採用分別啓動, 啓動順序 jaeger-collector --> jaeger-agent
./jaeger-collector --span-storage.type=elasticsearch --es.server-urls=http://10.190.33.138:9200
注:jaeger-collector啓動端口可能存在變更。
./jaeger-agent --reporter.grpc.host-port=10.190.33.138:14250 --reporter.grpc.discovery.min-peers=1
./jaeger-query --span-storage.type=elasticsearch --es.server-urls=http://10.190.33.138:9200

通過以上配置可以清晰的看到組建間各自的關係,至此簡單的 Jaeger 系統搭建完成

與 Gin 框架應用

package middleware
import (
   "github.com/gin-gonic/gin"
   "github.com/opentracing/opentracing-go"
   "log"
)

//jaeger-agent 默認開啓端口
const AgentHost = "10.190.33.138:6831"

//設置GlobalTracer
var TraceCloser io.Closer

func InitTracer(appName string) (err error) {
   cfg :=  config.Configuration{
      Sampler: &config.SamplerConfig{
         Type: jaeger.SamplerTypeConst,
         Param: 1,
      },
      Reporter: &config.ReporterConfig{
         LogSpans:           true,
         LocalAgentHostPort: AgentHost,
      },
   }
   TraceCloser, err = cfg.InitGlobalTracer(appName)
   if err != nil {
      return err
   }
   return nil
}
//最後需要手動關閉,因爲client發送span信息到jaeger-agent是異步發送,直接關閉程序可能會丟失數據。
func Closer() {
   TraceCloser.Close()
}

//編寫middleware攔截器從http header中進行攔截
func Trace() gin.HandlerFunc {
   return func(c *gin.Context) {
      tracer := opentracing.GlobalTracer()
      var span opentracing.Span
      //從http header中進行span攔截
      spanCtx, err := tracer.Extract(opentracing.HTTPHeaders,opentracing.HTTPHeadersCarrier(c.Request.Header))
      if err != nil {
         //未攔截到,當前span作爲root Span
         span = opentracing.StartSpan(c.Request.URL.Path)
         //Finish 將span發送給jaeger-agent
         defer span.Finish()
      } else {
         //攔截到,定義當前Span與header作爲父子Span
         span = opentracing.StartSpan(c.Request.URL.Path,opentracing.ChildOf(spanCtx))
         defer span.Finish()
      }
      log.Println(c.Request.Header)
      //將Span信息注入到gin.context中,供後續使用
      c.Set("ctx-span", span)
      c.Next()
   }

}
//從gin.context獲取Span 信息
func GetSpanFromContext(ctx *gin.Context) (opentracing.Span, bool) {
   spanFromCtx, exists := ctx.Get("ctx-span")
   if exists {
      span, ok := spanFromCtx.(opentracing.Span)
      if ok {
         return span, true
      }
   }
   return nil, false

}
//服務內部Span傳遞,比如調用Mysql、redis、或者某些函數等調用

func Mysql(ctx *gin.Context) interface{}  {
   parentSpan, ok := jaeger.GetSpanFromContext(ctx)
   var span opentracing.Span
   if ok {
      //parent span --> child span
      span = opentracing.StartSpan("Mysql",opentracing.ChildOf(parentSpan.Context()))
   } else {
      //no parent span --> root span
      span = opentracing.StartSpan("Mysql")
   }
   defer span.Finish()
   //do sth
   time.Sleep(time.Second * 3)
   return ""
}
//注入,將span信息注入到http header中用於span信息傳遞(以封裝Get方法爲例)
func Get(ctx *gin.Context,url string) (string,error) {
   client := &http.Client{Timeout: 10 * time.Second}
   req, err := http.NewRequest("GET", url, nil)
   if err != nil {
      return "", err
   }
   span, ok := jaeger.GetSpanFromContext(ctx)
   if ok {
      log.Println(span)
      err = opentracing.GlobalTracer().Inject(span.Context(),opentracing.HTTPHeaders,opentracing.HTTPHeadersCarrier(req.Header))//使用HTTPHeadersCarrier
      if err != nil {
         log.Println(err)
      }
   }

   resp, err := client.Do(req)
   if err != nil {
      return "", err
   }
   defer resp.Body.Close()
   result, _ := ioutil.ReadAll(resp.Body)
   return string(result), nil
}

訪問請求

結果查看
Jaeger Query 自帶 UI, 默認端口 16686
訪問查看 http://10.190.33.138:16686/search
主界面

選擇 Service 點擊 Find Traces (注意查看搜索範圍)

能看到所有的調用 trace, 點擊其中某一個

切換調用拓撲圖

數據探索

1.Span 在 gin.Context 的數據格式
通過打印 Span 信息

2021/07/05 15:25:21 1ecc68d0084d8a17:1ecc68d0084d8a17:0000000000000000:1
2021/07/05 15:25:15 1ecc68d0084d8a17:4b74e9882ebc1b27:1ecc68d0084d8a17:1
2021/07/05 15:25:15 1ecc68d0084d8a17:675653a549918d7a:4b74e9882ebc1b27:1

可以看出,Span 由幾個信息組成 traceid:spanid:parentspanid:flag 組成 flag 決定日誌的採樣率
2.Span 在 http header 中數據格式
通過打印 header 信息

2021/07/05 15:25:21 map[Accept-Encoding:[gzip] Uber-Trace-Id:[1ecc68d0084d8a17:1ecc68d0084d8a17:0000000000000000:1] User-Agent:[Go-http-client/1.1]]

可以看出,其是定義 Key Uber-Trace-Id 將 Span 信息設置成 value
3.ES 中數據結構
查看 ES 索引,可以看到會相應生成兩個索引
jaeger-service-2021-xx-xx 索引
jaeger-span-2021-xx-xx 索引
可以看出,jaeger 將 service 信息和 span 信息分開存儲。

jaeger-service 索引中數據

{
  "_index": "jaeger-service-2021-06-30",
  "_type": "_doc",
  "_id": "e322a9972f09925a",
  "_version": 1,
  "_score": 0,
  "fields": {
    "operationName": [
      "/serverA/to/serverB"
    ],
    "serviceName": [
      "quick-gin"
    ]
  }
}

數據含義顯而易見,不需要過多說明

jaeger-span 索引中的數據


能看出 references 保存着相關 parent 的 span 信息,duration 存儲耗時信息,也就是 span 定義到 Finish 的時間。

參考鏈接:
OpenTracing 文檔中文版 https://wu-sheng.gitbooks.io/opentracing-io/content/
Jaeger 官網 https://www.jaegertracing.io/

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/IdAQT3tNsCsGFwOXLf7Anw