分布式鏈路追蹤
題外話
微服務(wù)架構(gòu) 作為云原生核心技術(shù)之一,提倡將單一應(yīng)用程序劃分成一組小的服務(wù)(微服務(wù)),服務(wù)之間互相協(xié)調(diào)、互相配合,為用戶提供最終價(jià)值。但數(shù)量龐大的微服務(wù)實(shí)例治理起來(lái)給我們帶來(lái)了很多問題,通常的做法都是引入相應(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)的另外一種做法就是未來(lái)微服務(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)無(wú)關(guān)、廠商無(wú)關(guān)的 API,因此開發(fā)者只需要對(duì)接 OpenTracing API,無(wú)需關(guān)心后端采用的到底是什么分布式追蹤系統(tǒng),Jager、Skywalking、LightStep 等都可以無(wú)縫切換。數(shù)據(jù)模型
OpenTracing 定義了以下數(shù)據(jù)模型:
- Trace (調(diào)用鏈):一個(gè) Trace 代表一個(gè)事務(wù)或者流程在(分布式)系統(tǒng)中的執(zhí)行過(guò)程。例如來(lái)自客戶端的一個(gè)請(qǐng)求從接收到處理完成的過(guò)程就是一個(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 通過(guò)某種序列化協(xié)議 (Wire Protocol) 在進(jìn)程邊界上進(jìn)行傳遞,以將不同進(jìn)程中的 Span 關(guān)聯(lián)到同一個(gè) Trace 上。對(duì)于 HTTP 請(qǐng)求來(lái)說(shuō),SpanContext 一般是采用 HTTP header 進(jìn)行傳遞的。
單個(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(`?點(diǎn)擊開始發(fā)起請(qǐng)求?`))
}
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]無(wú)法添加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]無(wú)法添加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)?{
?//?通過(guò)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]無(wú)法添加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)?{
?//?通過(guò)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)。它們可以用來(lái)為各種現(xiàn)有開源框架提供分布式追蹤工具。
- Jaeger Agent : Jaeger 代理是一個(gè)網(wǎng)絡(luò)守護(hù)進(jìn)程,它會(huì)監(jiān)聽通過(guò) UDP 發(fā)送的 span,并發(fā)送到收集程序。這個(gè)代理應(yīng)被放置在要管理的應(yīng)用程序的同一主機(jī)上。這通常是通過(guò)如 Kubernetes 等容器環(huán)境中的 sidecar 來(lái)實(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
- 在 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?????????????8383/TCP,8686/TCP????????????????????????3m33s
my-jaeger-agent????????????????ClusterIP???None?????????????????????5775/UDP,5778/TCP,6831/UDP,6832/UDP??????15s
my-jaeger-collector????????????ClusterIP???10.111.136.244???????????9411/TCP,14250/TCP,14267/TCP,14268/TCP???15s
my-jaeger-collector-headless???ClusterIP???None?????????????????????9411/TCP,14250/TCP,14267/TCP,14268/TCP???15s
my-jaeger-query????????????????NodePort????10.105.255.201???????????16686:32710/TCP,16685:32493/TCP??????????15s
訪問 jaeger 用戶界面 http://集群域名:32710恭喜成功看到土撥鼠。Jaeger 使用
繼續(xù)回到上文的 OpenTracing API for Go 示例,現(xiàn)在就可以將我們的應(yīng)用程序連接到 Jaeger 了。安裝 Jaeger Client Gogo?get?-u?github.com/uber/jaeger-client-go
為 main.go
添加 init
初始化函數(shù)func?init()?{
?cfg?:=?jaegercfg.Configuration{
??Sampler:?