本文以 Dapper 論文為切入點,延伸到其相關的論文內容,結合歷史時間線發展的線索,給讀者展示出軟件從業者對鏈路追蹤技術的探索和實踐。
帶着疑問看歷史
提起鏈路追蹤,大部分人都會想起 Zipkin、Jaeger、Skywalking 這些已經比較成熟的鏈路追蹤開源軟件以及 Opentelemetry、OpenTracing、OpenCensus 這些開源標准。雖然實現各有差異,但是使用各種軟件、標准和實現組合搭建出來的不同的鏈路追蹤系統,卻有着許多相類似的地方。
例如這些鏈路追蹤系統都需要在調用鏈路上傳播元數據。他們對元數據內容的定義也大同小異,鏈路唯一的 trace id, 關聯父鏈路的 parent id,標識自身的 span id 這些。他們都是異步分散上報采集的追蹤信息,離線的聚合聚合追蹤鏈路。他們都有鏈路采樣等等。
鏈路追蹤系統架構和模型的設計看着都是如此相似,我不禁會產生一些疑問:開發者在設計鏈路追蹤的時候,想法都是這么一致嗎?為什么要在調用鏈路傳遞元數據?元數據的這些信息都是必要的嗎?不侵入修改代碼可以接入到鏈路追蹤系統嗎?為什么要異步分散上報,離線聚合?設置鏈路采樣有什么用?
帶着各種各樣的問題,我找到這些眾多鏈路追蹤軟件的靈感之源 -- 《Google Dapper》 論文,並且拜讀了原文以及相關的引用論文。這些論文逐漸解開了我心中的疑惑。
黑盒模式探索
早期學術界對分布式系統鏈路狀態檢測的探索,有一派的人們認為分布式系統里面的每個應用或者中間件,應該是一個個黑盒子,鏈路檢測不應該侵入到應用系統里面。那個時候 Spring 還沒被開發出來,控制反轉和切面編程的技術也還不是很流行,如果需要侵入到應用代碼里面,需要涉及到修改應用代碼,對於工程師來說額外接入門檻太高,這樣的鏈路檢測工具就會很難推廣開來。
如果不允許侵入應用里面修改代碼,那就只能夠從應用的外部做手腳,獲取並記錄鏈路信息了。而由於黑盒的限制,鏈路信息都是零散的無法串聯起來。如何把這些鏈路串聯起來成了需要解決的問題。
《Performance Debugging for Distributed Systems of Black Boxes》
這篇論文發表於 2003 年,是對黑盒模式下的調用鏈監測的探索,文中提出了兩種尋找鏈路信息的算法。
第一種算法稱為“嵌套算法”,首先是通過生成唯一 id 的方式,把一次跨服務調用的請求 (1 call)鏈路與返回(11 return)鏈路關聯再一起形成鏈路對。然后再利用時間的先后順序,把不同往返鏈路對做平級關聯或上下級關聯(參考圖1)。

如果應用是單線程情況,這種算法但是沒有什么問題。生產的應用往往是多線程的,所以使用這種方法無法很好的找到鏈路間對應關系。雖然論文提出了一種記分板懲罰的方法可以對一些錯誤關聯的鏈路關系進行除權重,但是這種方法對於一些基於異步 RPC 調用的服務,卻會出現一些問題。
另外一種算法稱為“卷積算法”,把往返鏈路當成獨立的鏈路,然后把每個獨立鏈路對當成一個時間信號,使用信號處理技術,找到信號之間的關聯關系。這種算法好處是能夠出使用在基於異步 RPC 調用的服務上。但是如果實際的調用鏈路存在回環的情況,卷積算法除了能夠得出實際的調用鏈路,還會得出其他調用鏈路。例如調用鏈路 A -> B -> C -> B -> A,卷積算法除了得出其本身調用鏈路,還會得出 A -> B -> A 的調用鏈路。如果某個節點在一個鏈路上出現次數多次,那么這個算法很可能會得出大量衍生的調用鏈路。
在黑盒模式下,鏈路之間的關系是通過概率統計的方式判斷鏈路之間的關聯關系。概率統計始終是概率,沒辦法精確得出鏈路之間的關聯關系。
另一種思路
怎么樣才能夠精確地得出調用鏈路之間的關系呢?下面這篇論文就給出了一些思路與實踐。
Pinpoint: Problem Determination in Large, Dynamic Internet Services
注:此 Pinpoint 非 github 上的 pinpoint-apm
這篇論文的研究對象主要是擁有不同組件的單體應用,當然相應的方法也可以擴展到分布式集群中。在論文中 Pinpoint 架構設計主要分為三部分。參考 圖2,其中 Tracing 與 Trace Log 為第一部分,稱為客戶端請求鏈路追蹤(Client Request Trace),主要用於收集鏈路日志。Internal F/D 、External F/D 和 Fault Log 為第二部分,是故障探測信息(Failure Detection),主要用於收集故障日志。Statistical Analysis 為第三部分,稱為數據聚類分析(Data Clustering Analysis),主要用於分析收集進來的日志數據,得出故障檢測結果。

Pinpoint 架構中,設計了一種能夠有效用於數據挖掘分析方法的數據。如 圖3 所示,每個調用鏈路作為一個樣本數據,使用唯一的標識 request id 標記,樣本的屬性記錄了這個調用鏈路所經過的程序組件(Component)以及故障狀態(Failure)。

為了能夠把每次調用的鏈路日志 (Trace Logs) 和 故障日志 (Fault Logs) 都關聯起來,論文就以 Java 應用為例子,描述了如何在代碼中實現這些日志的關聯。下面是 Pinpoint 實踐章節的一些關鍵點匯總:
- 需要為每一個組件生成一個
component id
- 對於每一個 http 請求生成一個唯一的
request id
,並且通過線程局部變量(ThreadLocal)傳遞下去 - 對於請求內新起來的線程,需要修改線程創建類,把
request id
繼續傳遞下去 - 對於請求內產生的 rpc 調用,需要修改請求端代碼,把
request id
信息帶入 header,並在接收端解析這個 header 注入到線程本地變量 - 每次調用到一個組件(component),就使用 (
request id
,component id
) 組合記錄一個 Trace Log
對 java 應用而言,這幾個點技術實踐簡單,操作性高,為現今鏈路追蹤系統實現鏈路串聯,鏈路傳播(Propegation)提供了基本思路。
這篇論文發表時間是 2002 年,那個時候 java 版本是 1.4,已經具備了線程本地變量(ThreadLocal)的能力,在線程中攜帶信息是比較容易做到的。但又因為在那個時代切面編程還不是很普及(Spring 出現在 2003年,javaagent 是在 java 1.5 才有的能力,發布於2004年),所以這樣的方法並不能夠被廣泛應用。如果反過來想,可能正是因為這些編程需求的出現,促使着 java 切面編程領域的技術進步。
重新構建調用鏈路
這篇論文主要研究對象是分布式集群里面的網絡鏈路。X-Trace 論文延續並擴展了 Pinpoint 論文的思路,提了能夠重新構建完整調用鏈路的框架和模型。為了達到目的,文中定義了三個設計原則:
- 在調用鏈路內攜帶元數據(在調用鏈路傳遞的數據也稱之為帶內數據,
in-bound data
) - 上報的鏈路信息不留存在調用鏈路內,收集鏈路信息的機制需要與應用本身正交(注:不在調用鏈路里面留存的鏈路數據,也稱之為帶外數據,
out-of-bound data
) - 注入元數據的實體應該與收集報告的實體解偶
原則 1,2 點是沿用至今的設計原則。原則 1 則是對 Poinpont 思路的擴展,鏈路傳遞從原來的request id
擴展了更多的元素,其中 TaskID
, ParentID
, OpID
就是 trace id
, parent id
, span id
的前身。span
這個單詞也在 X-Trace 論文的 Abstract 里面出現,也許是 Dapper 作者向 X-Trace 論文作者們的一種致敬。
下面再看看 X-Trace 對元數據的內容定義:
Flags
- 一個bit數組,用於標記
TreeInfo
,Destination
,Options
是否使用
- 一個bit數組,用於標記
TaskID
- 全局唯一的id,用於標識唯一的調用鏈
TreeInfo
ParentID
- 父節點id,調用鏈內唯一OpID
- 當前操作id,調用鏈內唯一EdgeType
- NEXT 表示兄弟關系,DOWN 表示父子關系
Destination
- 用於指定上報地址
Options
- 預留字段,用於擴展
除了對元數據的定義,論文還定義了兩個鏈路傳播的操作,分別是 pushDown()
與 pushNext()
。pushDown()
表示拷貝元數據到下一層級,pushNext()
則表示從當前節點傳播元數據到下一個節點。


在 X-Trace 上報鏈路數據的結構設計中,遵循了第 2 個設計原則。如 圖6 所示, X-Trace 為應用提供了一個輕量的客戶端包,使得應用端可以轉發鏈路數據到一個本地的守護進程。而本地的守護進程則是開放一個 UDP 協議端口,接收客戶端包發過來的數據,並放入到一個隊列里面。隊列的另外一邊則根據鏈路數據的具體具體配置信息,發送到對應的地方去,也許是一個數據庫,也許是一個數據轉發服務、數據收集服務或者是數據聚合服

X-Trace 上報鏈路數據的架構設計,對現在市面上的鏈路追蹤實現有着不小的影響。對照 Zipkin 的 collector 以及 Jeager 的 jaeger-agent,多少能夠看到 X-Trace 的影子。
X-Trace 的三個設計原則、帶內帶外數據的定義、元數據傳播操作定義、鏈路數據上報架構等,都是現今鏈路追蹤系統有所借鑒的內容。對照 Zipkin 的 collector 以及 Jeager 的 jaeger-agent,就多少能夠看到 X-Trace 鏈路數據上報架構的影子。
大規模商用實踐 -- Dapper
Dapper, a Large-Scale Distributed Systems Tracing Infrastructure
Dapper 是谷歌內部用於給開發者們提供復雜分布式系統行為信息的系統。Dapper 論文則是介紹谷歌對這個分布式鏈路追蹤基礎設施設計和實踐的經驗。Dapper 論文發布於2010年,根據論文的表述,Dapper 系統已經在谷歌內部有兩年的實踐經驗了。
Dapper 系統的主要目的是給開發者提供提供復雜分布式系統行為信息。文中分析為了實現這樣的系統,需要解決什么樣的問題。並根據這些問題提出了兩個基本的設計需求:大范圍部署和持續性的監控。針對着兩個基本設計要求,提出了三個具體的設計目標:
- 低開銷(Low overhead):鏈路追蹤系統需要保證對在線服務的的性能影響做到忽略不計的程度。即使是很小的監控消耗也會對一些高度優化過的服務有可覺察的影響,甚至迫使部署團隊關閉追蹤系統。
- 應用級透明化(Application-level transparecy):開發者不應該感知到鏈路追蹤設施。如果鏈路追蹤系統需要依賴應用級開發者協助才能夠工作,那么這個鏈路追蹤設施會變得非常最弱,而且經常會因為 bugs 或者疏忽導致無法正常工作。這違反了大范圍部署的設計需求。
- 可伸縮性(Scalability):鏈路追蹤系統需要能夠滿足 Google 未來幾年的服務和集群的規模。
雖然 Dapper 的設計概念與 Pinpoint、 Magpie、 X-Trace 有許多是想通的,但是 Dapper 也有自己的一些獨到的設計。其中一點就是為了達到低開銷的設計目標,Dapper 對請求鏈路進行了采樣收集。根據 Dapper 在谷歌的實踐經驗,對於許多常用的場景,即使對 1/1000 的請求進行采樣收集,也能夠得到足夠的信息。
另外一個獨到的特點是他們實現非常高的應用透明度。這個得益於 Google 應用集群部署有比較高的同質化,他們可以把鏈路追蹤設施實現代碼限制在軟件的底層而不需要在應用里面添加而外的注解信息。舉個例子,集群內應用如果使用相同的 http 庫、消息通知庫、線程池工廠和 RPC 庫,那么就可以把鏈路追蹤設施限制在這些代碼模塊里面。
如何定義鏈路信息的?
文中首先舉了一個簡單的調用鏈例子,如 圖7 ,作者認為對一個請求做分布式追蹤需要收集消息的識別碼以及消息對應的事件與時間。如果只考慮 RPC 的情況,調用鏈路可以理解為是 RPCs 嵌套樹。當然,谷歌內部的數據模型也不局限於 RPCs 調用。

圖8 闡述了 Dapper 追蹤樹的結構,樹的節點為基本單元,稱之為 span
。邊線為父子 span
之間的連接。一個 span
就是簡單帶有起止時間戳、RPC 耗時或者應用相關的注解信息。為了重新構建 Dapper 追蹤樹,span
還需要包含以下信息:
span name
: 易於閱讀的名字,如圖8中的Frontend.Request
span id
: 一個64bit的唯一標識符parent id
: 父span id

圖9 是一個 RPC span 的詳細信息。值得一提的是,一個相同的 span
可能包含多個主機的信息。實際上,每一個 RPC span 都包含了客戶端和服務端處理的注釋。由於客戶端的時間戳和服務端的時間戳來自不同的主機,所以需要異常關注這些時間的異常情況。圖9 是一個 span
的詳細信息

如何實現應用級透明的?
Dapper 通過對一些通用包添加測量點,對應用開發者在零干擾的情況下實現了分布式鏈路追蹤,主要有以下實踐:
- 當一個線程在處理鏈路追蹤路徑上時,Dapper 會把追蹤上下文關聯到線程本地存儲。追蹤上下文是一個小巧且容易復制的 span 信息容易。
- 如果計算過程是延遲的或者一步的,大多谷歌開發者會使用通用控制流庫來構造回調函數,並使用線程池線程池或者其他執行器來調度。這樣 Dapper 就可以保證所有的回調函數會在創建的時候存儲追蹤上下文,在回調函數被執行的時候追蹤上下文關聯到正確線程里面。
- Google 幾乎所有的線程內通信都是建立在一個 RPC 框架構建的,包括 C++ 和 Java 的實現。框架添加上了測量,用於定義所有 RPC 調用相關 span。在被跟蹤的 RPC,span 和 trace 的 id 會從客戶端傳遞到服務端。在 Google 這個是非常必要的測量點。
結尾
Dapper 論文給出了易於閱讀和有助於問題定位的數據模型設計、應用級透明的測量實踐以及低開銷的設計方案,為鏈路追蹤在工業級應用的使用清除了不少障礙,也激發了不少開發者的靈感。自從 Google Dapper 論文出來之后,不少開發者受到論文的啟發,開發出了各式各樣的鏈路追蹤,2012 年推特開源 Zipkin、Naver 開源 Pinpoint,2015 年吳晟開源 Skywalking,Uber 開源 Jaeger 等。從此鏈路追蹤進入了百家爭鳴的時代。