[TOC]
鏈路追蹤
當(dāng)代互聯(lián)網(wǎng)服務(wù)龄广,通常都是用復(fù)雜晶渠,大規(guī)模分布式集群來(lái)實(shí)現(xiàn)妄痪,微服務(wù)化萍倡,這些軟件模塊分布在不同的機(jī)器趴久,不同的數(shù)據(jù)中心缀旁,由不同團(tuán)隊(duì)鸳谜,語(yǔ)言開(kāi)發(fā)而成本刽。因此挪蹭,需要工具幫助理解亭饵,分析這些系統(tǒng)、定位問(wèn)題梁厉,做到追蹤每一個(gè)請(qǐng)求的完整調(diào)用鏈路辜羊,收集性能數(shù)據(jù),反饋到服務(wù)治理中词顾,鏈路追蹤系統(tǒng)應(yīng)運(yùn)而生八秃。
現(xiàn)有大部分 APM(Application Performance Management) 理論模型大多借鑒 google dapper 論文,Twitter的zipkin肉盹,Uber的 jaeger昔驱,淘寶的鷹眼,大眾的cat上忍,京東的Hydra等骤肛。
微服務(wù)問(wèn)題:
- 故障定位難
- 鏈路梳理難
- 容量預(yù)估難
舉個(gè)例子纳本,一個(gè)場(chǎng)景下,一個(gè)請(qǐng)求進(jìn)來(lái)腋颠,入口服務(wù)是 serviceA饮醇, serviceA 接到請(qǐng)求后訪問(wèn)數(shù)據(jù)庫(kù)讀取用戶(hù)數(shù)據(jù),然后向 serviceB 發(fā)起 rpc秕豫,serviceB 收到 rpc 請(qǐng)求時(shí)同時(shí)向后端服務(wù) serviceC 和 serviceD 發(fā)起請(qǐng)求,等待請(qǐng)求回復(fù)后再返回 serviceA 的 rpc 調(diào)用观蓄。如果我們發(fā)現(xiàn)發(fā)起的請(qǐng)求失敗混移,或者請(qǐng)求的時(shí)延很大,我們?cè)撊绾稳ザㄎ荒兀?/p>
基于這個(gè)需求侮穿,我們將服務(wù)介入追蹤系統(tǒng)歌径。
分布式追蹤系統(tǒng)發(fā)展很快,種類(lèi)繁多亲茅,但核心步驟一般有三個(gè):代碼埋點(diǎn)回铛,數(shù)據(jù)存儲(chǔ)、查詢(xún)展示
在數(shù)據(jù)采集過(guò)程克锣,需要侵入用戶(hù)代碼做埋點(diǎn)茵肃,不同系統(tǒng)的API不兼容會(huì)導(dǎo)致切換追蹤系統(tǒng)需要做很大的改動(dòng)。為了解決這個(gè)問(wèn)題袭祟,誕生了opentracing 規(guī)范验残。
+-------------+ +---------+ +----------+ +------------+
| Application | | Library | | OSS | | RPC/IPC |
| Code | | Code | | Services | | Frameworks |
+-------------+ +---------+ +----------+ +------------+
| | | |
| | | |
v v v v
+-----------------------------------------------------+
| · · · · · · · · · · OpenTracing · · · · · · · · · · |
+-----------------------------------------------------+
| | | |
| | | |
v v v v
+-----------+ +-------------+ +-------------+ +-----------+
| Tracing | | Logging | | Metrics | | Tracing |
| System A | | Framework B | | Framework C | | System D |
+-----------+ +-------------+ +-------------+ +-----------+
OpenTracing
opentracing (中文)是一套分布式追蹤協(xié)議,與平臺(tái)巾乳,語(yǔ)言無(wú)關(guān)您没,統(tǒng)一接口,方便開(kāi)發(fā)接入不同的分布式追蹤系統(tǒng)胆绊。
語(yǔ)義規(guī)范 : 描述定義的數(shù)據(jù)模型 Tracer氨鹏,Sapn 和 SpanContext 等;
語(yǔ)義慣例 : 羅列出 tag 和 logging 操作時(shí)压状,標(biāo)準(zhǔn)的key值仆抵;
Trace 和 sapn
opentracing 中的 Trace(調(diào)用鏈)通過(guò)歸屬此鏈的 Span 來(lái)隱性定義。一條 Trace 可以認(rèn)為一個(gè)有多個(gè) Span 組成的有向無(wú)環(huán)圖(DAG圖)何缓,Span 是一個(gè)邏輯執(zhí)行單元肢础,Span 與 Span 的因果關(guān)系命名為 References。
opentracing 定義兩種關(guān)系:
- Childof:如下例子中碌廓, SpanC 是 childof SpanA
- FollowsFrom:如下例子中传轰,SpanG 是 followsFrom SpanF
例子 Trace 包含 8個(gè) Span,
[Span A] ←←←(the root span)
|
+------+------+
| |
[Span B] [Span C] ←←←(Span C is a `ChildOf` Span A)
| |
[Span D] +---+-------+
| |
[Span E] [Span F] >>> [Span G] >>> [Span H]
↑
↑
↑
(Span G `FollowsFrom` Span F)
通過(guò)時(shí)間軸顯示一個(gè) Tracer 更加直觀谷婆,
––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–> time
[Span A···················································]
[Span B··············································]
[Span D··········································]
[Span C········································]
[Span E·······] [Span F··] [Span G··] [Span H··]
每個(gè)Span封裝了如下?tīng)顟B(tài):
- 操作名稱(chēng)
- 開(kāi)始時(shí)間戳
- 結(jié)束時(shí)間戳
- 一組零或多個(gè)鍵:值結(jié)構(gòu)的 Span標(biāo)簽 (Tags)慨蛙。鍵必須是字符串辽聊。值可以是字符串,布爾或數(shù)值類(lèi)型.
- 一組零或多個(gè) Span日志 (Logs)期贫,其中每個(gè)都是一個(gè)鍵:值映射并與一個(gè)時(shí)間戳配對(duì)跟匆。鍵必須是字符串,值可以是任何類(lèi)型通砍。 并非所有的 OpenTracing 實(shí)現(xiàn)都必須支持每種值類(lèi)型玛臂。
- 一個(gè) SpanContext (見(jiàn)下文)
- 零或多個(gè)因果相關(guān)的 Span 間的 References (通過(guò)那些相關(guān)的 Span 的 SpanContext )
每個(gè) SpanContext 封裝了如下?tīng)顟B(tài):
任何需要跟跨進(jìn)程 Span 關(guān)聯(lián)的,依賴(lài)于 OpenTracing 實(shí)現(xiàn)的狀態(tài)(例如 Trace 和 Span 的 id)
鍵:值結(jié)構(gòu)的跨進(jìn)程的 Baggage Items(區(qū)別于 span tag封孙,baggage 是全局范圍迹冤,在 span 間保持傳遞,而tag 是 span 內(nèi)部虎忌,不會(huì)被子 span 繼承使用泡徙。)
Inject 和 Extract 操作
跨進(jìn)程,機(jī)器通訊膜蠢,通過(guò)傳遞 Spancontext 來(lái)提供足夠的信息建立 span 間的關(guān)系堪藐。SpanContext 通過(guò) Inject 操作向 Carrier 中增加,傳遞后通過(guò) Extracted 從 Carrier 中取出挑围。
Sampling,采樣
OpenTracing API 不強(qiáng)調(diào)采樣的概念礁竞,但是大多數(shù)追蹤系統(tǒng)通過(guò)不同方式實(shí)現(xiàn)采樣。有些情況下杉辙,應(yīng)用系統(tǒng)需要通知追蹤程序苏章,這條特定的調(diào)用需要被記錄,即使根據(jù)默認(rèn)采樣規(guī)則奏瞬,它不需要被記錄枫绅。sampling.priority tag 提供這樣的方式。追蹤系統(tǒng)不保證一定采納這個(gè)參數(shù)硼端,但是會(huì)盡可能的保留這條調(diào)用并淋。
sampling.priority - integer
如果大于 0, 追蹤系統(tǒng)盡可能保存這條調(diào)用鏈
等于 0, 追蹤系統(tǒng)不保存這條調(diào)用鏈
如果此tag沒(méi)有提供,追蹤系統(tǒng)使用自己的默認(rèn)采樣規(guī)則
OpenTracing 多語(yǔ)言支持
提供不同語(yǔ)言的 API珍昨,用于在自己的應(yīng)用程序中執(zhí)行鏈路記錄县耽。
Jaeger
Jaeger (?yā-g?r) 是Uber開(kāi)發(fā)的一套分布式追蹤系統(tǒng),受啟發(fā)于 dapper 和 OpenZipkin镣典,兼容 OpenTracing 標(biāo)準(zhǔn)兔毙,CNCF的開(kāi)源項(xiàng)目。
系統(tǒng)框架
- Jaeger Client - 為不同語(yǔ)言實(shí)現(xiàn)了符合 OpenTracing 標(biāo)準(zhǔn)的 SDK兄春。應(yīng)用程序通過(guò) API 寫(xiě)入數(shù)據(jù)澎剥,client library 把 trace 信息按照應(yīng)用程序指定的采樣策略傳遞給 jaeger-agent。
- Agent - 是一個(gè)監(jiān)聽(tīng)在 UDP 端口上接收 span 數(shù)據(jù)的網(wǎng)絡(luò)守護(hù)進(jìn)程赶舆,它會(huì)將數(shù)據(jù)批量發(fā)送給 collector哑姚。它被設(shè)計(jì)成一個(gè)基礎(chǔ)組件祭饭,推薦部署到所有的宿主機(jī)上。Agent 將 client library 和 collector 解耦叙量,為 client library 屏蔽了路由和發(fā)現(xiàn) collector 的細(xì)節(jié)倡蝙。
- Collector - 接收 jaeger-agent 發(fā)送來(lái)的數(shù)據(jù),然后將數(shù)據(jù)寫(xiě)入后端存儲(chǔ)绞佩。Collector 被設(shè)計(jì)成無(wú)狀態(tài)的組件寺鸥,因此您可以同時(shí)運(yùn)行任意數(shù)量的 jaeger-collector。
- Data Store - 后端存儲(chǔ)被設(shè)計(jì)成一個(gè)可插拔的組件品山,支持將數(shù)據(jù)寫(xiě)入 cassandra析既、elastic search。
- Query - 接收查詢(xún)請(qǐng)求谆奥,然后從后端存儲(chǔ)系統(tǒng)中檢索 trace 并通過(guò) UI 進(jìn)行展示。Query 是無(wú)狀態(tài)的拂玻,您可以啟動(dòng)多個(gè)實(shí)例酸些,把它們部署在 nginx 這樣的負(fù)載均衡器后面。
官方釋放部署的鏡像到 dockerhub檐蚜,所以部署 jaeger 非常方便魄懂,如果是本地測(cè)試,可以直接用 jaeger 提供的 all-in-one 鏡像部署闯第。
快速搭建市栗,all-in-one
執(zhí)行一下命令,可以在本機(jī)拉起一個(gè) jaeger 環(huán)境咳短,上報(bào)的鏈路數(shù)據(jù)保存在本地內(nèi)存填帽,所以只能用于測(cè)試。
$ docker run -d --name jaeger \
-e COLLECTOR_ZIPKIN_HTTP_PORT=9411 \
-p 5775:5775/udp \kaixiao
-p 6831:6831/udp \
-p 6832:6832/udp \
-p 5778:5778 \
-p 16686:16686 \
-p 14268:14268 \
-p 9411:9411 \
jaegertracing/all-in-one:latest
通過(guò) http://localhost:16686 可以在瀏覽器查看 Jaeger UI
采樣速率
生產(chǎn)環(huán)境系統(tǒng)性能很重要咙好,所以對(duì)于所有的請(qǐng)求都開(kāi)啟 Trace 顯然會(huì)帶來(lái)比較大的壓力篡腌,另外,大量的數(shù)據(jù)也會(huì)帶來(lái)很大存儲(chǔ)壓力勾效。為此嘹悼,jaeger 支持設(shè)置采樣速率,根據(jù)系統(tǒng)實(shí)際情況設(shè)置合適的采樣頻率层宫。
Jaeger 官方提供了多種采集策略杨伙,使用者可以按需選擇使用
- const,全量采集萌腿,采樣率設(shè)置0,1 分別對(duì)應(yīng)打開(kāi)和關(guān)閉
- probabilistic 限匣,概率采集,默認(rèn)萬(wàn)份之一毁菱,0~1之間取值膛腐,
- rateLimiting 睛约,限速采集,每秒只能采集一定量的數(shù)據(jù)
- remote 哲身,一種動(dòng)態(tài)采集策略辩涝,根據(jù)當(dāng)前系統(tǒng)的訪問(wèn)量調(diào)節(jié)采集策略
追蹤實(shí)踐 - go
go 程序中集成鏈路追蹤并上報(bào)到 jaeger 需要用到一下兩個(gè)包 opentracing go api 和 jaeger go 客戶(hù)端。
一個(gè)簡(jiǎn)單的 trace
以下代碼上報(bào)一個(gè)包含一個(gè) span 的 trace勘天,程序在初始化階段通過(guò)環(huán)境變量獲取 jaeger 的配置并初始化全局 tracer怔揩。之后便可以通過(guò)這個(gè) tracer 開(kāi)啟 span(root span) 記錄程序鏈路。
package main
import (
"fmt"
"io"
"time"
opentracing "github.com/opentracing/opentracing-go"
jaeger "github.com/uber/jaeger-client-go"
jaegercfg "github.com/uber/jaeger-client-go/config"
)
// InitJaeger ...
func InitJaeger(service string) (opentracing.Tracer, io.Closer) {
cfg, err := jaegercfg.FromEnv()
/*
cfg.Sampler.Type = "const"
cfg.Sampler.Param = 1
cfg.Reporter.LocalAgentHostPort = "127.0.0.1:6831"
cfg.Reporter.LogSpans = true
*/
tracer, closer, err := cfg.New(service, jaegercfg.Logger(jaeger.StdLogger))
if err != nil {
panic(fmt.Sprintf("ERROR: cannot init Jaeger: %v\n", err))
}
return tracer, closer
}
func main() {
tracer, closer := InitJaeger("hello-world")
defer closer.Close()
opentracing.InitGlobalTracer(tracer)
helloStr := "hello jaeger"
span := tracer.StartSpan("say-hello")
time.Sleep(time.Duration(2) * time.Millisecond)
println(helloStr)
span.Finish()
}
然后通過(guò) jaeger ui 可以看到本次上報(bào)的 trace脯丝。
$ export JAEGER_DISABLED=false
$ export JAEGER_SAMPLER_TYPE="const"
$ export JAEGER_SAMPLER_PARAM=1
$ export JAEGER_REPORTER_LOG_SPANS=true
$ export JAEGER_AGENT_HOST="127.0.0.1"
$ export JAEGER_AGENT_PORT=6831
$ go run ./test.go
2019/06/09 23:01:31 Initializing logging reporter
hello jaeger
2019/06/09 23:01:31 Reporting span 2813d696ced4431:2813d696ced4431:0:1
在開(kāi)啟 span 記錄一個(gè)過(guò)程時(shí)商膊,還可以通過(guò) api 進(jìn)行 tag,logs等操作 宠进,并能在 UI 看到相應(yīng)設(shè)置的鍵z值
span.SetTag("value", helloStr)
span.LogFields(
log.String("event", "sayhello"),
log.String("value", helloStr),
)
//span.LogKV("event", "sayhello") // 單一設(shè)置
tag 和 logs 在opentarcing中提到一些推薦命名:語(yǔ)義慣例
使用 tag 是用于描述 span 中的特性晕拆,是對(duì)整個(gè)過(guò)程而言,而 log 是用于記錄 span 這個(gè)過(guò)程中的一個(gè)時(shí)間材蹬,因?yàn)橛涗?log 時(shí)會(huì)攜帶一個(gè)發(fā)生的時(shí)間戳实幕,是有先后之分的。
baggage
相比 tag堤器,log 限制在 span 中昆庇, baggage 同樣提供保存鍵值對(duì)設(shè)置,但是 baggage 數(shù)據(jù)有效是全 trace 的闸溃,所以使用的時(shí)候避免設(shè)置不必要的值整吆,導(dǎo)致傳遞開(kāi)銷(xiāo)。
// set
span.SetBaggageItem("greeting", greeting)
// get
greeting := span.BaggageItem("greeting")
使用上下文傳遞 span
當(dāng)我們提到調(diào)用鏈辉川,一般涉及多個(gè)函數(shù)表蝙,多個(gè)進(jìn)程甚至多個(gè)機(jī)器上運(yùn)行的過(guò)程,用 tracer 開(kāi)啟 root span 后乓旗,需要向其他過(guò)程傳遞以保持他們之間的關(guān)聯(lián)性勇哗,我們通過(guò)上下文來(lái)存儲(chǔ) span 并傳遞。
// 存儲(chǔ)到 context 中
ctx := context.Background()
ctx = opentracing.ContextWithSpan(ctx, span)
//....
// 其他過(guò)程獲取并開(kāi)始子 span
span, ctx := opentracing.StartSpanFromContext(ctx, "newspan")
defer span.Finish()
// StartSpanFromContext 會(huì)將新span保存到ctx中更新
或者先取出 parent span寸齐,然后在以 childof 開(kāi)啟span欲诺,需要手動(dòng)寫(xiě)入新 span 到 ctx中。
//獲取上一級(jí) span
parent := opentracing.SpanFromContext(ctx)
span1 := opentracing.StartSpan("from-sayhello-1", opentracing.ChildOf(span2.Context()))
...
span1.Finish()
ctx = opentracing.ContextWithSpan(ctx, span2) //更新ctx
span2 := opentracing.StartSpan("from-sayhello-2", opentracing.ChildOf(span2.Context()))
...
span2.Finish()
ctx = opentracing.ContextWithSpan(ctx, span2) //更新ctx
tracing grpc 調(diào)用
由于 grpc 調(diào)用和服務(wù)端都聲明了 UnaryInterceptor 和 StreamInterceptor 兩回調(diào)函數(shù)渺鹦,因此只需要重寫(xiě)這兩個(gè)函數(shù)扰法,在函數(shù)中調(diào)用 opentracing 的借口進(jìn)行鏈路追蹤,并初始化客戶(hù)端或者服務(wù)端時(shí)候注冊(cè)進(jìn)去就可以毅厚。
相應(yīng)的函數(shù)已經(jīng)有現(xiàn)成的包 grpc-opentracing
使用如下:
var tracer opentracing.Tracer = ...
//client
conn, err := grpc.Dial(
address,
... // other options
grpc.WithUnaryInterceptor(
otgrpc.OpenTracingClientInterceptor(tracer)),
grpc.WithStreamInterceptor(
otgrpc.OpenTracingStreamClientInterceptor(tracer)))
// server
s := grpc.NewServer(
... // other options
grpc.UnaryInterceptor(
otgrpc.OpenTracingServerInterceptor(tracer)),
grpc.StreamInterceptor(
otgrpc.OpenTracingStreamServerInterceptor(tracer)))