[TOC]
鏈路追蹤
當代互聯網服務,通常都是用復雜,大規模分布式集群來實現,微服務化,這些軟件模塊分布在不同的機器,不同的數據中心,由不同團隊,語言開發而成。因此,需要工具幫助理解,分析這些系統、定位問題,做到追蹤每一個請求的完整調用鏈路,收集性能數據,反饋到服務治理中,鏈路追蹤系統應運而生。
現有大部分 APM(Application Performance Management) 理論模型大多借鑒 google dapper 論文,Twitter的zipkin,Uber的 jaeger,淘寶的鷹眼,大眾的cat,京東的Hydra等。
微服務問題:
- 故障定位難
- 鏈路梳理難
- 容量預估難
舉個例子,一個場景下,一個請求進來,入口服務是 serviceA, serviceA 接到請求后訪問數據庫讀取用戶數據,然后向 serviceB 發起 rpc,serviceB 收到 rpc 請求時同時向后端服務 serviceC 和 serviceD 發起請求,等待請求回復后再返回 serviceA 的 rpc 調用。如果我們發現發起的請求失敗,或者請求的時延很大,我們該如何去定位呢?
基于這個需求,我們將服務介入追蹤系統。
分布式追蹤系統發展很快,種類繁多,但核心步驟一般有三個:代碼埋點,數據存儲、查詢展示
在數據采集過程,需要侵入用戶代碼做埋點,不同系統的API不兼容會導致切換追蹤系統需要做很大的改動。為了解決這個問題,誕生了opentracing 規范。
+-------------+ +---------+ +----------+ +------------+
| 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 (中文)是一套分布式追蹤協議,與平臺,語言無關,統一接口,方便開發接入不同的分布式追蹤系統。
Trace 和 sapn
opentracing 中的 Trace(調用鏈)通過歸屬此鏈的 Span 來隱性定義。一條 Trace 可以認為一個有多個 Span 組成的有向無環圖(DAG圖),Span 是一個邏輯執行單元,Span 與 Span 的因果關系命名為 References。
opentracing 定義兩種關系:
- Childof:如下例子中, SpanC 是 childof SpanA
- FollowsFrom:如下例子中,SpanG 是 followsFrom SpanF
例子 Trace 包含 8個 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)
通過時間軸顯示一個 Tracer 更加直觀,
––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–> time
[Span A···················································]
[Span B··············································]
[Span D··········································]
[Span C········································]
[Span E·······] [Span F··] [Span G··] [Span H··]
每個Span封裝了如下狀態:
- 操作名稱
- 開始時間戳
- 結束時間戳
- 一組零或多個鍵:值結構的 Span標簽 (Tags)。鍵必須是字符串。值可以是字符串,布爾或數值類型.
- 一組零或多個 Span日志 (Logs),其中每個都是一個鍵:值映射并與一個時間戳配對。鍵必須是字符串,值可以是任何類型。 并非所有的 OpenTracing 實現都必須支持每種值類型。
- 一個 SpanContext (見下文)
- 零或多個因果相關的 Span 間的 References (通過那些相關的 Span 的 SpanContext )
每個 SpanContext 封裝了如下狀態:
任何需要跟跨進程 Span 關聯的,依賴于 OpenTracing 實現的狀態(例如 Trace 和 Span 的 id)
鍵:值結構的跨進程的 Baggage Items(區別于 span tag,baggage 是全局范圍,在 span 間保持傳遞,而tag 是 span 內部,不會被子 span 繼承使用。)
Inject 和 Extract 操作
跨進程,機器通訊,通過傳遞 Spancontext 來提供足夠的信息建立 span 間的關系。SpanContext 通過 Inject 操作向 Carrier 中增加,傳遞后通過 Extracted 從 Carrier 中取出。
Sampling,采樣
OpenTracing API 不強調采樣的概念,但是大多數追蹤系統通過不同方式實現采樣。有些情況下,應用系統需要通知追蹤程序,這條特定的調用需要被記錄,即使根據默認采樣規則,它不需要被記錄。sampling.priority tag 提供這樣的方式。追蹤系統不保證一定采納這個參數,但是會盡可能的保留這條調用。
sampling.priority - integer
如果大于 0, 追蹤系統盡可能保存這條調用鏈
等于 0, 追蹤系統不保存這條調用鏈
如果此tag沒有提供,追蹤系統使用自己的默認采樣規則
OpenTracing 多語言支持
提供不同語言的 API,用于在自己的應用程序中執行鏈路記錄。
Jaeger
Jaeger (?yā-g?r) 是Uber開發的一套分布式追蹤系統,受啟發于 dapper 和 OpenZipkin,兼容 OpenTracing 標準,CNCF的開源項目。
系統框架
- Jaeger Client - 為不同語言實現了符合 OpenTracing 標準的 SDK。應用程序通過 API 寫入數據,client library 把 trace 信息按照應用程序指定的采樣策略傳遞給 jaeger-agent。
- Agent - 是一個監聽在 UDP 端口上接收 span 數據的網絡守護進程,它會將數據批量發送給 collector。它被設計成一個基礎組件,推薦部署到所有的宿主機上。Agent 將 client library 和 collector 解耦,為 client library 屏蔽了路由和發現 collector 的細節。
- Collector - 接收 jaeger-agent 發送來的數據,然后將數據寫入后端存儲。Collector 被設計成無狀態的組件,因此您可以同時運行任意數量的 jaeger-collector。
- Data Store - 后端存儲被設計成一個可插拔的組件,支持將數據寫入 cassandra、elastic search。
- Query - 接收查詢請求,然后從后端存儲系統中檢索 trace 并通過 UI 進行展示。Query 是無狀態的,您可以啟動多個實例,把它們部署在 nginx 這樣的負載均衡器后面。
官方釋放部署的鏡像到 dockerhub,所以部署 jaeger 非常方便,如果是本地測試,可以直接用 jaeger 提供的 all-in-one 鏡像部署。
快速搭建,all-in-one
執行一下命令,可以在本機拉起一個 jaeger 環境,上報的鏈路數據保存在本地內存,所以只能用于測試。
$ 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
通過 http://localhost:16686 可以在瀏覽器查看 Jaeger UI
采樣速率
生產環境系統性能很重要,所以對于所有的請求都開啟 Trace 顯然會帶來比較大的壓力,另外,大量的數據也會帶來很大存儲壓力。為此,jaeger 支持設置采樣速率,根據系統實際情況設置合適的采樣頻率。
Jaeger 官方提供了多種采集策略,使用者可以按需選擇使用
- const,全量采集,采樣率設置0,1 分別對應打開和關閉
- probabilistic ,概率采集,默認萬份之一,0~1之間取值,
- rateLimiting ,限速采集,每秒只能采集一定量的數據
- remote ,一種動態采集策略,根據當前系統的訪問量調節采集策略
追蹤實踐 - go
go 程序中集成鏈路追蹤并上報到 jaeger 需要用到一下兩個包 opentracing go api 和 jaeger go 客戶端。
一個簡單的 trace
以下代碼上報一個包含一個 span 的 trace,程序在初始化階段通過環境變量獲取 jaeger 的配置并初始化全局 tracer。之后便可以通過這個 tracer 開啟 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()
}
然后通過 jaeger ui 可以看到本次上報的 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
在開啟 span 記錄一個過程時,還可以通過 api 進行 tag,logs等操作 ,并能在 UI 看到相應設置的鍵z值
span.SetTag("value", helloStr)
span.LogFields(
log.String("event", "sayhello"),
log.String("value", helloStr),
)
//span.LogKV("event", "sayhello") // 單一設置
tag 和 logs 在opentarcing中提到一些推薦命名:語義慣例
使用 tag 是用于描述 span 中的特性,是對整個過程而言,而 log 是用于記錄 span 這個過程中的一個時間,因為記錄 log 時會攜帶一個發生的時間戳,是有先后之分的。
baggage
相比 tag,log 限制在 span 中, baggage 同樣提供保存鍵值對設置,但是 baggage 數據有效是全 trace 的,所以使用的時候避免設置不必要的值,導致傳遞開銷。
// set
span.SetBaggageItem("greeting", greeting)
// get
greeting := span.BaggageItem("greeting")
使用上下文傳遞 span
當我們提到調用鏈,一般涉及多個函數,多個進程甚至多個機器上運行的過程,用 tracer 開啟 root span 后,需要向其他過程傳遞以保持他們之間的關聯性,我們通過上下文來存儲 span 并傳遞。
// 存儲到 context 中
ctx := context.Background()
ctx = opentracing.ContextWithSpan(ctx, span)
//....
// 其他過程獲取并開始子 span
span, ctx := opentracing.StartSpanFromContext(ctx, "newspan")
defer span.Finish()
// StartSpanFromContext 會將新span保存到ctx中更新
或者先取出 parent span,然后在以 childof 開啟span,需要手動寫入新 span 到 ctx中。
//獲取上一級 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 調用
由于 grpc 調用和服務端都聲明了 UnaryInterceptor 和 StreamInterceptor 兩回調函數,因此只需要重寫這兩個函數,在函數中調用 opentracing 的借口進行鏈路追蹤,并初始化客戶端或者服務端時候注冊進去就可以。
相應的函數已經有現成的包 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)))