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

螞蟻金服分佈式鏈路跟蹤組件採樣策略和原始碼 | 剖析

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)

    分享創造快樂