分佈式鏈路追蹤

題外話

微服務架構 作爲雲原生核心技術之一,提倡將單一應用程序劃分成一組小的服務(微服務),服務之間互相協調、互相配合,爲用戶提供最終價值。

但數量龐大的微服務實例治理起來給我們帶來了很多問題,通常的做法都是引入相應組件完成,如 API 網關 (apisix, kong, traefik) 負責認證鑑權、負載均衡、限流和靜態響應處理;服務註冊與發現中心 (Consul, Etcd, ZooKeeper) 負責管理維護微服務實例,記錄服務實例元數據;可觀察性方面包括 Metrics 監控 (Prometheus) 負責性能指標統計告警,Logging 日誌 (Loki, ELK) 負責日誌的收集查看,Tracing 鏈路追蹤 (OpenTracing, Jaeger) 負責追蹤具體的請求和繪製調用的拓撲關係。對於這種需要自行引入各種組件完成微服務治理的稱爲 侵入式架構 ,與之相對應的另外一種做法就是未來微服務架構 —— 服務網格 (Service Mesh)

正文

本文主要介紹可觀察性的鏈路追蹤模塊,我將按以下幾個大綱逐步演進:

OpenTracing 介紹

起源

實現分佈式追蹤的方式一般是在程序代碼中進行埋點,採集調用的相關信息後發送到後端的一個追蹤服務器進行分析處理。在這種實現方式中,應用代碼需要依賴於追蹤服務器的 API,導致業務邏輯和追蹤的邏輯耦合。爲了解決該問題,CNCF (雲原生計算基金會)下的 OpenTracing 項目定義了一套分佈式追蹤的標準,以統一各種分佈式追蹤系統的實現。OpenTracing 中包含了一套分佈式追蹤的標準規範,各種語言的 API,以及實現了該標準的編程框架和函數庫。參考 [1]

OpenTracing 提供了平臺無關、廠商無關的 API,因此開發者只需要對接 OpenTracing API,無需關心後端採用的到底是什麼分佈式追蹤系統,Jager、Skywalking、LightStep 等都可以無縫切換。

數據模型

OpenTracing 定義了以下數據模型:

總結:多個 Span 共同組成一個有向無環圖(DAG)形成了 Trace ,SpanContext 則用於將一個 Span 的上下文傳遞到其下游的 Span 中,以將這些 Span 關聯起來。

例如:下面的示例 Trace 就是由 8 個 Span 組成的:參考 [2]

以樹的結構展示 Trace 調用鏈:

基於時間軸的時序圖展示 Trace 調用鏈:

單個Trace中,span間的時間關係


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

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

OpenTracing API for Go

以官方博客例子爲例 [3]

安裝

go get github.com/opentracing/opentracing-go

創建 main.go ,實現一個 Web 服務,並在請求流程中使用 OpenTracing API 進行埋點處理。

Show me the code !

package main

import (
 "fmt"
 "log"
 "math/rand"
 "net/http"
 "time"

 "github.com/opentracing/opentracing-go"
)

func main() {
 port := 8080
 addr := fmt.Sprintf(":%d", port)
 mux := http.NewServeMux()
 mux.HandleFunc("/", indexHandler)
 mux.HandleFunc("/home", homeHandler)
 mux.HandleFunc("/async", serviceHandler)
 mux.HandleFunc("/service", serviceHandler)
 mux.HandleFunc("/db", dbHandler)
 fmt.Printf("http://localhost:%d\n", port)
 log.Fatal(http.ListenAndServe(addr, mux))
}

// 主頁 Html
func indexHandler(w http.ResponseWriter, r *http.Request) {
 w.Write([]byte(`<a href="/home"> 點擊開始發起請求 </a>`))
}

func homeHandler(w http.ResponseWriter, r *http.Request) {
 w.Write([]byte("開始請求...\n"))

 // 在入口處設置一個根節點 span
 span := opentracing.StartSpan("請求 /home")
 defer span.Finish()

 // 發起異步請求
 asyncReq, _ := http.NewRequest("GET""http://localhost:8080/async", nil)
 // 傳遞span的上下文信息
 // 將關於本地追蹤調用的span context,設置到http header上,並傳遞出去
 err := span.Tracer().Inject(span.Context(),
  opentracing.TextMap,
  opentracing.HTTPHeadersCarrier(asyncReq.Header))
 if err != nil {
  log.Fatalf("[asyncReq]無法添加span context到http header: %v", err)
 }
 go func() {
  if _, err := http.DefaultClient.Do(asyncReq); err != nil {
   // 請求失敗,爲span設置tags和logs
   span.SetTag("error"true)
   span.LogKV(fmt.Sprintf("請求 /async error: %v", err))
  }
 }()

 time.Sleep(time.Duration(rand.Intn(200)) * time.Millisecond)

 // 發起同步請求
 syncReq, _ := http.NewRequest("GET""http://localhost:8080/service", nil)
 err = span.Tracer().Inject(span.Context(),
  opentracing.TextMap,
  opentracing.HTTPHeadersCarrier(syncReq.Header))
 if err != nil {
  log.Fatalf("[syncReq]無法添加span context到http header: %v", err)
 }
 if _, err = http.DefaultClient.Do(syncReq); err != nil {
  span.SetTag("error"true)
  span.LogKV(fmt.Sprintf("請求 /service error: %v", err))
 }
 w.Write([]byte("請求結束!"))
}

// 模擬業務請求
func serviceHandler(w http.ResponseWriter, r *http.Request) {
 // 通過http header,提取span元數據信息
 var sp opentracing.Span
 opName := r.URL.Path
 wireContext, err := opentracing.GlobalTracer().Extract(
  opentracing.TextMap,
  opentracing.HTTPHeadersCarrier(r.Header))
 if err != nil {
  // 獲取失敗,則直接新建一個根節點 span
  sp = opentracing.StartSpan(opName)
 } else {
  sp = opentracing.StartSpan(opName, opentracing.ChildOf(wireContext))
 }
 defer sp.Finish()

 dbReq, _ := http.NewRequest("GET""http://localhost:8080/db", nil)
 err = sp.Tracer().Inject(sp.Context(),
  opentracing.TextMap,
  opentracing.HTTPHeadersCarrier(dbReq.Header))
 if err != nil {
  log.Fatalf("[dbReq]無法添加span context到http header: %v", err)
 }
 if _, err = http.DefaultClient.Do(dbReq); err != nil {
  sp.SetTag("error"true)
  sp.LogKV("請求 /da error", err)
 }

 time.Sleep(time.Duration(rand.Intn(200)) * time.Millisecond)
}

// 模擬DB調用
func dbHandler(w http.ResponseWriter, r *http.Request) {
 // 通過http header,提取span元數據信息
 var sp opentracing.Span
 opName := r.URL.Path
 wireContext, err := opentracing.GlobalTracer().Extract(
  opentracing.TextMap,
  opentracing.HTTPHeadersCarrier(r.Header))
 if err != nil {
  // 獲取失敗,則直接新建一個根節點 span
  sp = opentracing.StartSpan(opName)
 } else {
  sp = opentracing.StartSpan(opName, opentracing.ChildOf(wireContext))
 }
 defer sp.Finish()

 time.Sleep(time.Duration(rand.Intn(200)) * time.Millisecond)
}

最後,只需要在應用程序啓動時連接到任意實現了 OpenTracing 標準的鏈路追蹤系統即可。詳見下文的 Jaeger 使用。

Jaeger 介紹

Jaeger 受 Dapper 和 OpenZipkin 的啓發,是 Uber Technologies 開源的分佈式跟蹤系統,遵循 OpenTracing 標準,功能包括:

架構

Jaeger 既可以部署爲一體式二進制文件 (ALL IN ONE),其中所有 Jaeger 後端組件都運行在單個進程中,也可以部署爲可擴展的分佈式系統 (高可用架構)

主要有以下幾個組件:

Jaeger 部署

Jaeger 部署方案主要圍繞以下幾個方面:

仁者見仁智者見智,結合自身業務場景選擇適合自己的即可。

本文爲了簡化操作,就以 Operator + Jaeger Agent sidecar + memory + ALL IN ONE 爲例。

  1. 在 Kubernetes 上安裝 Jaeger Operator
# 創建 observability 命名空間
kubectl create namespace observability
# 創建 crd 資源
kubectl create -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/master/deploy/crds/jaegertracing.io_jaegers_crd.yaml
# 聲明用戶權限
kubectl create -n observability -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/master/deploy/service_account.yaml
kubectl create -n observability -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/master/deploy/role.yaml
kubectl create -n observability -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/master/deploy/role_binding.yaml
# 部署 Jaeger Operator
kubectl create -n observability -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/master/deploy/operator.yaml
  1. 獲得集羣範圍的權限,可選
kubectl create -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/master/deploy/cluster_role.yaml
kubectl create -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/master/deploy/cluster_role_binding.yaml
  1. 查看 Jaeger Operator 是否部署成功
$ kubectl get deployment jaeger-operator -n observability
NAME              READY   UP-TO-DATE   AVAILABLE   AGE
jaeger-operator   1/1     1            1           10s
  1. 使用 Jaeger Operator 部署 Jaeger ,創建 Jaeger 定製資源 參考 [4]
apiVersion: jaegertracing.io/v1
kind: Jaeger
metadata:
  name: my-jaeger
spec:
  strategy: allInOne # 部署策略
  allInOne:
    image: jaegertracing/all-in-one:latest
    options:
      log-level: debug # 日誌等級
  storage:
    type: memory # 可選 Cassandra、Elasticsearch
    options:
      memory:
        max-traces: 100000
  ingress:
    enabled: false
  agent:
    strategy: sidecar # 代理部署策略可選 DaemonSet
  query:
    serviceType: NodePort # 用戶界面使用 NodePort
$ kubectl apply -f my-jaeger.yaml -n observability
jaeger.jaegertracing.io/my-jaeger created

$ kubectl get jaeger -n observability
NAME        STATUS   VERSION   STRATEGY   STORAGE   AGE
my-jaeger                      allinone   memory    10s

$ kubectl get svc -n observability
NAME                           TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)                                  AGE
jaeger-operator-metrics        ClusterIP   10.103.46.73     <none>        8383/TCP,8686/TCP                        3m33s
my-jaeger-agent                ClusterIP   None             <none>        5775/UDP,5778/TCP,6831/UDP,6832/UDP      15s
my-jaeger-collector            ClusterIP   10.111.136.244   <none>        9411/TCP,14250/TCP,14267/TCP,14268/TCP   15s
my-jaeger-collector-headless   ClusterIP   None             <none>        9411/TCP,14250/TCP,14267/TCP,14268/TCP   15s
my-jaeger-query                NodePort    10.105.255.201   <none>        16686:32710/TCP,16685:32493/TCP          15s

訪問 jaeger 用戶界面 http:// 集羣域名: 32710

恭喜成功看到土撥鼠。

Jaeger 使用

繼續回到上文的 OpenTracing API for Go 示例,現在就可以將我們的應用程序連接到 Jaeger 了。

安裝 Jaeger Client Go

go get -u github.com/uber/jaeger-client-go

main.go 添加 init 初始化函數

func init() {
 cfg := jaegercfg.Configuration{
  Sampler: &jaegercfg.SamplerConfig{
   Type:  jaeger.SamplerTypeConst,
   Param: 1,
  },
  Reporter: &jaegercfg.ReporterConfig{
   LogSpans: true,
  },
 }
 _, err := cfg.InitGlobalTracer(
  "jaeger-example", // 服務名
  jaegercfg.Logger(jaegerlog.StdLogger),
  jaegercfg.Metrics(metrics.NullFactory),
 )
 if err != nil {
  panic(err)
 }
}

將應用部署到 k8s 集羣

$ kubectl apply -f https://raw.githubusercontent.com/togettoyou/jaeger-example/master/jaeger-example.yaml -n observability
deployment.apps/jaeger-example created
service/jaeger-example-service created

$ kubectl get svc jaeger-example-service -n observability
NAME                     TYPE       CLUSTER-IP     EXTERNAL-IP   PORT(S)          AGE
jaeger-example-service   NodePort   10.106.2.139   <none>        8080:32668/TCP   11s

提示:要使 jaeger 能夠自動爲我們的應用注入邊車代理,只需要在部署的 Deployment 資源中添加 "sidecar.jaegertracing.io/inject": "true" 的註釋

訪問 http:// 集羣域名: 32668

訪問 jaeger 用戶界面

查看剛纔的調用鏈:

總結

本文主要介紹了 OpenTracing 以及 jaeger 之間的關係和使用方法,OpenTracing 是一個鏈路追蹤的規範,我們可以使用 OpenTracing API 完成代碼的監控埋點,最後可以自由選擇連接遵循 OpenTracing 標準的鏈路追蹤系統,比如 jaeger 。

本文所有代碼均託管在 github.com/togettoyou/jaeger-example[5]

參考資料

[1]

istio-handbook/practice/opentracing: https://www.servicemesher.com/istio-handbook/practice/opentracing.html

[2]

opentracing-specification-zh: https://github.com/opentracing-contrib/opentracing-specification-zh/blob/master/specification.md

[3]

opentracing-io/quick-start: https://wu-sheng.gitbooks.io/opentracing-io/content/pages/quick-start.html

[4]

jaeger-operator: https://github.com/jaegertracing/jaeger-operator/tree/master/examples

[5]

github.com/togettoyou/jaeger-example: https://github.com/togettoyou/jaeger-example

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