歡迎光臨
每天分享高質量文章

螞蟻金服分散式鏈路跟蹤元件取樣策略和原始碼 | 剖析

SOFA
Scalable Open Financial  Architecture 

是螞蟻金服自主研發的金融級分散式中介軟體,包含了構建金融級雲原生架構所需的各個元件,是在金融場景裡錘煉出來的最佳實踐。

 

SOFATracer 是一個用於分散式系統呼叫跟蹤的元件,透過統一的 TraceId 將呼叫鏈路中的各種網路呼叫情況以日誌的方式記錄下來,以達到透視化網路呼叫的目的,這些鏈路資料可用於故障的快速發現,服務治理等。

 

本文為《剖析 | SOFATracer 框架》第四篇,本篇作者米麒麟,來自陸金所。

《剖析 | SOFATracer 框架》系列由 SOFA 團隊和原始碼愛好者們出品,目前領取已經完成,感謝大家的參與。

 

SOFATracer: 

https://github.com/alipay/sofa-tracer

前言

由於分散式鏈路追蹤涉及到呼叫的每個環節,而每個環節都會產生大量的資料,為了儲存這種資料,可能需要大量的成本,另外在實際的生產過程中並非所有資料都是值得關註的。

基於這些原因,SOFATracer 提供鏈路資料取樣功能特性,一方面可以節約 I/O 磁碟空間,另一方面需要把無關資料直接過濾篩選。目前 SOFATracer 內建兩種取樣策略,一種是基於固定比率的取樣,另一種是基於使用者擴充套件實現的自定義取樣。自定義取樣樣式將 SofaTracerSpan 實體作為取樣計算的條件,使用者可以基於此實現自行擴充套件自定義的取樣規則。

本篇文章主要介紹 SOFATracer 資料取樣策略原理,透過剖析原始碼實現詳細講述取樣規則演演算法。

Dapper 論文中的取樣模型與策略

跟蹤取樣模型

每個請求都會利用到大量伺服器高吞吐量的線上服務,這是對有效跟蹤最主要的需求之一。這種情況需要生成大量的跟蹤資料,並且他們對效能的影響是最敏感的。延遲和吞吐量帶來的損失在把取樣率調整到小於1/16之後就能全部在實驗誤差範圍內。

在實踐中,我們發現即便取樣率調整到 1/1024 仍然是有足夠量的跟蹤資料用來跟蹤大量的服務。保持鏈路跟蹤系統的效能損耗基線在一個非常低的水平是很重要的,因為它為那些應用提供了一個寬鬆的環境使用完整的 Annotation API 而無懼效能損失。使用較低的取樣率還有額外好處,可以讓持久化到硬碟中的跟蹤資料在垃圾回收機制處理之前保留更長時間,這樣為鏈路跟蹤系統的收集元件提供更多靈活性。

分散式鏈路跟蹤系統中任何給定行程的消耗和每個行程單位時間的跟蹤取樣率成正比。然而,在較低的取樣率和較低的傳輸負載下可能會導致錯過重要事件,而想用較高的取樣率就需要能接受的相應的效能損耗。我們在部署可變取樣的過程中,引數化配置取樣率時,不是使用一個統一的取樣方案,而是使用一個取樣期望率來標識單位時間內取樣的追蹤。這樣一來,低流量低負載會自動提高取樣率,而在高流量高負載的情況下會降低取樣率,使損耗一直保持在控制之內。實際使用的取樣率會隨著跟蹤本身記錄下來,這有利於從跟蹤資料裡準確分析排查

跟蹤取樣策略

要真正做到應用級別的透明,我們需要把核心跟蹤程式碼做的很輕巧,然後把它植入到那些無所不在的公共元件中,比如執行緒呼叫、控制流以及 RPC 庫。使用自適應的取樣率可以使鏈路跟蹤系統變得可伸縮,並且降低效能損耗。鏈路跟蹤系統的實現要求效能低損耗,尤其在生產環境中不能影響到核心業務的效能,也不可能每次請求都跟蹤,所以要進行取樣,每個應用和服務可以自己設定取樣率。取樣率應該是在每個應用自己的配置裡設定的,這樣每個應用可以動態調整,特別是應用剛上線時可以適當調高取樣率。一般在系統峰值流量很大的情況下,只需要取樣其中很小一部分請求,例如 1/1000 的取樣率,即分散式跟蹤系統只會在 1000 次請求中取樣其中的某一次。

在 Dapper 論文中強調了資料取樣的重要性,如果將每條埋點資料都掃清到磁碟上會增大鏈路追蹤框架對原有業務效能的影響。如果取樣率太低,可能會導致一些重要資料的丟失。 論文中提到如果在高併發情況下 1/1024 的取樣率是足夠的,也不必擔心重要事件資料的丟失。因為在高併發環境下,一個異常資料出現一次,那麼就會出現1000 次。 然而在併發量不是很多的系統,並且對資料極為敏感時需要讓業務開發人員手動設定取樣率。

對於高吞吐量服務,積極取樣並不妨礙最重要的分析。如果一個顯著的操作在系統中出現一次,他就會出現上千次。低吞吐量服務可以負擔得起跟蹤每一個請求。這是促使我們下決心使用自適應取樣率的原因。為了維持物質資源的需求和漸增的吞吐要求之間的靈活性,我們在收集系統自身上增加了額外的取樣率支援。

如果整個跟蹤過程和收集系統只使用一個取樣率引數確實會簡單一些,但是這就不能應對快速調整在所有部署節點上的執行期取樣率配置的這個要求。我們選擇了執行期取樣率,這樣就可以優雅的去掉我們無法寫入到倉庫中的多餘資料。我們還可以透過調節收集系統中的二級取樣率繫數來調整這個執行期取樣率。Dapper 的管道維護變得更容易,因為我們可以透過修改二級取樣率的配置,直接增加或減少全域性改寫率和寫入速度。

SOFATracer 的取樣原始碼剖析

SOFATracer 提供鏈路資料取樣功能特性,支援兩種取樣策略:基於固定取樣率的取樣樣式和基於使用者擴充套件實現的自定義取樣樣式。

取樣介面模型

SOFATracer 提供定義鏈路追蹤資料取樣樣式介面 com.alipay.common.tracer.core.samplers.Sampler,此介面 sample 方法透過 SofaTracerSpan 實體引數作為取樣計算基礎條件決定鏈路是否取樣,實現豐富的資料取樣規則。

SOFATracer 基於 com.alipay.common.tracer.core.samplers.SamplerFactory 生成的取樣器執行鏈路資料取樣基本流程:

 

  • 構建鏈路追蹤器,透過取樣器工廠 SamplerFactory 根據自定義取樣規則實現類全限定名配置生成指定策略取樣器 Sampler,其中基於使用者擴充套件實現的取樣樣式優先順序高,預設取樣策略為基於固定取樣率的取樣計算規則;
  • Reporter 資料上報 reportSpan 或者鏈路跨度 SofaTracerSpan 啟動呼叫取樣器 sample 方法檢查鏈路是否需要取樣,獲取取樣狀態 SamplingStatus 是否取樣標識 isSampled。

取樣器的初始化

上面分析到,取樣策略實體是透過 SamplerFactory 來建立的,SamplerFactory 中提供了一個 getSampler 方法用於獲取取樣器:

從程式碼片段來看,使用者自定義的取樣策略將會優先被載入,如果在配置檔案中沒有找到自定義的 ruleClassName ,則構建預設的基於固定取樣率的取樣器。SamplerProperties 是取樣相關的配置屬性,預設提供的基於固定比率的取樣率是 100%,即預設情況下,所有的 Span 資料都會被記錄到日誌檔案中。關於具體配置,在下文案例中會有詳細介紹。

取樣計算

取樣是對於整條鏈路來說的,也就是說從 RootSpan 被建立開始,就已經決定了當前鏈路資料是否會被記錄了。在 SofaTracer 類中,Sampler 實體作為成員變數存在,並且被設定為 final,也就是當構建好 SofaTracer 實體之後,取樣策略就不會被改變。當 Sampler 取樣器系結到 SofaTracer 實體之後,SofaTracer 對於產生的 Span 資料的落盤行為都會依賴取樣器的計算結果(針對某一條鏈路而言)。

SOFATracer 構建 Span 區別於 OpenTracing 規範中基於 SpanBuilder#start 開始一個新的 Span 的定義:

  • 基於 OpenTracing 規範的實現,SofaTracerSpanBuilder#start
  • 基於 SofaTracerSpanContext 構建

對於第一種,會在 start 方法中實現計算,然後設定到 sofaTracerSpanContext 用於向下遊鏈路中進行透傳。下麵是第一種情況下計算當前 Span 是否需要取樣的邏輯:

第二種情況下是基於 SofaTracerSpanContext 構建,SOFATracer 中 SofaTracerSpanContext 的建構式預設會設定為不取樣,那麼對於這種情況,SOFATracer 會將取樣計算延遲到 Span 上報時進行,此時計算的條件是 SofaTracer 中有取樣器存在並且當前 Span 必須是 rootSpan :

取樣標記透傳

SOFATracer 在進行跨行程資料透傳時,會將取樣標記放在透傳資料中,隨著鏈路資料一直向下遊進行透傳。取樣標記的 key 為 X-B3-Sampled。當下游服務透過此 key 解析出取樣標記時,會直接在當前服務中使用此取樣標記,而不用再去重新計算。

取樣策略實現

SOFATracer 預設取樣策略使用基於固定取樣率透過 BitSet 底層實現的取樣樣式 SofaTracerPercentageBasedSampler,取樣計算規則核心實現入口:

SofaTracerPercentageBasedSampler 基於固定取樣比率採用時間複雜度為 O(N) 的蓄水池取樣演演算法 Reservoir Sampling 構建隨機 BitSet 檢查是否取樣。蓄水池取樣演演算法從包含 n 個專案的集合 S 中選取 k 個樣本,其中 n 為一很大或未知的數量,具體取樣步驟包括:

  1. 從集合 S 中抽取首 k 項放入「水塘」中
  2. 對於每一個 S[j] 項(j ≥ k):
  •    隨機產生一個範圍從 0 到 j 的整數 r
  •    若 r < k 則把水塘中的第 r 項換成 S[j] 項

SofaTracerPercentageBasedSampler 基於蓄水池取樣演演算法建立隨機 BitSet 來源 Stack Overflow:

取樣使用示例

使用 SOFATracer 的取樣能力基於 tracer-sample-with-springmvc 工程,除 application.properties 之外,其他均相同。

固定取樣率樣式

SOFATracer 提供基於固定取樣率的取樣實現,取樣樣式需設定為 PercentageBasedSampler 。當 com.alipay.sofa.tracer.samplerName=PercentageBasedSampler 時,使用者需配置com.alipay.sofa.tracer.samplerPercentage 取樣率。

透過 application.properties 增加取樣相關配置項提供基於固定取樣率的取樣樣式:

固定取樣率驗證方式:

  • 當取樣率設定為 100 時,每次都會列印摘要日誌
  • 當取樣率設定為 0 時,不列印
  • 當取樣率設定為 0~100 之間時,按機率列印

以請求 10 次來驗證下結果。

1.當取樣率設定為 100 時,每次都會列印摘要日誌
啟動工程,瀏覽器中輸入:http://localhost:8080/springmvc ;並且掃清地址 10 次,檢視日誌如下:

  1.  

    {"time":"2018-11-09 11:54:47.643","local.app":"SOFATracerSpringMVC","traceId":"0a0fe8ec154173568757510019269","spanId":"0.1","request.url":"http://localhost:8080/springmvc","method":"GET","result.code":"200","req.size.bytes":-1,"resp.size.bytes":0,"time.cost.milliseconds":68,"current.thread.name":"http-nio-8080-exec-1","baggage":""}

  2. {"time":"2018-11-09 11:54:50.980","local.app":"SOFATracerSpringMVC","traceId":"0a0fe8ec154173569097710029269","spanId":"0.1","request.url":"http://localhost:8080/springmvc","method":"GET","result.code":"200","req.size.bytes":-1,"resp.size.bytes":0,"time.cost.milliseconds":3,"current.thread.name":"http-nio-8080-exec-2","baggage":""}

  3. {"time":"2018-11-09 11:54:51.542","local.app":"SOFATracerSpringMVC","traceId":"0a0fe8ec154173569153910049269","spanId":"0.1","request.url":"http://localhost:8080/springmvc","method":"GET","result.code":"200","req.size.bytes":-1,"resp.size.bytes":0,"time.cost.milliseconds":3,"current.thread.name":"http-nio-8080-exec-4","baggage":""}

  4. {"time":"2018-11-09 11:54:52.061","local.app":"SOFATracerSpringMVC","traceId":"0a0fe8ec154173569205910069269","spanId":"0.1","request.url":"http://localhost:8080/springmvc","method":"GET","result.code":"200","req.size.bytes":-1,"resp.size.bytes":0,"time.cost.milliseconds":2,"current.thread.name":"http-nio-8080-exec-6","baggage":""}

  5. {"time":"2018-11-09 11:54:52.560","local.app":"SOFATracerSpringMVC","traceId":"0a0fe8ec154173569255810089269","spanId":"0.1","request.url":"http://localhost:8080/springmvc","method":"GET","result.code":"200","req.size.bytes":-1,"resp.size.bytes":0,"time.cost.milliseconds":2,"current.thread.name":"http-nio-8080-exec-8","baggage":""}

  6. {"time":"2018-11-09 11:54:52.977","local.app":"SOFATracerSpringMVC","traceId":"0a0fe8ec154173569297610109269","spanId":"0.1","request.url":"http://localhost:8080/springmvc","method":"GET","result.code":"200","req.size.bytes":-1,"resp.size.bytes":0,"time.cost.milliseconds":1,"current.thread.name":"http-nio-8080-exec-10","baggage":""}

  7. {"time":"2018-11-09 11:54:53.389","local.app":"SOFATracerSpringMVC","traceId":"0a0fe8ec154173569338710129269","spanId":"0.1","request.url":"http://localhost:8080/springmvc","method":"GET","result.code":"200","req.size.bytes":-1,"resp.size.bytes":0,"time.cost.milliseconds":2,"current.thread.name":"http-nio-8080-exec-2","baggage":""}

  8. {"time":"2018-11-09 11:54:53.742","local.app":"SOFATracerSpringMVC","traceId":"0a0fe8ec154173569374110149269","spanId":"0.1","request.url":"http://localhost:8080/springmvc","method":"GET","result.code":"200","req.size.bytes":-1,"resp.size.bytes":0,"time.cost.milliseconds":1,"current.thread.name":"http-nio-8080-exec-4","baggage":""}

  9. {"time":"2018-11-09 11:54:54.142","local.app":"SOFATracerSpringMVC","traceId":"0a0fe8ec154173569414010169269","spanId":"0.1","request.url":"http://localhost:8080/springmvc","method":"GET","result.code":"200","req.size.bytes":-1,"resp.size.bytes":0,"time.cost.milliseconds":2,"current.thread.name":"http-nio-8080-exec-6","baggage":""}

  10. {"time":"2018-11-09 11:54:54.565","local.app":"SOFATracerSpringMVC","traceId":"0a0fe8ec154173569456310189269","spanId":"0.1","request.url":"http://localhost:8080/springmvc","method":"GET","result.code":"200","req.size.bytes":-1,"resp.size.bytes":0,"time.cost.milliseconds":2,"current.thread.name":"http-nio-8080-exec-8","baggage":""}

     

2. 當取樣率設定為 0 時,不列印
啟動工程,瀏覽器中輸入:http://localhost:8080/springmvc ;並且掃清地址 10 次,檢視 ./logs/tracerlog/ 目錄,沒有 spring-mvc-degist.log 日誌檔案

3. 當取樣率設定為 0~100 之間時,按機率列印
這裡設定成 20

  • 掃清 10 次請求

  1.  

    {"time":"2018-11-09 12:14:29.466","local.app":"SOFATracerSpringMVC","traceId":"0a0fe8ec154173686946410159846","spanId":"0.1","request.url":"http://localhost:8080/springmvc","method":"GET","result.code":"200","req.size.bytes":-1,"resp.size.bytes":0,"time.cost.milliseconds":2,"current.thread.name":"http-nio-8080-exec-5","baggage":""}

  2. {"time":"2018-11-09 12:15:21.776","local.app":"SOFATracerSpringMVC","traceId":"0a0fe8ec154173692177410319846","spanId":"0.1","request.url":"http://localhost:8080/springmvc","method":"GET","result.code":"200","req.size.bytes":-1,"resp.size.bytes":0,"time.cost.milliseconds":2,"current.thread.name":"http-nio-8080-exec-2","baggage":""}

     

  • 掃清 20 次請求

  1.  

    {"time":"2018-11-09 12:14:29.466","local.app":"SOFATracerSpringMVC","traceId":"0a0fe8ec154173686946410159846","spanId":"0.1","request.url":"http://localhost:8080/springmvc","method":"GET","result.code":"200","req.size.bytes":-1,"resp.size.bytes":0,"time.cost.milliseconds":2,"current.thread.name":"http-nio-8080-exec-5","baggage":""}

  2. {"time":"2018-11-09 12:15:21.776","local.app":"SOFATracerSpringMVC","traceId":"0a0fe8ec154173692177410319846","spanId":"0.1","request.url":"http://localhost:8080/springmvc","method":"GET","result.code":"200","req.size.bytes":-1,"resp.size.bytes":0,"time.cost.milliseconds":2,"current.thread.name":"http-nio-8080-exec-2","baggage":""}

  3. {"time":"2018-11-09 12:15:22.439","local.app":"SOFATracerSpringMVC","traceId":"0a0fe8ec154173692243810359846","spanId":"0.1","request.url":"http://localhost:8080/springmvc","method":"GET","result.code":"200","req.size.bytes":-1,"resp.size.bytes":0,"time.cost.milliseconds":1,"current.thread.name":"http-nio-8080-exec-6","baggage":""}

  4. {"time":"2018-11-09 12:15:22.817","local.app":"SOFATracerSpringMVC","traceId":"0a0fe8ec154173692281510379846","spanId":"0.1","request.url":"http://localhost:8080/springmvc","method":"GET","result.code":"200","req.size.bytes":-1,"resp.size.bytes":0,"time.cost.milliseconds":2,"current.thread.name":"http-nio-8080-exec-8","baggage":""}

     

按 20% 進行取樣,測試結果僅供參考。

自定義取樣樣式

SOFATracer 提供基於使用者自定義擴充套件的取樣介面,取樣樣式需實現 com.alipay.common.tracer.core.samplers.Sampler 介面。當 com.alipay.sofa.tracer.samplerCustomRuleClassName = CustomOpenRulesSamplerRuler 時,使用者需實現 CustomOpenRulesSamplerRuler.sample 方法基於當前 SofaTracerSpan 引數取樣條件定義取樣計算規則。

透過 application.properties 增加取樣相關配置項支援自定義取樣樣式:

使用者自定義取樣規則類實現 com.alipay.common.tracer.core.samplers.Sampler 介面示例:

在 sample 方法中,使用者可以根據當前 SofaTracerSpan 提供的資訊來決定是否進行列印。此案例是透過判斷 isServer 來決定是否取樣,isServer=true 不取樣,否則取樣。 相關實驗結果,大家可以自行驗證下。

總結

本篇主要剖析 Dapper 論文采樣模型策略和 SOFATracer 取樣原始碼實現,詳細描述針對埋點資料如何制定取樣規則。按照 SOFATracer 基於固定取樣率的取樣樣式和基於使用者擴充套件實現的自定義取樣樣式選擇適合業務需求場景的取樣策略,更好地整合 SOFATracer 資料取樣版塊實現自定義取樣計算規則。透過此篇原始碼剖析希望幫助大家更好的理解 SOFATracer 鏈路跟蹤取樣模組的核心原理和具體實現。

文中出現的相關連結:

  • Drapper 論文原文地址:

    https://ai.google/research/pubs/pub36356

  • 時間複雜度為 O(N) 的蓄水池取樣演演算法 Reservoir Sampling :

    https://en.wikipedia.org/wiki/Reservoir_sampling

  • 隨機 BitSet 來源 StackOverflow:

    https://stackoverflow.com/questions/12817946/generate-a-random-bitset-with-n-1s

  • tracer-sample-with-springmvc:

    https://github.com/alipay/sofa-tracer/tree/master/tracer-samples/tracer-sample-with-springmvc

長按關註,獲取分散式架構乾貨

歡迎大家共同打造 SOFAStack https://github.com/alipay

    贊(0)

    分享創造快樂