分布式鏈路追蹤
題外話
微服務(wù)架構(gòu) 作為云原生核心技術(shù)之一,提倡將單一應(yīng)用程序劃分成一組小的服務(wù)(微服務(wù)),服務(wù)之間互相協(xié)調(diào)、互相配合,為用戶提供最終價(jià)值。
但數(shù)量龐大的微服務(wù)實(shí)例治理起來給我們帶來了很多問題,通常的做法都是引入相應(yīng)組件完成,如 API 網(wǎng)關(guān) ( apisix, kong, traefik ) 負(fù)責(zé)認(rèn)證鑒權(quán)、負(fù)載均衡、限流和靜態(tài)響應(yīng)處理;服務(wù)注冊(cè)與發(fā)現(xiàn)中心 ( Consul, Etcd, ZooKeeper ) 負(fù)責(zé)管理維護(hù)微服務(wù)實(shí)例,記錄服務(wù)實(shí)例元數(shù)據(jù);可觀察性方面包括 Metrics 監(jiān)控 ( Prometheus ) 負(fù)責(zé)性能指標(biāo)統(tǒng)計(jì)告警,Logging 日志 ( Loki, ELK ) 負(fù)責(zé)日志的收集查看,Tracing 鏈路追蹤 ( OpenTracing, Jaeger ) 負(fù)責(zé)追蹤具體的請(qǐng)求和繪制調(diào)用的拓?fù)潢P(guān)系。對(duì)于這種需要自行引入各種組件完成微服務(wù)治理的稱為 侵入式架構(gòu) ,與之相對(duì)應(yīng)的另外一種做法就是未來微服務(wù)架構(gòu) —— 服務(wù)網(wǎng)格 ( Service Mesh ) 。
正文
本文主要介紹可觀察性的鏈路追蹤模塊,我將按以下幾個(gè)大綱逐步演進(jìn):
OpenTracing 介紹 Jaeger 介紹 Jaeger 部署 Jaeger 使用
OpenTracing 介紹
起源
實(shí)現(xiàn)分布式追蹤的方式一般是在程序代碼中進(jìn)行埋點(diǎn),采集調(diào)用的相關(guān)信息后發(fā)送到后端的一個(gè)追蹤服務(wù)器進(jìn)行分析處理。在這種實(shí)現(xiàn)方式中,應(yīng)用代碼需要依賴于追蹤服務(wù)器的 API,導(dǎo)致業(yè)務(wù)邏輯和追蹤的邏輯耦合。為了解決該問題,CNCF (云原生計(jì)算基金會(huì))下的 OpenTracing 項(xiàng)目定義了一套分布式追蹤的標(biāo)準(zhǔn),以統(tǒng)一各種分布式追蹤系統(tǒng)的實(shí)現(xiàn)。OpenTracing 中包含了一套分布式追蹤的標(biāo)準(zhǔn)規(guī)范,各種語(yǔ)言的 API,以及實(shí)現(xiàn)了該標(biāo)準(zhǔn)的編程框架和函數(shù)庫(kù)。參考[1]
OpenTracing 提供了平臺(tái)無關(guān)、廠商無關(guān)的 API,因此開發(fā)者只需要對(duì)接 OpenTracing API,無需關(guān)心后端采用的到底是什么分布式追蹤系統(tǒng),Jager、Skywalking、LightStep 等都可以無縫切換。
數(shù)據(jù)模型

OpenTracing 定義了以下數(shù)據(jù)模型:
Trace (調(diào)用鏈):一個(gè) Trace 代表一個(gè)事務(wù)或者流程在(分布式)系統(tǒng)中的執(zhí)行過程。例如來自客戶端的一個(gè)請(qǐng)求從接收到處理完成的過程就是一個(gè) Trace。 Span(跨度):Span 是分布式追蹤的最小跟蹤單位,一個(gè) Trace 由多段 Span 組成??梢员焕斫鉃橐淮畏椒ㄕ{(diào)用, 一個(gè)程序塊的調(diào)用, 或者一次 RPC/數(shù)據(jù)庫(kù)訪問。只要是一個(gè)具有完整時(shí)間周期的程序訪問,都可以被認(rèn)為是一個(gè) Span。 SpanContext(跨度上下文):分布式追蹤的上下文信息,包括 Trace id,Span id 以及其它需要傳遞到下游服務(wù)的內(nèi)容。一個(gè) OpenTracing 的實(shí)現(xiàn)需要將 SpanContext 通過某種序列化協(xié)議 (Wire Protocol) 在進(jìn)程邊界上進(jìn)行傳遞,以將不同進(jìn)程中的 Span 關(guān)聯(lián)到同一個(gè) Trace 上。對(duì)于 HTTP 請(qǐng)求來說,SpanContext 一般是采用 HTTP header 進(jìn)行傳遞的。
總結(jié):多個(gè) Span 共同組成一個(gè)有向無環(huán)圖(DAG)形成了 Trace ,SpanContext 則用于將一個(gè) Span 的上下文傳遞到其下游的 Span 中,以將這些 Span 關(guān)聯(lián)起來。
例如:下面的示例 Trace 就是由 8 個(gè) Span 組成的:參考[2]
以樹的結(jié)構(gòu)展示 Trace 調(diào)用鏈:
單個(gè)Trace中,span間的因果關(guān)系
[Span A] ←←←(the root span)
|
+------+------+
| |
[Span B] [Span C] ←←←(Span C 是 Span A 的孩子節(jié)點(diǎn), ChildOf)
| |
[Span D] +---+-------+
| |
[Span E] [Span F] >>> [Span G] >>> [Span H]
↑
↑
↑
(Span G 在 Span F 后被調(diào)用, FollowsFrom)
基于時(shí)間軸的時(shí)序圖展示 Trace 調(diào)用鏈:
單個(gè)Trace中,span間的時(shí)間關(guān)系
––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–> 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
創(chuàng)建 main.go ,實(shí)現(xiàn)一個(gè) Web 服務(wù),并在請(qǐng)求流程中使用 OpenTracing API 進(jìn)行埋點(diǎn)處理。
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))
}
// 主頁(yè) Html
func indexHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`<a href="/home"> 點(diǎn)擊開始發(fā)起請(qǐng)求 </a>`))
}
func homeHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("開始請(qǐng)求...\n"))
// 在入口處設(shè)置一個(gè)根節(jié)點(diǎn) span
span := opentracing.StartSpan("請(qǐng)求 /home")
defer span.Finish()
// 發(fā)起異步請(qǐng)求
asyncReq, _ := http.NewRequest("GET", "http://localhost:8080/async", nil)
// 傳遞span的上下文信息
// 將關(guān)于本地追蹤調(diào)用的span context,設(shè)置到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 {
// 請(qǐng)求失敗,為span設(shè)置tags和logs
span.SetTag("error", true)
span.LogKV(fmt.Sprintf("請(qǐng)求 /async error: %v", err))
}
}()
time.Sleep(time.Duration(rand.Intn(200)) * time.Millisecond)
// 發(fā)起同步請(qǐng)求
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("請(qǐng)求 /service error: %v", err))
}
w.Write([]byte("請(qǐng)求結(jié)束!"))
}
// 模擬業(yè)務(wù)請(qǐng)求
func serviceHandler(w http.ResponseWriter, r *http.Request) {
// 通過http header,提取span元數(shù)據(jù)信息
var sp opentracing.Span
opName := r.URL.Path
wireContext, err := opentracing.GlobalTracer().Extract(
opentracing.TextMap,
opentracing.HTTPHeadersCarrier(r.Header))
if err != nil {
// 獲取失敗,則直接新建一個(gè)根節(jié)點(diǎn) 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("請(qǐng)求 /db error", err)
}
time.Sleep(time.Duration(rand.Intn(200)) * time.Millisecond)
}
// 模擬DB調(diào)用
func dbHandler(w http.ResponseWriter, r *http.Request) {
// 通過http header,提取span元數(shù)據(jù)信息
var sp opentracing.Span
opName := r.URL.Path
wireContext, err := opentracing.GlobalTracer().Extract(
opentracing.TextMap,
opentracing.HTTPHeadersCarrier(r.Header))
if err != nil {
// 獲取失敗,則直接新建一個(gè)根節(jié)點(diǎn) 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)
}
最后,只需要在應(yīng)用程序啟動(dòng)時(shí)連接到任意實(shí)現(xiàn)了 OpenTracing 標(biāo)準(zhǔn)的鏈路追蹤系統(tǒng)即可。詳見下文的 Jaeger 使用。
Jaeger 介紹
Jaeger 受 Dapper 和 OpenZipkin 的啟發(fā),是 Uber Technologies 開源的分布式跟蹤系統(tǒng),遵循 OpenTracing 標(biāo)準(zhǔn),功能包括:
分布式上下文傳播 監(jiān)控分布式事務(wù) 執(zhí)行根原因分析 服務(wù)依賴分析 優(yōu)化性能和延遲時(shí)間
架構(gòu)
Jaeger 既可以部署為一體式二進(jìn)制文件 (ALL IN ONE),其中所有 Jaeger 后端組件都運(yùn)行在單個(gè)進(jìn)程中,也可以部署為可擴(kuò)展的分布式系統(tǒng) (高可用架構(gòu))

主要有以下幾個(gè)組件:
Jaeger Client : OpenTracing API 的具體語(yǔ)言實(shí)現(xiàn)。它們可以用來為各種現(xiàn)有開源框架提供分布式追蹤工具。 Jaeger Agent : Jaeger 代理是一個(gè)網(wǎng)絡(luò)守護(hù)進(jìn)程,它會(huì)監(jiān)聽通過 UDP 發(fā)送的 span,并發(fā)送到收集程序。這個(gè)代理應(yīng)被放置在要管理的應(yīng)用程序的同一主機(jī)上。這通常是通過如 Kubernetes 等容器環(huán)境中的 sidecar 來實(shí)現(xiàn)的。 Jaeger Collector : 與代理類似,該收集器可以接收 span,并將其放入內(nèi)部隊(duì)列以便進(jìn)行處理。這允許收集器立即返回到客戶端/代理,而不需要等待 span 進(jìn)入存儲(chǔ)。 Storage : 收集器需要一個(gè)持久的存儲(chǔ)后端。Jaeger 帶有一個(gè)可插入的機(jī)制用于 span 存儲(chǔ)。 Query : Query 是一個(gè)從存儲(chǔ)中檢索 trace 的服務(wù)。 Ingester : 可選組件。Jaeger 可以使用 Apache Kafka 作為收集器和實(shí)際后備存儲(chǔ)之間的緩沖。Ingester 是一個(gè)從 Kafka 讀取數(shù)據(jù)并寫入另一個(gè)存儲(chǔ)后端的服務(wù)。 Jaeger Console : Jaeger 提供了一個(gè)用戶界面,可讓您可視覺地查看所分發(fā)的追蹤數(shù)據(jù)。在搜索頁(yè)面中,您可以查找 trace,并查看組成一個(gè)獨(dú)立 trace 的 span 詳情。
Jaeger 部署
Jaeger 部署方案主要圍繞以下幾個(gè)方面:
ALL IN ONE 還是分布式 后端存儲(chǔ)的選擇(Elasticsearch、Cassandra 甚至 memory) 是否引入 Kafka 作為中間緩沖器 Jaeger Agent 代理安裝方式:sidecar 還是 DaemonSet 安裝工具的選擇:Operator 還是 Helm chart
仁者見仁智者見智,結(jié)合自身業(yè)務(wù)場(chǎng)景選擇適合自己的即可。
本文為了簡(jiǎn)化操作,就以 Operator + Jaeger Agent sidecar + memory + ALL IN ONE 為例。
在 Kubernetes 上安裝 Jaeger Operator
# 創(chuàng)建 observability 命名空間
kubectl create namespace observability
# 創(chuàng)建 crd 資源
kubectl create -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/master/deploy/crds/jaegertracing.io_jaegers_crd.yaml
# 聲明用戶權(quán)限
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
獲得集群范圍的權(quán)限,可選
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 ,創(chuàng)建 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 # 日志等級(jí)
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 使用
繼續(xù)回到上文的 OpenTracing API for Go 示例,現(xiàn)在就可以將我們的應(yīng)用程序連接到 Jaeger 了。
安裝 Jaeger Client Go
go get -u github.com/uber/jaeger-client-go
為 main.go 添加 init 初始化函數(shù)
func init() {
cfg := jaegercfg.Configuration{
Sampler: &jaegercfg.SamplerConfig{
Type: jaeger.SamplerTypeConst,
Param: 1,
},
Reporter: &jaegercfg.ReporterConfig{
LogSpans: true,
},
}
_, err := cfg.InitGlobalTracer(
"jaeger-example", // 服務(wù)名
jaegercfg.Logger(jaegerlog.StdLogger),
jaegercfg.Metrics(metrics.NullFactory),
)
if err != nil {
panic(err)
}
}
將應(yīng)用部署到 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 能夠自動(dòng)為我們的應(yīng)用注入邊車代理,只需要在部署的 Deployment 資源中添加 "sidecar.jaegertracing.io/inject": "true" 的注釋
訪問 http://集群域名:32668


訪問 jaeger 用戶界面

查看剛才的調(diào)用鏈:

總結(jié)
本文主要介紹了 OpenTracing 以及 jaeger 之間的關(guān)系和使用方法,OpenTracing 是一個(gè)鏈路追蹤的規(guī)范,我們可以使用 OpenTracing API 完成代碼的監(jiān)控埋點(diǎn),最后可以自由選擇連接遵循 OpenTracing 標(biāo)準(zhǔn)的鏈路追蹤系統(tǒng),比如 jaeger 。
本文所有代碼均托管在 github.com/togettoyou/jaeger-example[5]
參考資料
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
