ElasticSearch 如何使用 TDigest 算法計算億級數據的百分位數?

點擊上方"程序員歷小冰",選擇“置頂或者星標”

   你的關注意義重大!

大家好,我是歷小冰。ElasticSearch 作爲一個分佈式的開源搜索和分析引擎,不僅能夠進行全文匹配搜索,還可以進行聚合分析。

今天,我們就來了解一下其聚合分析中較爲常見的 percentiles 百分位數分析。n 個數據按數值大小排列,處於 p% 位置的值稱第 p 百分位數。

比如說,ElasticSearch 記錄了每次網站請求訪問的耗時,需要統計其 TP99,也就是整體請求中的 99% 的請求的最長耗時。

近似算法

當數據量較小或者數據集中存儲在同一位置時,進行類似 TP99 這樣的百分位數分析就很容易。

但當數據量不斷增長時,對於數據進行聚合分析就需要在數據量,精確度和實時性三個方面進行取捨,只能滿足其中兩項。

如上圖所示,我們一共有三種選擇方案:

  • 有限數據計算:選擇了精確度高和實時性,必然不能處理較大量級的數據,比如 MySQL 對單機數據進行統計分析;

  • 離線計算:選擇了大數據量和精確度高,導致實時性較差,比如 Hadoop 可以在 PB 級別數據上提供精確分析,但是可能要很長時間;

  • 近似計算:選擇了大數據量和實時性,但會損失一定的精確度,比如0.5%,但提供相對準確的分析結果。


Elasticsearch 目前支持兩種近似算法,分別是 cardinality 和 percentiles。

cardinality 用於計算字段的基數,即該字段的 distinct 或者 unique 值的數量。cardinality 基於 HyperLogLog(HLL)算法實現。

HLL 會先對數據進行哈希運算,然後根據哈希運算的結果中的位數做概率估算從而得到基數。有關 HLL 算法的細節可以閱讀《Redis HyperLogLog 詳解》一文。

而 percentiles 則是本文的重點。

百分位數

ElasticSearch 可以使用 percentiles 來分析指定字段的百分位數,具體請求如下所示,分析 logs 索引下的 latency 字段的百分位數,也就是計算網站請求的延遲百分位數。

percentiles 默認情況下會返回一組預設的百分位數值,分別是 [1, 5, 25, 50, 75, 95, 99] 。它們表示了人們感興趣的常用百分位數值,極端的百分位數在範圍的兩邊,其他的一些處於中部。

具體的返回值如下圖所示,我們可以看到最小延時在 75ms 左右,而最大延時差不多有 600ms。與之形成對比的是,平均延時在 200ms 左右。

和前文的 cardinality 基數一樣,計算百分位數需要一個近似算法。

對於少量數據,在內存中維護一個所有值的有序列表, 就可以計算各類百分位數,但是當有幾十億數據分佈在幾十個節點時,這類算法是不現實的。

因此,percentiles 使用 TDigest 算法,它是一種近似算法,對不同百分位數的計算精確度不同,較爲極端的百分位數範圍更加準確,比如說 1% 或 99% 的百分位要比 50% 的百分位要準確。這是一個好的特性,因爲多數人只關心極端的百分位。

TDigest 算法

TDigest 是一個簡單,快速,精確度高,可並行化的近似百分位算法,被 ElastichSearch、Spark 和 Kylin 等系統使用。TDigest 主要有兩種實現算法,一種是 buffer-and-merge 算法,一種是 AV L樹的聚類算法。

TDigest 使用的思想是近似算法常用的 Sketch,也就是素描,用一部分數據來刻畫整體數據集的特徵,就像我們日常的素描畫一樣,雖然和實物有差距,但是卻看着和實物很像,能夠展現實物的特徵。

下面,我們來介紹一下 TDigest 的原理。比如有 500 個 -30 ~ 30 間的數字,我們可以使用概率密度函數(PDF)來表示這一數據集。

該函數上的某一點的 y 值就是其 x 值在整體數據集中的出現概率,整個函數的面積相加就正好爲 1 ,可以說它刻畫了數據在數據集中的分佈態勢(大家較爲熟悉的正太分佈示意圖展示的就是該函數)。

有了數據集對應的 PDF 函數,數據集的百分位數也能用 PDF 函數的面積表示。如下圖所示,75% 百分位數就是面積佔了 75% 時對應的 x 座標。

我們知道,PDF 函數曲線中的點都對應着數據集中的數據,當數據量較少時,我們可以使用數據集的所有點來計算該函數,但是當數據量較大時,我們只有通過少量數據來代替數據集的所有數據。

這裏,我們需要將數據集進行分組,相鄰的數據分爲一組,用 平均數(Mean)和 個數(Weight)來代替這一組數。這兩個數合稱爲 Centroid(質心數),然後用這個質心數來計算 PDF,這就是 TDigest 算法的核心思想。

如上圖所示,質心數的平均值作爲 x 值,個數作爲 y 值,可以通過這組質心數大致繪製出這個數據集的 PDF 函數。

對應的,計算百分位數也只需要從這些質心數中找到對應的位置的質心數,它的平均值就是百分位數值。


很明顯,質心數的個數值越大,表達它代表的數據越多,丟失的信息越大,也就越不精準。如上圖所示,太大的質心數丟失精準度太多,太小的質心數則有消耗內存等資源較大,達不到近似算法實時性高的效果。

所以,TDigest 在壓縮比率(壓縮比率越大,質心數代表的數據就要越多)的基礎上,按照百分位數來控制各個質心數代表的數據的多少,在兩側的質心數較小,精準度更高,而在中間的質心數則較大,以此達到前文所說的 1% 或 99% 的百分位要比 50% 的百分位要準確的效果。

源碼分析

ElasticSearch 直接使用了 TDigest 的開源實現 t-digest,其 github 地址爲 https://github.com/tdunning/t-digest,我們可以在 ElastichSearch 的 TDigestState 類看到其對 t-digest 實現的封裝。

t-digest 提供了兩種 TDigest 算法的實現:

  • MergingDigest 對應上文所說的 buffer-and-merge 算法

  • AVLGroupTree對應 AVL 樹的聚類算法。

MergingDigest用於數據集已經排序的場景,可以直接根據壓縮比率計算質心數,而 AVLGroupTree 則需要使用 AVL 樹來自信對數據根據其”接近程度“進行判斷,然後計算質心數。

MergingDigest的實現較爲簡單,顧名思義,其算法名稱叫做 buffer-and-merge,所以實現上使用 tempWeight 和  tempMean 兩個數組來代表質心數數組,將數據和保存的質心數進行 merge,然後如果超出 weight 上限,則創建新的質心數,否則修改當前質心數的平均值和個數。

AVLGroupTreeMergingDigest 相比,多了一步通過 AVL 二叉平衡樹搜索數據最靠近質心數的步驟,找到最靠近的質心數後,也是將二者進行 merge,然後判斷是否超過 weight 上線,進行修改或者創建操作。

下面,我們直接來看 AVLGroupTree 的 add 方法。

當 ElasticSearch 處理一個數據集時,就是不斷將數據集中的數據通過調用 add 函數加入到質心數中,然後統計完畢後,調用其 quantile 來計算百分位數。

後記

歡迎大家繼續關注程序員歷小冰,後續會繼續爲大家帶來有關數據存儲,數據分析,分佈式相關的文章。下一篇文章我們回來學習一下 ElasticSearch 的其他聚合分析操作的實現原理。

-關注我

推薦閱讀

TCP/IP的底層隊列

基於Redis和Lua的分佈式限流

AbstractQueuedSynchronizer超詳細原理解析

參考

  • https://blog.bcmeng.com/post/tdigest.html

  • https://blog.bcmeng.com/pdf/TDigest.pdf

  • https://github.com/tdunning/t-digest

  • https://op8867555.github.io/posts/2018-04-09-tdigest.html




本文分享自微信公衆號 - 程序員歷小冰(gh_a1d0b50d8f0a)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章