現代網際網路通常被實現為複雜的大規模分布式微服務系統。這些應用可能是由不同團隊使用不同程式語言開發的軟體模塊集合構建的,並且可能跨越數千臺計算機及多個物理設備。在這樣的環境中,有一個幫助理解系統行為和關於性能問題推理的工具顯得非常寶貴。這裡介紹由Google生產的分布式系統鏈路追蹤系統Dapper的設計,並描述其設計目標如何滿足大規模系統低開銷、對應用透明性和廣泛的覆蓋部署
設計目標介紹
分布式服務追蹤是整個分布式系統中跟蹤一個用戶請求的過程,包括數據採集、數據傳輸、數據存儲、數據分析和數據可視化,捕獲此類跟蹤讓我們構建用戶交互背後的整個調用鏈的視圖,這是調試和監控微服務的關鍵工具。Google Dapper 就是這樣需求下的一套應用於大型分布式系統環境下的鏈路追蹤系統。藉助 Dapper 理念,系統開發人員只需將 Trace 組件嵌入到基礎通用庫中,就可以正常運行,而應用層開發者則不需要關心具體 Trace 組件實現、集成方式,達到以應用層透明的方式嵌入各個模塊的目的
Dapper與其他追蹤系統存在概念上的相似之處,特別是PinPoint、Magpi和X-Trace,但是某些設計選擇是其在Google的環境中取得成功的關鍵,例如使用抽樣和將指令限制到相當少量的的公共庫。Dapper最初作為一個獨立的追蹤工具,後來發展成為一個監控平臺,為Google的開發者提供有關複雜分布式系統行為的更多信息。這樣的系統特別令人感興趣,因為大規模小型服務集合是一個特別具有成本效益的網際網路服務工作負載平臺。理解此上下文系統行為需要觀察許多不同程序和機器上的相關活動
網絡搜索示例將能夠說明這樣一個系統需要解決的一些挑戰。一個前端服務可以將Web查詢分發給數百個查詢服務,每個查詢服務在其自己的索引中搜索。該查詢還可以被發送到許多其他子系統,這些子系統可以處理廣告、檢查拼寫或尋找專門的結果,包括圖像、視頻、新聞等,所有這些服務的結果有選擇地組合在結果頁面中,我們稱這個模型為「通用搜索」。總共可能需要數千臺機器和許多不同的服務來處理一個通用搜索查詢。此外,網絡搜索用戶對延遲很敏感,這可能是由於任何子系統的性能不佳造成的。工程師僅僅考慮延遲可能知道存在問題,但可能無法猜測哪個服務有問題,也無法猜測其行為不良的原因。首先,工程師可能無法準確知道正在使用哪些服務,每周都可能加入新服務和修改部分服務,以增加用戶可見的功能,並改進性能和安全性等其他方面;其次,工程師不可能是所有內部微服務的專家,每一個微服務可能有不同團隊構建和維護;第三,服務和機器可以由許多不同的客戶端同時共享,因此性能問題可能是由於另一個應用的行為引起
上述場景給Dapper提出了兩個基本要求:覆蓋面的廣度和持續監控。覆蓋面的廣度,因為即使系統的一小部分未被監控,追蹤系統是不是值得信任都會被人質疑。此外,監控應該是7X24的,因為通常情況下,異常或其他值得注意的系統行為難以或不可能再現。根據這兩個明確的要求推出以下三個具體的設計目標
低開銷 分布式系統對性能和資源要求都很嚴格,Trace組件對服務的影響必須足夠小。一些高度優化的服務中,即使很小的監控開銷也很容易察覺到,並且可能迫使部署團隊關閉追蹤系統應用級透明 Trace組件嵌入到基礎通用庫中,以提高穩定性,應用開發者不需要關心它們。可擴展性 至少在未來幾年內它需要處理Google服務和集群規模一個額外的設計目標是--追蹤數據產生後可以快速導出數據並快速進行分析,在最短時間內定位系統異常,理想情況是數據存入追蹤倉庫後一分鐘內就能統計出來。儘管追蹤分析系統使用一小時前的舊數據進行分析依然相當有價值,但如果追蹤系統能夠提供足夠快的信息反饋,就可以對生產環境下的異常狀況做出快速反應
要真正做到應用級別的透明可能是最具有挑戰的設計目標,通過將Dapper的核心追蹤代碼做的很輕巧,然後把它植入到那些無所不在的公共組件中,比如線程調用,控制流以及RPC庫。使用自適應採樣有助於使系統變得具有可擴展並降低性能損耗。結果展示的相關系統也需要包含一些用來收集跟蹤數據的代碼,用來圖形化的工具,以及用來分析大規模跟蹤數據的庫和API,雖然單獨使用Dapper有時就足夠讓開發人員查明異常來源,但是Dapper的數據往往側重性能方面的調查,其初衷也並非取代所有其他監控工具
這些要求產生來三個具體的設計目標
低開銷追蹤系統對運行服務的影響能夠忽略不計,在一些高度優化的服務中,即使很小的監控開銷也很容易引起注意,並且可能迫使部署團隊關閉追蹤系統對應用透明可擴展性至少在未來幾年內它需要處理Google服務和集群規模一個額外的設計目標是追蹤數據產生後可以快速進行分析:理想情況下在一分鐘之內。儘管追蹤分析系統使用數小時內的追蹤數據進行分析依然十分有價值,新信息的可用性使得能夠對產生異常作出更快地反應。
真正對應用透明可能是最具有挑戰的設計目標,通過將Dapper的核心追蹤工具限制為一個無處不在的線程,控制流和RPC庫代碼的小型語料庫來實現的。使用自適應採樣有助於減少開銷使系統具有可擴展性
分布式鏈路追蹤
當我們進行微服務架構開發時,通常會根據業務或功能劃分成若干個不同的微服務模塊,各模塊之間通過 REST/RPC 等協議進行調用。一個用戶在前端發起某種操作,可能需要很多微服務的協同才能完成,如果在業務調用鏈路上任何一個微服務出現問題或者網絡超時,將導致功能失效,或出現某些模塊在調用鏈中耗時突然增大等問題。隨著業務越來越複雜,微服務架構橫向、縱向擴展之後,其規模越來越大,對於微服務之間的調用鏈路的分析將會越來越複雜。此時你會發現,如果在最初架構設計時,能夠將分布式鏈路追蹤這種需求考慮進來,後期微服務集群擴容時候,前期所做的工作將會達到事半功倍的效果
一個簡單常見的服務調用示例: 圖中,A-E分別表示5個微服務,用戶發起一個請求RequestX到達前端A,它將分別向B、C服務發送RPC,服務B可以立即響應,服務C需要等服務D、E工作返回後才能響應服務A,A收到B和C的響應後以ReplyX作為消息返回用戶RequestX請求
以上一個完整的調用迴路中,一次請求需要經過多個系統處理完成,並且追蹤系統像是內嵌在RPC調用鏈上的樹形結構,然而,我們的核心數據模型不僅限於特定的RPC框架;我們還會追蹤Gmail中的SMTP會話,來自外部世界的HTTP請求以及SQL服務的出站查詢。形式上,分布式鏈路追蹤使用Trace樹形結構來記錄請求之間的關係(父子關係、先後順序等)
Trace Trees 和 Span
Trace 的三個主要構成元素
Span 基本工作單元,例如,在一個新建的 Span 中發送一個 RPC 等同於發送一個回應請求給 RPC,Span 通過一個 64 位 ID 唯一標識,Trace 以另一個 64 位 ID 表示。Span 還有其他數據信息,比如摘要、時間戳事件、關鍵值注釋 (Tags)、Span 的 ID、以及進度 ID (通常是 IP 地址)。Span 在不斷地啟動和停止,同時記錄了時間信息,當你創建一個 Span,你必須在未來的某個時刻停止它。將兩個服務例如上面圖 1 中的服務 A 和服務 B 的請求/響應過程叫做一次 SpanTrace trees 在分布式追蹤系統中使用Trace表示對一次請求完整調用鏈的追蹤。可以看出每一次跟蹤 Trace 都是一個樹型結構,Span 可以體現出服務之間的具體依賴關係Annotation 用來及時記錄一個事件的存在,一些核心 Annotation 用來定義一個請求的開始和結束分布式鏈路追蹤就是記錄每個服務上發送和接收的每條消息的消息標識符和時間戳事件的集合,要將所有記錄條目與給定的發起者(RequestX)關聯上,以形成一個完整調用鏈。目前已提出兩類聚合該信息的解決方案: 黑盒和基於註解的監控方案。黑盒方案假設除了上述消息記錄之外沒有其他信息,並使用統計回歸技術來推斷該關聯。基於注釋的解決方案(Dapper)依賴應用程式或中間件使用全局標識符精確地為每一條記錄打標籤,該標識符將這些消息記錄連結到原始請求。雖然黑盒方案比基於註解的方案更便攜,但由於它們依賴於統計推斷,它們需要更多數據才能獲得足夠的準確性。顯然,基於註解的方法的主要缺點是需要工具程序。在我們的環境中,由於所有應用程式都使用相同的線程模型、控制流和RPC系統,因此我們發現可以將檢測限制在一小組公共庫中,並實現一個能對應用程式開發人員有效且透明的監控系統
對於每一個Trace樹,定義一個全局概率唯一64位的整數Trace ID。追蹤系統中用時間戳Span記錄一個服務調用的開始和結束時間--時間區間,每一個Span有一個Parent Span ID和自身的Span ID,沒有Parent Span ID的Span稱為Root Span,當前Span的Parent Span ID級為調用鏈路上遊的Span ID,所有的Span都關聯到一個特定的Trace並共享該Trace的Trace ID
下圖提供了一個典型Dapper追蹤Span中記錄事件的更詳細視圖,這個特定的Span描述了上圖中兩個 "Helper.Call" RPC的周期。Dapper的RPC庫工具記錄了Span開始和結束時間以及任何RPC時間信息。如果應用程式所有制選擇使用自己的註解(如圖"foo"註解)來擴充追蹤,則這些註解也會與其餘的Span數據一起記錄
Span可以包含來自不同主機的信息,事實上,每一個RPC Span可能包含客戶端和服務端兩個過程的Annotations內容。由於客戶端和服務端時間戳來源於不同主機,我們必須考慮Server之間的時鐘偏差。在我們的鏈路追蹤分析工具中,利用客戶端發送請求總是在服務端接收它之前,服務端響應消息總是在接收消息之後這樣一個事實,這樣每個服務端的RPC調用請求就有一個時間戳的上限和下限
從圖3可以很清楚的看出,這是一個Span名為"Helper.Call"的調用,Span ID是5,Span Parent ID是3,Trace ID是100。我們重點看看Span對應的四種狀態
Client Send(CS) 客戶端發起一個請求的發送時間,這個Annotation描述這個Span的開始Server Received(SR) 服務端接收時間,服務端獲得請求並準備開始處理它。如果將 SR 減去 CS 便可獲得網絡延遲Server Send(SS) 服務端發送時間,Annotation表明請求處理的完成(當請求返回客戶端)。如果 SS 減去 SR 便可獲得服務端處理請求的時間Client Received(CR) 客戶端接收時間,表明Span的結束,客戶端成功接收到服務端的響應,如果 CR 減去 CS 便可獲得客戶端從服務端獲取回復所需時間通過收集這四個時間戳,就可以在一次請求完成之後計算出整個Trace的執行耗時和網絡耗時,以及Trace中每個Span過程的執行耗時和網絡耗時
服務調用耗時 = CR - CS服務處理耗時 = SS - SR網絡耗時 = 服務調用耗時 - 服務處理耗時Dapper在應用開發人員幾乎零幹預的情況下能夠遵循分布式控制路徑,幾乎完全依賴於一些常用庫工具
當線程處理追蹤的控制路徑時,Dapper將追蹤上下文附加到線程本地存儲。追蹤上下文是一個小且易於複製的span屬性容器,例如trace和span id當計算延遲或異步時,大多數Google開發人員使用公共控制流庫來構造回調並在線程池或其他執行程序中調度它們。Dapper確保所有此類回調都存儲其創建者的追蹤上下文,並且在調用回調時,此追蹤上下文與響應的線程相關聯。通過這種方式,用於追蹤重建的Dapper Id能夠透明地遵循異步控制路徑幾乎所有Google的進程間通信圍繞一個帶有C++和Java綁定的RPC框架構建的。我們已經使用該框架來定義圍繞所有RPC的span,span和trace id從客戶端傳輸到伺服器以用於跟蹤RPC三階段收集Trace數據
Dapper追蹤日誌和收集管道是一個三階段過程
Span數據寫入本地日誌文件由Dapper收集器將這些數據通過Dapper守護進程從所有主機中拉出來最終將其寫入到Dapper的Bigtable倉庫中。一次Trace被設計成Bigtable的一行,,每一列相當於一個SpanBigtable支持稀疏表格布局正適合這種情況,因為一個Trace可能會產生任意數量的Span。追蹤數據收集的中位延遲(即數據從應用程式二進位文件採集並傳播到中央存儲庫所需要的時間)不到15秒,隨著時間的推移98%的延遲本身就是雙峰;大約75%的時間,98%收集延遲不超過2分鐘,但是其他大約25%的時間可以增長到幾個小時
帶外追蹤收集
出於兩個不相關的原因,Dapper系統使用請求樹本身執行帶外追蹤記錄和收集
首先,帶內收集方案(追蹤數據在RPC響應頭中發回)可能會影響應用程式網絡動態。在Google的許多大型系統中,尋找具有數千Span的Trace並不罕見,然而,RPC應答數據規模相對較小,通常不超過10KB。而頭部數據往往非常龐大,在這種情況下,如果將二者放在一起傳輸帶內Dapper追蹤數據會「矮化」應答數據,影響後續分析其次,帶內收集方案需要保證所有RPC都完全嵌套。我們發現有許多中間件系統在他們自己的後端返回最終結果之前將結果返回給調用者。帶內收集無法考慮這種非嵌套的分布式執行模式Dapper部署狀態
也許Dapper代碼庫中最關鍵部分的工具是RPC、線程和控制流庫,包括Span創建、採樣及記錄到本地磁碟。除了輕量級外,代碼還需要穩定和健壯,因為它連結了大量應用程式,使得維護和bug修復變得困難。核心工具不超過1000行C++ 代碼,在Java中不到800行。健值註解的實現增加了500行代碼
可以從兩個維度評估Dapper的滲透率:可以生成Dapper追蹤的生產過程比例(即那些被Dapper工具運行時庫連結的應用)和運行Dapper追蹤收集守護進程機器的比例。Dapper守護進程是我們基本機器鏡像的一部分,幾乎存在於Google的每個伺服器上
有些情況下,Dapper無法正確遵循控制路徑。這些通常源於使用非標準控制流原語,或者當Dapper錯誤地將因果關係歸因於不相關事件時。Dapper提供了一個簡單的庫來幫助開發人員手動控制追蹤傳播,目前有40個C++應用程式和33個Java應用程式需要一些手動追蹤傳播,相當於數千個傳播中的一小部分
管理追蹤開銷
由於生成鏈路追蹤和收集開銷以及存儲和分析追蹤數據所需的資源量,追蹤系統的成本會受到監控
追蹤鏈路生成的開銷是Dapper性能中最關鍵的部分,因為在緊急情況下收集和分析更容易被關閉。Dapper運行時庫中最重要的追蹤生成開銷是創建和銷毀Spans和Annotations並把它們記錄到本地磁碟以供後續收集。Root Span創建和解構平均需要204納秒,而Non Root Span相同的操作需要176納秒,不同之處在於為Root Span分配全局唯一Trace ID的額外成本
追蹤鏈路收集開銷讀取保存在本地磁碟的鏈路追蹤數據還可能干擾被監控中的前臺工作負載;將Dapper守護進程限制在內核調度程序中的最低優先級,以防在負載很重的主機中出現CPU爭用;Dapper也是輕量的網絡資源消費者,我們的存儲庫中每個Span平均對應426個字節,作為被監控的應用程式中網絡活動的一小部分,Dapper追蹤數據收集佔Google生產環境中不到0.01%的流量
對生產負載的影響每個使用大量機器的高吞吐量在線服務請求是最需要被有效追蹤的,它們趨向於生產大量的追蹤數據,它們也是對性能干擾敏感的
自適應採樣歸因於任何給定過程的Dapper開銷與每單位時間處理樣本追蹤數成比例。Dapper的第一個生產版本對Google的所有流程使用了統一的採樣頻率,平均每1024個候選者採樣一條Trace。這個簡單的方案對高吞吐量的在線服務是有效的,因為絕大多數感興趣的事件仍然可能經常出現以至於被捕獲。但是較低流量的工作負載可能在如此低的採樣率下丟失重要事件,同時容忍更高的採樣率和可接受的性能開銷。這種系統的解決方案是覆蓋默認採樣率,這需要我們在Dapper中試圖避免人工幹預。自適應採樣方案通過每單位時間所需的採樣追蹤速率參數化。這樣低流量的工作負載會自動提高其採樣率,而流量非常高的負載會降低採樣率從而控制開銷。實際採樣率跟Trace本身一起記錄;這有助於在Dapper數據周圍建立的分析工具中準確計算追蹤頻率
通用Dapper工具
Dapper從一個原型開始,迭代構建收集基礎架構、程序接口及基於web交互的用戶接口幫助Dapper用戶獨立解決他們的問題。以下介紹通用分析工具
Dapper Depot API(DAPI) 提供直接從Dapper區域存儲倉庫中訪問分布式鏈路追蹤記錄,DAPI和Dapper追蹤倉庫被設計為串行,DAPI意味著為Dapper存儲庫中的原始數據提供一個清晰直觀的界面。建議以下三種方式訪問鏈路追蹤數據通過Trace ID訪問 DAPI可以根據給定的全局唯一Trace ID加載任何Trace批量訪問 DAPI可以利用MapReduce並行提供對數十億Dapper Trace的訪問,用戶重寫一個虛擬函數,該函數接受Dapper Trace作為其唯一參數,並框架將在用用戶指定的時間窗口內為每個收集的Trace調用一次該函數索引訪問 Dapper倉庫支持單個索引匹配我們的公共訪問模式。該索引從常用的追蹤特徵映射到不同的Dapper Trace。由於Trace ID是為隨機分配的,因此這是快速訪問與特定服務或主機關聯Trace的最佳方法The Dapper user interface大多數Dapper使用都發生在基於web的交互式用戶界面中,下圖展示了典型的用戶工作流
小結
Google Dapper分布式鏈路追蹤,是諸多開源解決方案(Zipkin)的原型,它能夠實時監控分析應用性能,將其應用於複雜分布式微服務架構中也能快速幫助定位故障,是日常運維排障不可多得的好幫手,也是學習分布式鏈路追蹤的經典
推 薦 閱 讀
[1]dapper a large-scale distributed systems tracing infrastructure
(文章圖片引用自Dapper論文)