分佈式鏈路追蹤
題外話
微服務架構 作爲雲原生核心技術之一,提倡將單一應用程序劃分成一組小的服務(微服務),服務之間互相協調、互相配合,爲用戶提供最終價值。
但數量龐大的微服務實例治理起來給我們帶來了很多問題,通常的做法都是引入相應組件完成,如 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 定義了以下數據模型:
-
Trace (調用鏈):一個 Trace 代表一個事務或者流程在(分佈式)系統中的執行過程。例如來自客戶端的一個請求從接收到處理完成的過程就是一個 Trace。
-
Span(跨度):Span 是分佈式追蹤的最小跟蹤單位,一個 Trace 由多段 Span 組成。可以被理解爲一次方法調用, 一個程序塊的調用, 或者一次 RPC / 數據庫訪問。只要是一個具有完整時間週期的程序訪問,都可以被認爲是一個 Span。
-
SpanContext(跨度上下文):分佈式追蹤的上下文信息,包括 Trace id,Span id 以及其它需要傳遞到下游服務的內容。一個 OpenTracing 的實現需要將 SpanContext 通過某種序列化協議 (Wire Protocol) 在進程邊界上進行傳遞,以將不同進程中的 Span 關聯到同一個 Trace 上。對於 HTTP 請求來說,SpanContext 一般是採用 HTTP header 進行傳遞的。
總結:多個 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 Client : OpenTracing API 的具體語言實現。它們可以用來爲各種現有開源框架提供分佈式追蹤工具。
-
Jaeger Agent : Jaeger 代理是一個網絡守護進程,它會監聽通過 UDP 發送的 span,併發送到收集程序。這個代理應被放置在要管理的應用程序的同一主機上。這通常是通過如 Kubernetes 等容器環境中的 sidecar 來實現的。
-
Jaeger Collector : 與代理類似,該收集器可以接收 span,並將其放入內部隊列以便進行處理。這允許收集器立即返回到客戶端 / 代理,而不需要等待 span 進入存儲。
-
Storage : 收集器需要一個持久的存儲後端。Jaeger 帶有一個可插入的機制用於 span 存儲。
-
Query : Query 是一個從存儲中檢索 trace 的服務。
-
Ingester : 可選組件。Jaeger 可以使用 Apache Kafka 作爲收集器和實際後備存儲之間的緩衝。Ingester 是一個從 Kafka 讀取數據並寫入另一個存儲後端的服務。
-
Jaeger Console : Jaeger 提供了一個用戶界面,可讓您可視覺地查看所分發的追蹤數據。在搜索頁面中,您可以查找 trace,並查看組成一個獨立 trace 的 span 詳情。
Jaeger 部署
Jaeger 部署方案主要圍繞以下幾個方面:
-
ALL IN ONE 還是分佈式
-
後端存儲的選擇(Elasticsearch、Cassandra 甚至 memory)
-
是否引入 Kafka 作爲中間緩衝器
-
Jaeger Agent 代理安裝方式:sidecar 還是 DaemonSet
-
安裝工具的選擇:Operator 還是 Helm chart
仁者見仁智者見智,結合自身業務場景選擇適合自己的即可。
本文爲了簡化操作,就以 Operator + Jaeger Agent sidecar + memory + ALL IN ONE 爲例。
- 在 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
- 獲得集羣範圍的權限,可選
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
- 查看 Jaeger Operator 是否部署成功
$ kubectl get deployment jaeger-operator -n observability
NAME READY UP-TO-DATE AVAILABLE AGE
jaeger-operator 1/1 1 1 10s
- 使用 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