鏈路追蹤(Tracing)的前世今生(上)

語言: CN / TW / HK

                     

帶著疑問看歷史

提起鏈路追蹤,大部分人都會想起 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)。

                                                                              圖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),主要用於分析收集進來的日誌資料,得出故障檢測結果。

                                                                                           圖2

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

                                                                                               圖3

為了能夠把每次呼叫的鏈路日誌 (Trace Logs) 和 故障日誌 (Fault Logs) 都關聯起來,論文就以 Java 應用為例子,描述瞭如何在程式碼中實現這些日誌的關聯。下面是 Pinpoint 實踐章節的一些關鍵點彙總:

  1. 需要為每一個元件生成一個 component id
  2. 對於每一個 http 請求生成一個唯一的 request id,並且通過執行緒區域性變數(ThreadLocal)傳遞下去
  3. 對於請求內新起來的執行緒,需要修改執行緒建立類,把 request id 繼續傳遞下去
  4. 對於請求內產生的 rpc 呼叫,需要修改請求端程式碼,把 request id 資訊帶入 header,並在接收端解析這個 header 注入到執行緒本地變數
  5. 每次呼叫到一個元件(component),就使用 (request id, component id) 組合記錄一個 Trace Log

對 java 應用而言,這幾個點技術實踐簡單,操作性高,為現今鏈路追蹤系統實現鏈路串聯,鏈路傳播(Propegation)提供了基本思路。

這篇論文發表時間是 2002 年,那個時候 java 版本是 1.4,已經具備了執行緒本地變數(ThreadLocal)的能力,線上程中攜帶資訊是比較容易做到的。但又因為在那個時代切面程式設計還不是很普及(Spring 出現在 2003年,javaagent 是在 java 1.5 才有的能力,釋出於2004年),所以這樣的方法並不能夠被廣泛應用。如果反過來想,可能正是因為這些程式設計需求的出現,促使著 java 切面程式設計領域的技術進步。

重新構建呼叫鏈路

X-Trace: A Pervasive Network Tracing Framework

這篇論文主要研究物件是分散式叢集裡面的網路鏈路。X-Trace 論文延續並擴充套件了 Pinpoint 論文的思路,提了能夠重新構建完整呼叫鏈路的框架和模型。為了達到目的,文中定義了三個設計原則:

  1. 在呼叫鏈路內攜帶元資料(在呼叫鏈路傳遞的資料也稱之為帶內資料,in-bound data
  2. 上報的鏈路資訊不留存在呼叫鏈路內,收集鏈路資訊的機制需要與應用本身正交(注:不在呼叫鏈路裡面留存的鏈路資料,也稱之為帶外資料,out-of-bound data
  3. 注入元資料的實體應該與收集報告的實體解偶

原則 1,2 點是沿用至今的設計原則。原則 1 則是對 Poinpont 思路的擴充套件,鏈路傳遞從原來的request id 擴充套件了更多的元素,其中 TaskID , ParentID , OpID 就是 trace id , parent id, span id 的前身。span 這個單詞也在 X-Trace 論文的 Abstract 裡面出現,也許是 Dapper 作者向 X-Trace 論文作者們的一種致敬。

下面再看看 X-Trace 對元資料的內容定義:

  1. Flags
    • 一個bit陣列,用於標記 TreeInfoDestinationOptions 是否使用
  2. TaskID
    • 全域性唯一的id,用於標識唯一的呼叫鏈
  3. TreeInfo
    • ParentID - 父節點id,呼叫鏈內唯一
    • OpID - 當前操作id,呼叫鏈內唯一
    • EdgeType - NEXT 表示兄弟關係,DOWN 表示父子關係
  4. Destination
    • 用於指定上報地址
  5. Options
    • 預留欄位,用於擴充套件

除了對元資料的定義,論文還定義了兩個鏈路傳播的操作,分別是 pushDown()pushNext()pushDown()表示拷貝元資料到下一層級,pushNext() 則表示從當前節點傳播元資料到下一個節點。

                                                                         圖4 pushDown() 與 pushNext() 的虛擬碼

                                                            圖5 pushDown() 與 pushNext() 操作在呼叫鏈路中的執行的位置

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

                                                                                                          圖6

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 系統的主要目的是給開發者提供提供複雜分散式系統行為資訊。文中分析為了實現這樣的系統,需要解決什麼樣的問題。並根據這些問題提出了兩個基本的設計需求:大範圍部署和持續性的監控。針對著兩個基本設計要求,提出了三個具體的設計目標:

  1. 低開銷(Low overhead):鏈路追蹤系統需要保證對線上服務的的效能影響做到忽略不計的程度。即使是很小的監控消耗也會對一些高度優化過的服務有可覺察的影響,甚至迫使部署團隊關閉追蹤系統。
  2. 應用級透明化(Application-level transparecy):開發者不應該感知到鏈路追蹤設施。如果鏈路追蹤系統需要依賴應用級開發者協助才能夠工作,那麼這個鏈路追蹤設施會變得非常最弱,而且經常會因為 bugs 或者疏忽導致無法正常工作。這違反了大範圍部署的設計需求。
  3. 可伸縮性(Scalability):鏈路追蹤系統需要能夠滿足 Google 未來幾年的服務和叢集的規模。

雖然 Dapper 的設計概念與 Pinpoint、 Magpie、 X-Trace 有許多是想通的,但是 Dapper 也有自己的一些獨到的設計。其中一點就是為了達到低開銷的設計目標,Dapper 對請求鏈路進行了取樣收集。根據 Dapper 在谷歌的實踐經驗,對於許多常用的場景,即使對 1/1000 的請求進行取樣收集,也能夠得到足夠的資訊。

另外一個獨到的特點是他們實現非常高的應用透明度。這個得益於 Google 應用叢集部署有比較高的同質化,他們可以把鏈路追蹤設施實現程式碼限制在軟體的底層而不需要在應用裡面新增而外的註解資訊。舉個例子,叢集內應用如果使用相同的 http 庫、訊息通知庫、執行緒池工廠和 RPC 庫,那麼就可以把鏈路追蹤設施限制在這些程式碼模組裡面。

如何定義鏈路資訊的?

文中首先舉了一個簡單的呼叫鏈例子,如 圖7 ,作者認為對一個請求做分散式追蹤需要收集訊息的識別碼以及訊息對應的事件與時間。如果只考慮 RPC 的情況,呼叫鏈路可以理解為是 RPCs 巢狀樹。當然,谷歌內部的資料模型也不侷限於 RPCs 呼叫。

                                                                 圖7

圖8 闡述了 Dapper 追蹤樹的結構,樹的節點為基本單元,稱之為 span。邊線為父子 span 之間的連線。一個 span 就是簡單帶有起止時間戳、RPC 耗時或者應用相關的註解資訊。為了重新構建 Dapper 追蹤樹,span 還需要包含以下資訊:

  1. span name: 易於閱讀的名字,如圖8中的 Frontend.Request
  2. span id: 一個64bit的唯一識別符號
  3. parent id: 父 span id

                                                               圖8

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

                                                                                      圖9

如何實現應用級透明的?

Dapper 通過對一些通用包新增測量點,對應用開發者在零干擾的情況下實現了分散式鏈路追蹤,主要有以下實踐:

  1. 當一個執行緒在處理鏈路追蹤路徑上時,Dapper 會把追蹤上下文關聯到執行緒本地儲存。追蹤上下文是一個小巧且容易複製的 span 資訊容易。
  2. 如果計算過程是延遲的或者一步的,大多谷歌開發者會使用通用控制流庫來構造回撥函式,並使用執行緒池執行緒池或者其他執行器來排程。這樣 Dapper 就可以保證所有的回撥函式會在建立的時候儲存追蹤上下文,在回撥函式被執行的時候追蹤上下文關聯到正確執行緒裡面。
  3. Google 幾乎所有的執行緒內通訊都是建立在一個 RPC 框架構建的,包括 C++ 和 Java 的實現。框架新增上了測量,用於定義所有 RPC 呼叫相關 span。在被跟蹤的 RPC,span 和 trace 的 id 會從客戶端傳遞到服務端。在 Google 這個是非常必要的測量點。

結尾

Dapper 論文給出了易於閱讀和有助於問題定位的資料模型設計、應用級透明的測量實踐以及低開銷的設計方案,為鏈路追蹤在工業級應用的使用清除了不少障礙,也激發了不少開發者的靈感。自從 Google Dapper 論文出來之後,不少開發者受到論文的啟發,開發出了各式各樣的鏈路追蹤,2012 年推特開源 Zipkin、Naver 開源 Pinpoint,2015 年吳晟開源 Skywalking,Uber 開源 Jaeger 等。從此鏈路追蹤進入了百家爭鳴的時代。

                    

                                                      歡迎點選一鍵訂閱《雲薦大咖》專欄,獲取更多精品內容。

                                                      看雲端技術起落,聽大咖指點迷津。