原理 | 分散式鏈路跟蹤元件 SOFATracer 和 Zipkin 模型轉換

螞蟻金服分散式架構發表於2018-09-29

SOFA 中介軟體是螞蟻金服自主研發的金融級分散式中介軟體,包含了構建金融級雲原生架構所需的各個元件,包括微服務研發框架,RPC 框架,服務註冊中心,分散式定時任務,限流/熔斷框架,動態配置推送,分散式鏈路追蹤,Metrics 監控度量,分散式高可用訊息佇列,分散式事務框架,分散式資料庫代理層等元件,是在金融場景裡錘鍊出來的最佳實踐。


SOFATracer 是一個用於分散式系統呼叫跟蹤的元件,通過統一的 TraceId 將呼叫鏈路中的各種網路呼叫情況以日誌的方式記錄下來或者上報到 Zipkin,以達到透視化網路呼叫的目的,這些鏈路資料可用於故障的快速發現,服務治理等。


SOFATracer 的 Github 的地址是:

https://github.com/alipay/sofa-tracer


之前我們發了一篇 實操 | 基於 SOFATracer + Zipkin 實現分散式鏈路跟蹤,本篇將展開進行原理的分析。

SOFATracer 是一個用於分散式系統呼叫跟蹤的元件,通過統一的 traceId 將呼叫鏈路中的各種網路呼叫情況以日誌的方式記錄下來或者上報到 做 Zipkin,以達到透視化網路呼叫的目的。這種以日誌的方式記錄下來或者上報到 Zipkin 通常稱為 Report,即資料上報 SOFATracer 的資料上報是在遵循OpenTracing 規範基礎上擴充套件出來的能力,OpenTracing 規範本身只是約定了資料模型和行為。本文主要目的在於分析 SOFATracer 的資料上報功能部分,主要內容如下:

  • 基於 OpenTracing 規範的分散式鏈路跟蹤解決方案

  • SOFATracer Report 資料上報模型

  • SOFATracer 和 Zipkin 模型轉換原理

基於 OpenTracing 規範的分散式鏈路跟蹤解決方案

OpenTracing 是一個輕量級的標準化層,它位於應用程式/類庫和追蹤或日誌分析程式之間。為了解決不同的分散式追蹤系統 API 不相容的問題,OpenTracing 提供了一套平臺無關、廠商無關的 API,同時也提供了統一的概念和資料標準。關於對 OpenTracing 標準的版本化描述可以參考 https://github.com/opentracing/specification/blob/master/specification.md。一些具體的概念下面將結合 SOFATracer 的實現來一一說明。

目前基於 OpenTracing 規範實現的鏈路跟蹤元件有 Jaeger,Appdash,Apache SkyWalking ,Datadog 等。像谷歌的 StackDriver Tracer 實際上並不是遵循 OpenTracing 規範的,但是都源自於 Dapper 這篇論文。

規範其實就是對模型和行為的約束,在 OpenTracing 規範中有三種關鍵和相互關聯的模型:Tracer、Span 和SpanContext,並且在規範中對於每個模型的行為也做了約定。

1. Tracer

Tracer 可以被認為是一個由多個 Span 組成的有向無環圖。一個 Tracer 可以用來描述一個請求從發出到收到響應整個鏈路過程。前提是需要在適當的地方進行埋點。下圖就是一條完整的鏈路的展示:

原理 | 分散式鏈路跟蹤元件 SOFATracer 和 Zipkin 模型轉換原理 | 分散式鏈路跟蹤元件 SOFATracer 和 Zipkin 模型轉換

在 SOFATracer 中 ,SOFATracer 實現了 Tracer 介面,實現了構建 span,資料載入(Inject)和 資料提取(Extract ) 的能力。

  • Start a new Span :建立一個新的 Span 。

    通過指定的 operationName 來建立一個新的 Span。operationName 表示由 Span 完成的具體的工作 ( 例如,RPC 方法名稱、函式名稱或一個較大的計算任務中的階段的名稱)。

  • Inject a SpanContext:將 SpanContext 注入到給定型別的 “carrier” 中,用於進行跨程式的傳輸。

  • Extract a SpanContext :從載體中提取中 spanContext 例項物件。

    這個過程是注入的逆過程。spanContext 中包括了貫穿整個鏈路的 traceId ,變化的 spanId ,父 spanId 以及透傳資料等。

2. Span

一個 span 代表系統中具有開始時間和執行時長的邏輯執行單元。span 之間通過巢狀或者順序排列建立邏輯因果關係,然後再通過這種關係來構建整個呼叫鏈路(Tracer)。

原理 | 分散式鏈路跟蹤元件 SOFATracer 和 Zipkin 模型轉換

OpenTracing 規範 API 約定 Span 的模型如下(實際上就是 Span 介面中對應的方法,需要由遵循該規範的實現者必須提供的最小能力的集合):

  • Get the Span's SpanContext: 通過 Span 獲取 SpanContext (即使 span 已經結束,或者即將結束)

  • Finish:結束一個 Span 。

    Finish 必須是 span 例項的最後一個被呼叫的方法。但是在主執行緒處理失敗或者其他程式錯誤發生時,Finish 方法可能不會被呼叫。在這種情況下,實現者應該明確的記錄 Span,保證資料的持久化(這一點 SOFATracer 其實是沒有做的)。

  • Set a K:V tag on the Span:為 Span 設定 tag 。

    tag 的 key 必須是 string 型別;value 必須是 string、boolean 或數字型別。通常會使用 Tag 來記錄跟蹤系統感興趣的一些指標資料。

  • Add a new log event:為 Span 增加一個 log 事件,用於記錄 Span 生命週期中發生的事件。

  • Set a Baggage item: 設定一個 string:string 型別的鍵值對,一般是業務資料在全鏈路資料透明傳輸,儲存在 SpanContext 中。

  • Get a Baggage item: 通過 key 獲取 Baggage 中的元素。

3. SpanContext

Span 上下文,幾乎包含了需要在鏈路中傳遞的全部資訊。另外,Span 間 References 就是通過 SpanContext 來建立關係的。根據 OpenTracing 規範要求,SpanContext 是不可變的,目的是防止由於 Span 的結束和相互關係,造成的複雜生命週期問題。

SpanContext 表示必須傳播到後代 Spans 和跨程式邊界的 Span 狀態。SpanContext 在邏輯上分為兩部分:

  • 跨 Span 邊界傳播的使用者級 “Baggage”

  • 識別或以其他方式關聯 Span 例項所需的任何 Tracer 實現特定欄位(例如,trace_id,span_id,sampling,元組)

Opentracing 中 SpanContext 介面中只有一個 baggageItems 方法,通過這個方法來遍歷所有的 baggage 元素。

public interface SpanContext {
    Iterable<Map.Entry<String, String>> baggageItems();
}複製程式碼

4. SOFATracer 擴充套件的 Tracer 的能力

上面簡單介紹了 OpenTracing 規範 API 對於 Tracer、Span、SpanContext 三個核心模型的規範定義。下面來看下 SOFATracer 是如何遵循規範並做擴充套件的。

原理 | 分散式鏈路跟蹤元件 SOFATracer 和 Zipkin 模型轉換

在 OpenTracing 規範 基礎上,SOFATracer 提供了實現,並在規範基礎上提供了擴充套件功能。本文主要介紹上圖中標綠色的部分,即資料上報功能。


SOFATracer 中提供了 Report 介面,然後基於此介面擴充套件了兩個實現:

  • 第一種 Report 擴充套件是基於 Disruptor:

    https://github.com/LMAX-Exchange/disruptor,高效能無鎖迴圈佇列的非同步落地磁碟的日誌列印

  • 第二種 Report 擴充套件是提供遠端上報,能夠將 SOFATracer 的鏈路資料模型彙報到 Zipkin 中做呼叫鏈路的展示

當然,SOFATracer 也允許使用者自定義上報功能,只需要在自己的工程程式碼中實現 Report 介面即可,下面是 Report 介面的定義:

public interface Reporter {
    // 上報到遠端伺服器的持久化型別
    String REMOTE_REPORTER    = "REMOTE_REPORTER";
    // 組合型別
    String COMPOSITE_REPORTER = "COMPOSITE_REPORTER";
    // 獲取 Reporter 例項型別
    String getReporterType();
    // 上報 span
    void report(SofaTracerSpan span);
    // 關閉上報 span 的能力
    void close();
}複製程式碼

SOFATracer Report 資料上報模型

上面提到 SOFATracer 的 Report 有兩種機制,一種是落到磁碟,另外一種是上報到 Zipkin 。SOFATracer 中這兩種方案並不是二選一的,而是可以同時使用多個實現。


例如,我們希望上報資料到 Zipkin ,先引入 tracer-sofa-boot-starter 這個依賴,並進行相關 Zipkin 的配置之後就可以將鏈路資料上報到 Zipkin,如果沒有引入依賴則不會上報。

本節來分析下 SOFATracer 上報資料過程的具體邏輯。

原理 | 分散式鏈路跟蹤元件 SOFATracer 和 Zipkin 模型轉換

上面這張圖描述了資料上報的幾種方式:

  • 綠色部分,上報 Zipkin:這裡其實就是實現上報 Zipkin 的一個回撥,當進行 reportSpan 操作時,會執行一個invokeReportListeners ,這個方法就是通知所有實現了 SpanReportListener 介面的類執行回撥方法,然後在這個回撥方法中將 span 資料上報到 Zipkin 。

  • 紅色部分,輸出到磁碟:SOFATracer 為了提供更好的擴充套件能力,將輸出日誌的 Report 細分為 client 和 server 兩種;並在 Tracer 基類中提供 generateClientStatReporter 和 generateServerStatReporter 兩個抽象方法,供不同的元件自己來實現一些特殊化的定製。      

關於何時進行上報,其實這個在 Opentracing API 的規範中已經給出了明確的時機。在上面的介紹中提到,“Finish必須是 span 例項的最後一個被呼叫的方法”,當 finish 方法被呼叫時也就意味著一個 span 生命週期的結束,為了保證 span 資料的完整性和正確性,SOFATracer reportSpan 的邏輯就是在 finish 方法被呼叫時觸發執行。

資料落地磁碟

SOFATracer 日誌落盤是基於 Disruptor 高效能無鎖迴圈佇列實現的,提供了非同步列印日誌到本地磁碟的能力。

原理 | 分散式鏈路跟蹤元件 SOFATracer 和 Zipkin 模型轉換

append : 追溯 Report,無論是 clientReport 還是 serverReport ,底層均依賴 DiskReporterImpl 的實現。DiskReporterImpl 是 SOFATracer 統籌處理日誌落盤的類。clientReport 和 serverReport 的最終呼叫都會走到DiskReporterImpl 中的 digestReport 這個方法。digestReport 中會將當前 span append 到環形緩衝佇列中,append 操作就是釋出一個事件的過程。

consume:consume 是 Disruptor 中的對應的消費模型;SOFATracer 中這個消費者就是將 SofaTracerSpan 中的資料寫到日誌檔案中的。

事件釋出過程:


原理 | 分散式鏈路跟蹤元件 SOFATracer 和 Zipkin 模型轉換

資料上報 Zipkin

前面提到,上報 Zipkin 的是通過 onSpanReport 這個回撥函式完成的。tracer-sofa-boot-starter 這個依賴中提供了 SpanReportListener 介面實現 ZipkinSofaTracerSpanRemoteReporter 。而在 onSpanReport 這個回撥函式中,又將具體上報委託給了 AsyncReporter 來處理。

@Override

public void onSpanReport(SofaTracerSpan span) {
    if (span == null) {
        return;
    }
    //convert
    Span zipkinSpan = convertToZipkinSpan(span);
    this.delegate.report(zipkinSpan);
}複製程式碼

構建 AsyncReporter 物件需要兩個引數:

  • sender: 資料傳送器,SOFATracer 中,sender 的是通過 RestTemplate 以 http 方式 來與Zipkin 進行通訊傳輸的。

  • url:Zipkin 預設的 Collector 使用 http 協議裡收集 Trace 資訊,客戶端呼叫 /api/v1/spans 或 /api/v2/spans 來上報 tracer 資訊。這裡我們使用的是 Zipkin V2 的 API。


AsyncReporter 中實際構建的是 BoundedAsyncReporter 物件 , 並且在構建一個非同步報告器是,會根據 messageTimeoutNanos 是否大於 0 來決定是否起一個守護執行緒 flushThread;flushThread 作用是一直迴圈呼叫 BoundedAsyncReporter 的 flush 方法,將記憶體中的 Span 資訊上報給 Zipkin。具體細節這裡不展開分析。

SOFATracer 和 Zipkin 模型轉換原理

在上小節中貼出的小段程式碼中,除了構建 delegate 物件用於執行上報外;另一個關鍵就是 SOFATracer 的 Span 模型轉換成 Zipkin Span 模型。SOFATracer 從 2.2.0 版本之後支援 Zipkin v2 的模型 ,對於 Zipkin v1 的模型不在提供支援。

Zipkin v2的模型

下面是 zipkin GitHub 上提供的 Zipkin v2 的模型的結構化資料 Demo。

關於 Zipkin 的 Span 模型支援可以檢視 :https://github.com/openzipkin/zipkin/issues/1499

{
  "kind": "CLIENT",
  "traceId": "5af7183fb1d4cf5f",
  "parentId": "6b221d5bc9e6496c",
  "id": "352bff9a74ca9ad2",
  "name": "query",
  "timestamp": 1461750040359000,
  "duration": 5000,
  "localEndpoint": {
    "serviceName": "zipkin-server",
    "ipv4": "172.19.0.3",
    "port": 9411
  },
  "remoteEndpoint": {
    "serviceName": "mysql",
    "ipv4": "172.19.0.2",
    "port": 3306
  },
  "tags": {
    "jdbc.query": "//....discard"
  }
}複製程式碼

Zipkin v2 的模型結構較為簡潔,整體看起來並沒有什麼繁重,這種對於使用者來說是很友好的,方便理解。其實在 Zipkin v1 模型時,其整個模型也是比較複雜的,Zipkin 社群對於 Zipkin 資料模型的變更也有討論,見

https://github.com/openzipkin/zipkin/issues/939 ;

像現在 v2 模型中的 tags,替換了原本 v1 中的 binaryAnnotations,binaryAnnotations 的存在是 v1 模型複雜的重要原因。詳見去除原因 :

https://github.com/openzipkin/zipkin/releases/tag/2.10.1。

SOFATracerSpan 模型

SOFATracerSpan 是基於 Opentracing 標準來的。但是 Opentracing 標準並沒有規定一個 Span 模型必須有哪些屬性。所以各個基於該標準的產品在於 Span 的模型上是不統一的,大多會基於其本身產生的場景帶有一些特殊的屬性。

{
    "client":true,
    "server":false,
    "durationMicroseconds":775,
    "endTime":1536288243446,
    "logType":"httpclient-digest.log",
    "operationName":"GET",
    "logs":[
     // ...    ], 
    "sofaTracer":{
        "clientReporter":{},
        "tracerTags":{},
        "tracerType":"httpclient"
    },
    "sofaTracerSpanContext":{
        // sofaTracerSpanContext info    },
    "spanReferences":[],
    "startTime":1536288242671,
    "tagsWithBool":{},
    "tagsWithNumber":{},
    "tagsWithStr":{},
    "thisAsParentWhenExceedLayer":{}
}複製程式碼


SOFATracer 的 Span 模型相較於 Opentracing 規範模型和 Zipkin v2 的模型來說,記錄的資料資訊更加豐富,且在 Opentracing 規範的基礎上擴充套件了一套自己的 API,可以讓使用者能夠更加方便的在自己的程式碼中來獲取鏈路中的資訊;在日誌中展示更多的 span 資訊,能夠幫助我們去了解一些呼叫細節,在發生問題時,也提供了更多排查問題的依據資訊。

模型轉換對照

為了使得 SOFATracer 的資料能夠被 Zipkin 解析,需要將 SOFATracer 的 Span 模型轉換成 Zipkin v2 的資料模型。


Zipkin v2 Span Model

SOFATracer Span Model

備註

traceId

traceId

traceId

id

spanId

spanId

parentId

parentId

父spanId

name

operationName

span 名,用來描述當前span 的行為

duration

-

當前span的時間跨度;這裡通過span的(結束時間-開始時間)獲取

timestamp

timestamp

當前span的開始時間

localEndPoint

operationName&host&logData

標明這個span的來源

remoteEndPoint

-

被呼叫方的服務名和地址

tags

bizBaggage & tags

額外的用於描述span的資訊


整體來看,Span 模型相似度是很高,但是實際上並不能直接將某些相同的欄位直接進行值複製;這裡有一個案例:

https://github.com/alipay/sofa-tracer/issues/57 。

traceId 和 spanId 處理

Zipkin 在自己的模型裡做了很多特殊的處理。比如 traceId 需滿足16 或者 32 位,長度不夠的會高位補 0;所以在使用 SOFATracer 時,日誌中的 traceId 和上報到 Zipkin 的 traceId 長度不一致是合理的。

關於 spanId,我們期望在 Zipkin 中展示是以(0.1,0.1.1,...)這種形式來描述,能夠直觀的看到 span 之間的依賴關係。但是目前使用的 Zipkin 模型並不能滿足我們的需求,主要原因在於雖然 Zipkin 在 v2 模型中雖然支援 string 型別的 id ,但是其長度限制是 16 位,對於 SOFATracer 來說,如果存在較長的鏈路呼叫,會導致層次丟失。

另外,如果上報 Zipkin 的 span 的 parentId 為 0,那麼 Zipkin 將會不進行設定;而 SOFATracer 的第一個 span 的 id 就是從 0 開始的,所以會導致鏈路構建失敗,如果我們嘗試通過改變起始 id 來改變,會對整個模型產生影響。經過驗證測試,我們最終採用的方案是使用衝突較小的 FNV64 Hash 演算法將 String 型別轉換成 long 型來描述我們的 spanId。

SOFARPC 上報的資料處理

在整個模型轉換中,比較核心的就是如何相容 SOFARPC 上報的資料。Zipkin 在構建鏈路數時,其基本的模型是 client-server-client-server-.. 這種模式;不會出現 a server calling a server 這種情況,也就是帶有kind = server 的 span 的 父span 應該是 kind = client。

SOFARPC 對於一個 rpc span 上報了兩個 span 資訊,這兩個 span 除了 kind 型別不同之外,其他的資訊是一樣的。當資料上報給 Zipkin 之後,Zipkin 通過自己的演算法來構建依賴樹時,會對上報的 SOFARPC 資料處理有問題。下圖是沒有適配 SOFARPC 生成的鏈路:


原理 | 分散式鏈路跟蹤元件 SOFATracer 和 Zipkin 模型轉換


這裡可以看出,從 mvc 到 rpc 之間的關係被‘切斷’了。

造成上述問題的原因在於,SOFATracer 上報資料到 Zipkin 時,在 v2 模型中,Zipkin 會通過廣度優先遍歷來構建依賴樹,實際上在展示 services 或者 dependencies 時,Zipkin ui 中的展示會依賴 endpiont 中的 serviceName ;兩個條件:

  • SOFARPC 的 span 有兩個(client&server),但是這兩個 span 具有相同的 spanId 和 parentId,span.kind 不同。

  • Zipkin 在構建依賴樹時,依賴於 endpiont 中的 serviceName。該 servieName 依賴於 idToNode(Node.TreeBuilder 中的屬性,Map 結構,對映關係為 spanId -> span)。

 Node<V> previous = idToNode.put(id, node);
 if (previous != null) 
	node.setValue(mergeFunction.merge(previous.value, node.value));複製程式碼

這裡當前 node 為 rpc server 型別時,previous 返回結果不為 null,會執行 merge 操作,該 merge 操作的核心就是設定當前 rpc node 的 remoteEndpoint,值為 rpc client 的 localEndpoint。

這樣會有一個問題,就是 RPC 的 client 和 server Span 在 Zipkin 模型中的會被合併成一個 span;這樣就會導致 server -> server 的情況,與 Zipkin 的 client -> server 鏈路模型有衝突。如下圖(綠色為SOFATracer span,黃色為 Zipkin span):

原理 | 分散式鏈路跟蹤元件 SOFATracer 和 Zipkin 模型轉換

通過分析 zipkin 的構建過程,適配 SOFARPC 上報資料時,SOFARPC server span 的 remoteEndpoint 不能依賴 SOFARPC client span 的 localEndpoint,而應該依賴 SOFARPC client parentSpan 的 localEndpoint。下圖為 SOFARPC 適配之後的依賴關係圖:

原理 | 分散式鏈路跟蹤元件 SOFATracer 和 Zipkin 模型轉換

總結

本文從 OpenTracing 規範說起,對 OpenTracing 規範中的模型和行為進行了簡單的描述。結合 OpenTracing 規範,介紹了螞蟻金服 SOFATracer 分散式鏈路跟蹤的模型實現。在此基礎上,對 SOFATracer 的資料上報功能進行了詳細的分析,包括基於 disruptor 實現的非同步日誌落盤和上報資料到Zipkin。最後對 SOFATracer 和 Zipkin 模型轉換原理進行了說明,並對 SOFARPC 模型資料的上報處理進行了解析。

相關文件連結

  • SOFATracer GitHub : https://github.com/alipay/sofa-tracer

  • Zipkin 官網 : https://zipkin.io/

  • Zipkin GitHub : https://github.com/openzipkin/zipkin

  • opentracing 規範 : http://opentracing.io/documentation/pages/spec.html

  • opentracing 官網 : http://opentracing.io/

  • disruptor :

    https://github.com/LMAX-Exchange/disruptor

小彩蛋

來這裡看看有沒有你的 ID 吧:

http://www.sofastack.tech/awesome

原理 | 分散式鏈路跟蹤元件 SOFATracer 和 Zipkin 模型轉換

參與調研,幫助 SOFA 成長

歡迎大家共同打造 SOFAStack https://github.com/alipay



相關文章