[說明:本文是閱讀Google論文“Dapper, a Large-Scale Distributed Systems Tracing Infrastructure”之后的一個簡要總結,完整譯文可參考此處。 另論文“Uncertainty in Aggregate Estimates from Sampled Distributed Traces”中有關於采樣的更詳細分析。此外,Twitter開源的Zipkin就是參考Google Dapper而開發。]
Dapper最初是為了追蹤在線服務系統的請求處理過程。比如在搜索系統中,用戶的一個請求在系統中會經過多個子系統的處理,而且這些處理是發生在不同機器甚至是不同集群上的,
當請求處理發生異常時,需要快速發現問題,並准確定位到是哪個環節出了問題,這是非常重要的,Dapper就是為了解決這樣的問題。
對系統行為進行跟蹤必須是持續進行的,因為異常的發生是無法預料的,而且可能是難以重現的。同時跟蹤需要是無所不在,遍布各處的,否則可能會遺漏某些重要的點。基於此
Dapper有如下三個最重要的設計目標:
低的額外開銷,對應用的透明性,可擴展。同時產生的
跟蹤
數據需要可以被快速分析,這樣可以幫助用戶
實時
獲取在線服務狀態。
實現方法
低的額外開銷:不是對所有的請求進行跟蹤而是要采樣,收集跟蹤數據時進行二次采樣
對應用的透明性:
修改多線程,控制流,RPC等基礎庫代碼,插入負責跟蹤的代碼。
在Google,應用使用的是相同的
多線程,控制流,RPC等基礎庫代碼,所以僅通過修改它們就可以實現跟蹤功能。
當線程進行一個被跟蹤的處理時,Dapper會將一個trace context關聯到線程本地化存儲中。trace context中包含了span相關屬性,比如trace和span id。對於需要進行異步處理的情況,Google開發者通常都會采用一個通用的控制流庫來實現回調,並將它們調度到一個線程池或是執行器中調用。Dapper保證所有回調都會保存它們創建者的
trace context,同時在該回調被調用時該
trace context也會被關聯到對應線程。這樣,Dapper就可以實現這種異步處理過程的跟蹤。對於被跟蹤的RPC調用,span和trace id也會跟着被從客戶端傳到服務端。從功能上看這部分代碼主要包括span的創建,采樣,本地磁盤日志寫入,但是因為它會被很多應用所依賴,維護和bug fix難度高,需要非常高的穩定性和健壯性。同時還要輕量,實際上這部分代碼的
C++實現
也總共不到1000行。
Dapper支持用戶直接獲取Tracer對象,並輸出自己的自定義信息,用戶可以輸出自己任意想輸出的內容,為防止用戶過度輸出,提供用戶可配置參數來控制其上限。
跟蹤時需要對請求進行標記,會產生一個唯一ID(在Dapper中是一個64位整數)用來標識該請求。
對於Dapper來說,一個trace(跟蹤過程)實際上是一顆樹,樹中的節點被稱為一個span,根節點被稱為root span。如下圖所述:



跟蹤開銷
如果跟蹤帶來的額外開銷太高,用戶通常會選擇關掉它,因此低開銷非常重要。采樣可以降低開銷,但是簡單的采樣可能導致采樣結果無代表性,Dapper通過采用自適應的采樣機制來滿足性能和代表性兩方面的需求。
trace的生成開銷對Dapper來說是最關鍵的,因為數據的收集和分析可以臨時關掉,只要數據一直生成就可以后面再進行收集分析。在trace生成中,最大頭的地方在於span和annotation的創建和銷毀上。根span的創建和銷毀平均需要204ns,普通span只需要176ns,區別在於根span需要產生一個全局唯一的trace id。如果span沒有被采樣到,那么對它添加
annotation的開銷基本可忽略,大概需要9ns。但是如果是被采樣到的話,那么平均開銷是40ns。這些測試是在一個2.2GHZ的x86服務器上進行的。本地磁盤寫入是Dapper運行庫中最昂貴的操作,但是它們可以異步化,批量化,因此它們基本上只會影響到那些高吞吐率的應用。
將跟蹤數據通過Dapper后台進程讀出也會產生一些開銷。但是根據我們的觀察,Dapper daemon的CPU開銷始終在0.3%之下,內存占用也很少,此外也會帶來輕量的網絡開銷,但是每個span平均只有426字節大小,網絡開銷只占了整個產品系統流量的0.01%不到。
對於那些每個請求都可能產生大量跟蹤數據的應用來說,我們還會通過采樣來降低開銷。我們通過保證跟蹤開銷可以始終保持在很低的水平上,使得用戶可以放心大膽的使用它。最初我們的采樣策略很簡單,是在每1024個請求中選擇一個進行跟蹤,這種模式對於那種請求量很高的服務來說,可以保證跟蹤到有價值的信息,但是對於那些負載不高的服務來說,可能會導致采樣頻率過低,從而遺漏重要信息。因此我們決定以時間為采樣單位,保證單位時間內可以進行固定次數的采樣,這樣采樣頻率和開銷都更好控制了。
應用
用戶可以通過DAPI(Dapper“Depot API”)直接訪問跟蹤數據。DAPI提供如下幾種訪問方式:指定trace id進行訪問;大規模批量訪問,用戶可以通過MapReduce job並行訪問,用戶只需要實現一個以Dapper trace為參數的虛函數,在該函數內完成自己的處理即可,框架負責為用戶指定時間段內的所有trace調用該函數;通過索引進行訪問,Dapper會為跟蹤數據建立索引,用戶可通過索引進行查詢,因為trace id是隨機生成的,因此用戶通常需要通過服務名或機器名進行檢索(實際上Dapper是按照(服務名,機器名,時間戳)進行索引的)。
大部分用戶都是通過一個交互式web接口來使用Dapper的,典型流程如下圖所示:

1.用戶輸入他感興趣的服務和時間窗口,選擇相應跟蹤模式(這里是span名稱),以及他最關心的某個度量參數(這里是服務延遲)
2.頁面上會展示一個指定服務的所有分布式執行過程的性能摘要,用戶可能會對這些執行過程根據需要進行排序,然后選擇一個詳細看
3.一旦用戶選定了某個執行過程后,將會有一個關於該執行過程的圖形化描述展現出來,用戶可以點擊選擇自己關心的那個過程
4.系統根據用戶在1中選擇的度量參數,以及3中選擇的具體過程,顯示一個直方圖。在這里顯示的是有關getdocs的延遲分布的直方圖,用戶可以點擊右側的example,選擇具體的一個執行過程進行查看
5.顯示關於該執行過程的具體信息,上方是一個時間軸,下方用戶可以進行展開或折疊,查看該執行過程各個組成部分的開銷,其中綠色代表處理時間,藍色代表花在網絡上的時間。
經驗教訓
在開發過程中使用Dapper,可以幫助用戶提高性能(分析請求延遲,發現關鍵路徑上不必要的串行化),進行正確性檢查(用戶請求是否正確發送給了服務提供者),理解系統(請求處理可能依賴很多其他系統,Dapper幫助用戶了解總體延遲,重新設計最小化依賴),測試(新代碼release前要通過一個Dapper跟蹤測試,驗證系統行為和性能)。比如,通過使用Dapper,Ads Review團隊將延遲降低了兩個數量級。
同時我們還將跟蹤系統與異常監控系統進行集成,如果異常發生在一個采樣到的Dapper tracer上下文中,那么相應的trace和span id還會被作為異常報告的元數據,前端的異常監控服務還會提供相應鏈接指向跟蹤系統。這樣可以幫助用戶了解異常發生時的情況。
解決長尾延遲,幫助用戶分析復雜系統環境下的延遲問題。網絡性能的瞬時下降不會影響系統的吞吐率,但是對延遲有很大影響。很多開銷昂貴的查詢模式是由於未預料到的服務間的交互造成的,Dapper的出現使得
這種問題的
發現變得非常容易。
幫助用戶進行服務間依賴的推理。Google維護了非常多的集群,每個集群上承載了各種各樣的任務,而任務間可能存在依賴關系。但是各個任務需要精確知道它所依賴的服務信息,以幫助發現瓶頸或進行服務的移動。服務間的依賴是復雜的而且是動態的,單純依賴配置文件很難判斷。但是通過使用Dapper的trace信息和DAPI MapReduce接口可以自動化的確定服務間依賴關系。
幫助網絡管理員對跨集群的網絡活動進行應用層的分析。幫助發現某些昂貴的網絡請求開銷的產生原因。
很多存儲系統都是共享的。比如GFS,會有很多用戶,有的可能是直接訪問GFS,有的可能是比如通過Bigtable產生對GFS的訪問,如果沒有Dapper,對這種共享式系統將會很難調試,通過Dapper提供的數據,共享服務的owner可以方便的對用戶根據各項指標(比如網絡負載,請求耗時)進行排序。
救火。但是不是所有的救火,都可以使用Dapper來完成
。比如,那些正在救火的Dapper用戶需要訪問最新的數據,但是他可能根本沒有時間寫一個新的DAPI代碼或者等待周期性報告的產生。對於那些正在經歷高延遲的服務,Dapper的用戶接口並不適於用來快速定位延遲瓶頸。但是可以直接與Dapper daemon進行交互,通過它可以很容易地收集最新數據。在發生災難性的故障時,通常沒有必要去看統計結果,單個例子就可以說明問題。但是對於那些共享存儲服務來說,信息
聚合
會很重要。對於共享服務來說,Dapper的聚合信息可以用來做事后分析。但是如果這些信息的聚合無法在問題爆發后的10分鍾之內完成,那么它的作用將會大大削弱,因此對於共享服務來說,Dapper在救火時的效果沒有想象中的那么好。
通過開放跟蹤數據給用戶,激發了用戶創造力,產生了很多意料之外的應用。
那些沒有跟蹤功能的應用只需要用新的庫重新編譯下它們的程序,就獲得了跟蹤功能,遷移非常方便。
但是還存在如下一些需要改進的地方:
合並產生的影響。我們通常假設各種子系統會一次處理一個請求。但是某些情況下,請求會被緩存,然后一次性地在一組請求上執行某個操作(比如磁盤寫入)。在這種情況下,被追蹤的請求拿到的實際上並不是它本身的處理過程。
對批量處理進行跟蹤。雖然Dapper是為在線服務系統而設計,最初是為了理解用戶發送請求給Google后產生的一系列系統行為。但是離線的數據處理實際上也有這樣的需求。在這種情況下,我們可以將trace id與某些有意義的工作單元進行關聯,比如輸入數據里的一個key(或者是一個key range)。
尋找根本原因。Dapper可以迅速找到系統短板,但是在尋找根本原因時並不那么高效。比如某個請求變慢可能並不是因為它自己的原因,而是因為在它之前已經有很多請求在排隊。用戶可以在應用層將某些參數,比如隊列大小交給跟蹤系統。
記錄內核級信息。我們有很多工具可以進行內核執行過程的跟蹤和profiling,但是直接將內核級信息捆綁到一個應用層的trace context中很難優雅的實現。我們打算采用一種折中的解決方案,通過在應用層獲取內核活動參數的快照,然后將它們與一個活動span關聯起來。