Dapper分散式跟蹤系統論文

cdh0805010118發表於2018-06-26

1. 背景

分散式系統在生產環境中執行可能存在一些耗時請求,我們可能很難定位,這裡有三種情況:

工程師無法準確地定位到這次全域性搜尋是呼叫了哪些服務,因為新的服務、或者服務上的某些片段,都有可能在任何時間上線或者修改過。

不能苛求工程師對所有參與這次全域性搜尋的服務都瞭如指掌,每個服務都有可能是由不同的團隊開發或維護的。

這些暴露出來的服務或者伺服器有可能同時被其他客戶端使用著,所以這些全域性搜尋的效能問題可能是由其他應用造成的。

上面這些情況都是可能出現的,所以針對Dapper,我們只有兩點要求:

無處不在的部署

持續的監控

上面兩點如果做到了,這我們可以對分散式系統中的任何服務都能做到持續監控和報表輸出,以及監控告警

針對上面的兩點要求,我們可以直接推出三個具體的設計目標:

低消耗:跟蹤系統對線上服務的影響做到足夠小。

應用級的透明:對於應用的開發者來說,是不需要知道有跟蹤系統這回事的。應用透明,才可以無所不在的透明侵入

延展性:Google至少在未來幾年的服務和叢集的規模下,監控系統都應該能完全把控住

一個額外的設計目標是為跟蹤資料產生之後,進行分析的速度要快,理想情況是資料存入跟蹤倉庫後一分鐘內就能統計出來。

做到真正的應用級別的透明,這應該是當下面臨的最有挑戰性的設計目標。具體做法:我們把核心跟蹤程式碼做得很輕巧,然後把它植入到哪些無所不在的公共元件中,比如:執行緒呼叫、控制流以及RPC庫。 我們發現,Dapper資料往往側重效能方面的調查。

Dapper吸收了一些優秀文章(Pinpoint, Magpie和X-Trace)的設計理念, 同時提出了做出了新的貢獻。例如:要想實現低損耗,採用率是很必要的, 即便是1/1000的採用率,對於跟蹤資料的使用層面上,也可以提供足夠多的資訊。

2. 設計方法

分散式跟蹤系統需要記錄在一次特定請求後,系統完成的所有工作資訊。例如:前端A、兩個中間層BC,以及兩個後端DE。當一個使用者發起一個請求時,首先到達前端,然後傳送兩個RPC到中間層BC,B立即返回,但是C需要和DE互動後再返回A,由A來響應最初的請求。

對於這樣一個請求,簡單使用的分散式跟蹤實現,就是為各個服務上每一次你傳送和接收動作,來收集跟蹤識別符號(message identifiers)和時間戳(timestamp), 前者用於呼叫鏈跟蹤,後者表示各個節點的時間開銷

為了將所有記錄條目與一個給定的發起者關聯上並記錄所有資訊,現在有兩種解決方案:黑盒(black-box)和基於標註(annotation-based)的監控方案。

黑盒方案:假定需要跟蹤的除了上述資訊之外沒有額外的資訊,這樣使用統計迴歸技術來推斷兩者之間的關係。

基於標註的方案:依賴於應用程式或者中介軟體明確地標記一個全域性ID,從而連結每一條記錄和發起者的請求。

兩者比較,黑盒方案比標註方案更輕便,他們需要更多的資料,以獲得足夠的精度,因為它們依賴於統計推論。標註方案最主要的缺點是,需要程式碼植入。

我們傾向於認為,Dapper跟蹤架構像是內嵌在RPC呼叫的樹形結構。然而,我們的核心資料模型不只侷限於我們特性RPC框架,我們還能跟蹤其他行為。從形式上看,我們的Dapper跟蹤模型使用的樹形結構,Span以及Annotation

2.1 跟蹤樹和span

在Dapper跟蹤樹結構中,樹節點是整個架構的基本單元,而每個節點又是對span的引用。節點之間的連線表示該節點span和它的父span直接的關係。雖然span在日誌檔案中只是簡單的代表span的開始和結束時間,他們在整個樹形結構中卻是相對獨立的。

|------------------------------time----------------------------------------------->
|     |-----------------------------------------------------------------|         |
|-----|      Frontend.Request[no parent id, and span_id: 1]             |---------|
|     |_________________________________________________________________|         |
|         |         backend.Call           |                                      |
|---------| [parent id: 1, and span_id: 2] |--------------------------------------|
|         |________________________________| _________________________________    |
|                                           |      backend.DoSomething        |   |
|-------------------------------------------|    [parent id: 1, span_id: 3 ]  |---|
|                                           |_________________________________|   |
|                                             |        helper.Call            |   |
|---------------------------------------------|    [parent id: 3, span_id: 4] |---|
|                                             |_______________________________|   |
|                                              |          helper.Call         |   |
|----------------------------------------------|   [parent id: 3, span_id: 5] |---|
|                                              |______________________________|   |
|---20-----22------24------26------28-------30-------32------34-----36--------38--|

上面這幅圖說明了span在一個大的跟蹤過程中是什麼樣的。Dapper記錄了span名稱,以及每個span的ID和父ID,已重建在一次追蹤過程中不同span之間的關係。如果一個span沒有父ID被稱為root span。所有span都掛在 一個特定的跟蹤上,也共用一個跟蹤ID。所以這些ID用全域性唯一的64位證照標示。在一個典型的Dapper跟蹤中,我們希望為每一個RPC對應到一個單一的span上,而且每個額外的元件層都對應一個跟蹤樹形結構的層級。

上幅圖所示的一個單獨span的細節圖

上圖給出了一個更詳細的典型Dapper跟蹤span記錄點檢視。它表示helper.Call的RPC(分別為server端和client端). span的開始時間和結束時間,以及任何RPC的時間資訊都通過Dapper在RPC元件庫的植入記錄下來。如果應用程式開發者選擇在跟蹤中增加他們自己的註釋(如圖中"foo"的註釋)(業務資料),這些資訊也會和其他span資訊一樣記錄下來。

記住,任何一個span可以包含來自不同的主機資訊,這些也要記錄下來。事實上,每一個RPC span可以包含客戶端和伺服器兩個過程的註釋,使得連線兩個主機的span會成為模型中所說的span。

2.2 植入點-Dapper的透明性

Dapper可以對應用開發者近乎零侵入的成本,對分散式控制路徑進行跟蹤,幾乎完全依賴於基於少量通用元件庫的改造。如下:

當一個執行緒在處理跟蹤控制路徑的過程中,Dapper把這次跟蹤的上下文在ThreadLocal中進行儲存。追蹤上下文是一個小而且容易複製的容器,其中承載了Scan的屬性如跟蹤ID和span id。

當計算過程是延遲呼叫的或者非同步的,大多數Google開發者通過執行緒池或者其他執行器,使用一個通用的控制流庫來回撥。Dapper確保所有的回撥可以儲存這次跟蹤的上下文,而當回撥函式被處罰時,這次跟蹤的上下文會與適當的執行緒關聯上。在這種方式上,Dapper可以使用trace ID和span ID來輔助構建非同步呼叫的路徑。

幾乎所有的Google程式間通訊是建立在一個用C++和Java開發的RPC框架上。我們把跟蹤植入該框架來定義RPC中所有的span。span ID和跟蹤ID會從客戶端傳送到服務端。像那樣的基於RPC的系統被廣泛應用在Google中,這是一個重要的植入點。當那些非RPC通訊框架發展成熟並找到了自己的使用者群之後,我們會計劃對RPC通訊框架植入。

Dapper的跟蹤資料是獨立於語言的,很多在生產環境中的跟蹤結合了用C++和Java寫的程式資料。在後面的章節中,我們討論應用程式的透明度時,我們會把這些理論是如何實踐的進行討論。

2.3 標註:Annotation
// C++
const string& request = ...;
if (HitCache())
    TRACEPRINTF("cache hit for %s", request.c_str());
else
    TRACEPRINTF("cache miss for %s", request.c_str());

// Java:
Tracer t = Tracer.getCurrentTracer();
string request = ...;
if (hitCache())
    t.Record("cache hit for "+ request);
else 
    t.Record("cache miss for "+ request);

上述植入點足夠推匯出複雜的分散式系統的跟蹤細節,使得Dapper的核心功能在不改動Google應用的情況下可用。然而,Dapper還允許應用程式開發人員在Dapper跟蹤的過程中新增額外的資訊,已監控更高階別的系統行為,或幫助除錯問題。我們允許使用者通過一個簡單的API定義帶時間戳的Annotation,核心的示例程式碼如圖4所示。這些Annotation可以新增任意內容。為了保護Dapper的使用者過分對日誌記錄特別感興趣,每個跟蹤span有一個可配置的總Annotation量的上限。但是,應用程式的Annotation是不能代替用於表示span結構的資訊和記錄著RPC相關的資訊

2.4 取樣率

低損耗是Dapper的一個關鍵設計目標。具體做法:為了減少分散式跟蹤系統對應用效能的損耗影響,那就是在遇到大量請求時只記錄其中的一小部分。

2.5 跟蹤的收集

Dapper日誌收集流程

Dapper的跟蹤記錄和收集管道的過程分為三個階段(見上圖)。

span資料寫入(1)本地日誌檔案中。

Dapper的守護程式和收集元件把這些資料從生產環境的主機中拉出來(2)

寫到(3)Dapper的Bigtable倉庫中。

一次跟蹤被設計成Bigtable中的一行,每一列相當於一個span。且Bigtable支援稀疏矩陣

2.6 帶外資料跟蹤收集

tip1: 帶外資料:傳輸層協議使用帶外資料(out-of-band, OOB)來傳送一些重要資料,如果通訊一方有重要的資料需要通知對方時,協議能夠將這些資料快速地傳送到對方。為了傳送這些資料,協議一般不適用與普通資料相同的通道,而是使用另外的通道。

tip2: 這裡指的in-band策略是把跟蹤資料隨著呼叫鏈進行傳送,out-of-band是通過其它的鏈路進行跟蹤資料的收集,Dapper的寫日誌然後進行日誌採集的方式就屬於out-of-band策略

3. Dapper部署情況

3.1 Dapper執行庫

Dapper程式碼中最關鍵的部分,就是對基礎RPC、執行緒控制和流程控制元件庫的植入,其中包括span的建立,取樣率的設定,以及把日誌寫入本地磁碟。

3.2 生產環境下的涵蓋面

Dapper的滲透總結為兩個方面:

可以建立Dapper跟蹤過程,這個需要在生產環境上執行Dapper跟蹤收集守護程式

在某些情況下Dapper是不能正確的跟蹤控制路徑的,這些通常源於使用非標準的控制流,或是Dapper錯誤地把路徑關聯歸到不相關的事情上

考慮到生產環境的安全,Dapper也是可以直接關閉的

Dapper提供了一個簡單的庫來幫助開發者手動控制跟蹤傳播作為一種變通方法。 還有一些程式使用了非元件性質的通訊庫(比如:原生的TCP Socket或者SOAP RPC),這些也不能直接支援Dapper的跟蹤。

3.3 跟蹤Annotation的使用

開發者傾向於使用特定應用程式的Annotation, 無論是作為一種分散式除錯日誌檔案,還是通過一些應用程式特定的功能對跟蹤進行分類。

目前,70%的Dapper span和90%的所有Dapper跟蹤,都至少有一個特殊應用的Annotation。

4. 處理跟蹤損耗

跟蹤系統的成本由兩部分構成:

  1. 正在被監控的系統,在生成追蹤和手機追蹤資料的消耗導致系統效能下降。
  2. 需要使用一部分資源來儲存和分析跟蹤資料。

下面主要展示三個方面:

  1. Dapper元件操作的消耗;
  2. 跟蹤收集的消耗;
  3. Dapper對生產環境負載的影響。

4.1 生成跟蹤的損耗

生成跟蹤的開銷是Dapper效能影響中最關鍵的部分,因為收集和分析可以更容易在緊急情況下關閉。

Dapper執行庫中最重要的跟蹤生成消耗在於建立和銷燬span和annotation, 並記錄到本地磁碟供後續的收集

根span的建立和銷燬需要損耗平均204ns,而同樣的操作在其他span上需要消耗176ns。時間上的差別主要在於需要給span跟蹤分配一個全域性唯一的ID

如果一個span沒有被取樣的話,那麼這個額外的span建立Annotation的成本幾乎可以忽略不計

在Dapper執行期寫入到本地磁碟,是最昂貴的操作,但是通過非同步寫入,他們可見損耗大大減少

4.2 跟蹤收集的消耗

獨處跟蹤資料也會對正在被監控的負載產生干擾。

在最壞的情況,跟蹤資料處理中,Dapper守護程式被拉取跟蹤資料時的CPU使用率,也沒有超過0.3%。而且限制Dapper守護程式為核心scheduler最低的優先順序,以防發生CPU競爭

4.3 在生產環境下對負載的影響

延遲和吞吐量帶來的損失在把取樣率調整到1/16之後就全部在實驗誤差範圍內。在實踐中,我們發現即便取樣率調整到1/1024,仍然是有足夠量的跟蹤資料的用來跟蹤大量的服務。保持Dapper的效能損耗基線在一個非常低的水平是很重要的。

相關文章