分布式鏈路追蹤技術


分布式鏈路追蹤技術

概述

在微服務架構的系統中,請求在各服務之間流轉,調用鏈錯綜復雜,一旦出現了問題和異常,很難追查定位,這個時候就需要鏈路追蹤來幫忙了。鏈路追蹤系統能追蹤並記錄請求在系統中的調用順序,調用時間等一系列關鍵信息,從而幫助我們定位異常服務和發現性能瓶頸。

img

微服務的監控主要包含一下三個方面:

  • 通過收集日志,對系統和各個服務的運行狀態進行監控
  • 通過收集量度(Metrics),對系統和各個服務的性能進行監控
  • 通過分布式追蹤,追蹤服務請求是如何在各個分布的組件中進行處理的細節

對於是日志和量度的收集和監控,大家會比較熟悉。常見的日志收集架構包含利用Fluentd對系統日志進行收集,然后利用ELK或者Splunk進行日志分析。而對於性能監控,Prometheus是常見的流行的選擇。

分布式鏈路跟蹤主要功能:

  • 故障快速定位:可以通過調用鏈結合業務日志快速定位錯誤信息。
  • 鏈路性能可視化:各個階段鏈路耗時、服務依賴關系可以通過可視化界面展現出來。
  • 鏈路分析:通過分析鏈路耗時、服務依賴關系可以得到用戶的行為路徑,匯總分析應用在很多業務場景。

基本原理

目前常見的鏈路追蹤系統的原理基本都是根據2010年由谷歌發布的一篇《Dapper, a Large-Scale Distributed Systems Tracing Infrastructure》論文為原型實現的。

img

Trace

指一個請求經過所有服務的路徑,每一條局部鏈路都用一個全局唯一的traceid來標識。

Span

為了表達父子調用關系,引入了span

同一層級parent id相同,span id不同,span id從小到大表示請求的順序。

總結:通過事先在日志中埋點,找出相同traceId的日志,再加上parent id和span id就可以將一條完整的請求調用鏈串聯起來。

Annotations

Dapper中還定義了annotation的概念,用於用戶自定義事件,用來輔助定位問題。

通常包含以下四個Annotaion注解信息,分別對應客戶端和服務端相應事件。

img

調用耗時可以通過T4-T1得到,客戶端發送數據包的網絡耗時可以通過T2-T1實現。

鏈路信息的還原依賴於帶內和帶外兩種數據。

帶外數據是各個節點產生的事件,如cs,ss,這些數據可以由節點獨立生成,並且需要集中上報到存儲端。

帶內數據如traceid,spanid,parentid,這些數據需要從鏈路的起點一直傳遞到終點。通過帶內數據的傳遞,可以將一個鏈路的所有過程串起來。

采樣和存儲

為了減少性能消耗,避免存儲資源的浪費,dapper並不會上報所有的span數據,而是使用采樣的方式。舉個例子,每秒有1000個請求訪問系統,如果設置采樣率為1/1000,那么只會上報一個請求到存儲端。

鏈路中的span數據經過收集和上報后會集中存儲在一個地方,Dapper使用了BigTable數據倉庫,常用的存儲還有ElasticSearch, HBase, In-memory DB等。

Opentraceing 接口

Opentracing 是分布式鏈路追蹤的一種規范標准,是 CNCF(雲原生計算基金會)下的項目之一。只要某鏈路追蹤系統實現了 Opentracing 規定的接口(interface),符合Opentracing 定義的表現行為,那么就可以說該應用符合 Opentracing 標准。

它的數據模型和谷歌Dapper論文里的如出一轍。

Spans

Span 是一條追蹤鏈路中的基本組成要素,一個 Span 表示一個獨立的工作單元,比如可以表示一次函數調用,一次 HTTP 請求等等。Span 會記錄如下基本要素:

  • 服務名稱(Operation name)

  • 服務的開始時間和結束時間

  • K/V形式的Tags

    保存用戶自定義標簽,主要用於鏈路追蹤結果的查詢過濾。Span 中的 tag 僅自己可見,不會隨着 SpanContext 傳遞給后續 Span。

  • K/V形式的Logs

    與 tags 不同的是,logs 還會記錄寫入 logs 的時間,因此 logs 主要用於記錄某些事件發生的時間

  • SpanContext

    SpanContext攜帶着一些用於跨服務通信的(跨進程)數據,主要包含:

    • 足夠在系統中標識該span的信息,比如:span_id,trace_id
    • Baggage Items,為整條追蹤鏈保存跨服務(跨進程)的K/V格式的用戶自定義數據。"Baggage"會隨着trace一同傳播,他因此得名。
  • References:該span對一個或多個span的引用(通過引用SpanContext),有ChildOfFollowsFrom兩種

    t=0            operation name: db_query               t=x

     +-----------------------------------------------------+
     | · · · · · · · · · ·    Span     · · · · · · · · · · |
     +-----------------------------------------------------+

Tags:
- db.instance:"customers"
- db.statement:"SELECT * FROM mytable WHERE foo='bar'"
- peer.address:"mysql://127.0.0.1:3306/customers"

Logs:
- message:"Can't connect to mysql server on '127.0.0.1'(10061)"

SpanContext:
- trace_id:"abc123"
- span_id:"xyz789"
- Baggage Items:
  - special_id:"vsid1738"

Baggage vs Span Tags

  • Baggage在全局范圍內,(伴隨業務系統的調用)跨進程傳輸數據。Span的tag不會進行傳輸,因為他們不會被子級的span繼承。
  • span的tag可以用來記錄業務相關的數據,並存儲於追蹤系統中。實現OpenTracing時,可以選擇是否存儲Baggage中的非業務數據,OpenTracing標准不強制要求實現此特性。

Tracers

Trace表示一次完整的追蹤鏈路,trace由一個或多個span組成。

				[Span A]  ←←←(the root span)
            |
     +------+------+
     |             |
 [Span B]      [Span C] ←←←(Span C 是 Span A 的孩子節點, ChildOf)
     |             |
 [Span D]      +---+-------+
               |           |
           [Span E]    [Span F] >>> [Span G] >>> [Span H]
                                       ↑
                                       ↑
                                       ↑
                         (Span G 在 Span F 后被調用, FollowsFrom)

Tracer接口能創建Spans,還知道如何跨進程邊界注入(序列化)和提取(反序列化)元數據,主要包含三個方面的能力:

  • 開啟一個新的Span.
  • SpanContext注入到carrier中.
  • carrier中提取出SpanContext.
type Tracer interface {
  //     sp := tracer.StartSpan(
	//         "GetFeed",
	//         opentracing.ChildOf(parentSpan.Context()),
	//         opentracing.Tag{"user_agent", loggedReq.UserAgent},
	//         opentracing.StartTime(loggedReq.Timestamp),
	//     )

  StartSpan(operationName string, opts ...StartSpanOption) Span
  
  // Example usage (sans error handling):
	//
	//     carrier := opentracing.HTTPHeadersCarrier(httpReq.Header)
	//     err := tracer.Inject(
	//         span.Context(),
	//         opentracing.HTTPHeaders,
	//         carrier)
	//
  Inject(sm SpanContext, format interface{}, carrier interface{}) error
  
  // Example usage (with StartSpan):
	//
	//
	//     carrier := opentracing.HTTPHeadersCarrier(httpReq.Header)
	//     clientContext, err := tracer.Extract(opentracing.HTTPHeaders, carrier)
	//
	//     // ... assuming the ultimate goal here is to resume the trace with a
	//     // server-side Span:
	//     var serverSpan opentracing.Span
	//     if err == nil {
	//         span = tracer.StartSpan(
	//             rpcMethodName, ext.RPCServerOption(clientContext))
	//     } else {
	//         span = tracer.StartSpan(rpcMethodName)
	//     }
	//
  Extract(format interface{}, carrier interface{}) (SpanContext, error)
}

一旦Tracer對象實例被創建出來,就可以用來手工創建Span,或傳遞該對象到框架或庫中.

為了不強制用戶傳遞Tracer對象,提供了一個全局的GlobalTracer實例來存儲Tracer對象,在任何地方都可以通過該全局實例來獲取Tracer對象.

func SetGlobalTracer(tracer Tracer) {
	globalTracer = registeredTracer{tracer, true}
}

// GlobalTracer returns the global singleton `Tracer` implementation.
// Before `SetGlobalTracer()` is called, the `GlobalTracer()` is a noop
// implementation that drops all data handed to it.
func GlobalTracer() Tracer {
	return globalTracer.tracer
}

當創建一個新的Span且該Span沒有關聯到一個父Span時,一個新的trace就開啟了.當創建一個新的Span時,需要為其定義一個operation name,主要用來幫助確定Span與代碼的關聯關系.

Span之間的關聯關系目前支持ChildOfFollowsFrom

Inject/Extract

為了在分布式系統中跨進程邊界進行追蹤,服務需要具備繼續追蹤每個被客戶端注入追蹤信息的請求.OpenTracing通過提供了InjectExtract方法來實現此目標,將Span的上下文編碼為載體.

/images/tracing_extract.png

Opentracing 提供了 Inject/Extract 用於在請求中注入 SpanContext 或者從請求中提取出 SpanContext。

客戶端通過 Inject 將 SpanContext 注入到載體中,隨着請求一起發送到服務端。

服務端則通過 Extract 將 SpanContext 提取出來,進行后續處理。

carrier := make(opentracing.TextMapCarrier)
err := tracer.Inject(span.Context(), opentracing.TextMap, carrier)
//----------

carrier := make(opentracing.TextMapCarrier)
for k, v := range md {
    carrier.Set(k, v[0])
}

spanctx, err := tracer.Extract(opentracing.TextMap, carrier)
span := tracer.StartSpan(info.FullMethod, opentracing.ChildOf(spanctx))

一個端到端的傳播例子

  • 客戶端進程擁有一個SpanContext實例,准備發起一個基於HTTP協議的RPC請求.
  • 客戶端調用Tracer.Inject(...),傳遞當前的SpanContext實例,采用text map格式,把其作為參數.
  • Inject把text map注入到Carrier中,客戶端程序把數據編碼寫入HTTP協議中(一般是放入headers中).
  • 發起HTTP請求,數據跨進程邊界傳輸.
  • 在服務端,應用程序從HTTP協議中提取text map數據,並初始化為一個Carrier.
  • 服務端程序調用Tracer.Extract(...),傳入text map格式的名稱和上面生成的Carrier.
  • 在沒有數據損壞或其它錯誤的情況下,服務端獲取了一個SpanContext實例,和客戶端的是同一個.

常見框架

框架對比

Google Dapper論文發出來之后,很多公司基於鏈路追蹤的基本原理給出了各自的解決方案,具體如下:

  • Twitter:Zipkin
  • Uber:Jaeger
  • Elastic Stack:Elastic APM
  • Apache:SkyWalking(國內開源愛好者吳晟開源)
  • Naver:Pinpoint(韓國公司開發)
  • 阿里:鷹眼。
  • 大眾點評:Cat。
  • 京東:Hydra

為了便於各系統間能彼此兼容互通,OpenTracing組織制定了一系列標准,旨在讓各系統提供統一的接口。

國內這些基本都沒開源,主要的開源框架對比如下:

img

原理

Zipkin

Zipkin 相對成熟,開源於2012年,同時也比較簡單,Java 系大部分都會選擇 Zipkin。

img

在服務運行的過程中會產生很多鏈路信息,產生數據的地方可以稱之為Reporter。將鏈路信息通過多種傳輸方式如HTTP,RPC,kafka消息隊列等發送到Zipkin的采集器,Zipkin處理后最終將鏈路信息保存到存儲器中。運維人員通過UI界面調用接口即可查詢調用鏈信息。

Jaeger

Jaeger 則是 CNCF 旗下,對 K8s 有較好的兼容性,Go 語言系可能是個不錯的選擇。

jaeger-architecture.png

Jaeger的原理和Zipkin二者的架構有些類似。都是由嵌入到代碼中的client來收集數據,並傳輸到Collector端進行存儲,然后集中通過UI進行展示。

具體流程如下:

  • 1)客戶端通過 6831 端口上報數據給 agent
  • 2)agent通過 14250 端口將數據發送給 collector
  • 3)collector 將數據寫入 kafka
  • 4)Ingester 從 kafka中讀取數據並寫入存儲后端
  • 5)query 從存儲后端查詢數據並展示

另外近兩年基於 ServiceMesh 的 ”無” 侵入式鏈路追蹤也廣受歡迎,似乎是一個被看好的方向,其代表作之一 Istio 便是使用 CNCF 出身的 Jaeger,且 Jaeger 還兼容 Zipkin,在這點上 Jaeger 完勝。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM