預聚合是高性能分析中的常用技術,例如,每小時100億條的網站訪問數據可以通過對常用的查詢緯度進行聚合,被降低到1000萬條訪問統計,這樣就能降低1000倍的數據處理量,從而在查詢時大幅減少計算量,提升響應速度。更高層的聚合可以帶來進一步的性能提升,例如,在時間維按天聚合,或者通過站點而不是URL聚合。
本文,我們將介紹 spark-alchemy 這個開源庫中的 HyperLogLog 這一個高級功能,並且探討它是如何解決大數據中數據聚合的問題。首先,我們先討論一下這其中面臨的挑戰。
再聚合(Reaggregation)的挑戰
預聚合是數據分析領域的一個強大的技術手段,前提就是所要計算的指標是可重聚合的。聚合操作,顧名思義,是滿足結合律的,所以很容易引入再聚合操作,因爲聚合操作可以再被進一步聚合。Counts 可以在通過 SUM 再聚合,最小值可以通過 MIN 再聚合,最大值也可以通過 MAX 再聚合。而 distinct counts 是特例,無法做再聚合,例如,不同網站訪問者的 distinct count 的總和並不等於所有網站訪問者的 distinct count 值,原因很簡單,同一個用戶可能訪問了不同的網站,直接求和就存在了重複統計的問題。
Distinct count 的不可再聚合的特性造成了很大的影響,計算 distinct count 必須要訪問到最細粒度的數據,更進一步來說,就是計算 distinct count 的查詢必須讀取每一行數據。
當這個問題遇上大數據,就會產生新的挑戰:計算過程所需的內存和 distinct count 的結果數量是成正比的。近年來,諸如 Apache Spark 的大數據系統以及諸如 Amazon Redshift 的分析型數據庫都引入了 distinct count 的近似計算功能——基數估計(cardinality estimation),利用 HyperLogLog(HLL)概率數據結構來實現。在 Spark 中使用近似計算,只需要將 COUNT(DISTINCT x) 替換爲 approx_count_distinct(x [, rsd]),其中額外的參數 rsd 表示最大允許的偏差率,默認值爲 5%。Databricks 給出的 HLL 性能分析表明,只要最大偏差率大於等於 1%,Spark 的 distinct count 近似計算的運行速度比精確計算高2~8倍。不過,如果我們需要更小的偏差率,近似計算可能會比精確計算耗時更長。
2~8倍的性能提升是相當可觀的,不過它犧牲的精確性,大於等於 1% 的最大偏差率在某些場合可能是無法被接受的。另外,2~8倍的性能提升在預聚合所帶來的上千倍的性能提升面前也是微不足道的,那我們能做什麼?
HyperLogLog 算法回顧
答案其實就在 HyperLogLog 算法本身,Spark 通過 partition 分片執行 MapReduce 實現 HLL 算法的僞代碼如下所示:
-
Map (每個 partition)
- 初始化 HLL 數據結構,稱作 HLL sketch
- 將每個輸入添加到 sketch 中
- 發送 sketch
-
Reduce
- 聚合所有 sketch 到一個 aggregate sketch 中
-
Finalize
- 計算 aggregate sketch 中的 distinct count 近似值
值得注意的是,HLL sketch 是可再聚合的:在 reduce 過程合併之後的結果就是一個 HLL sketch。如果我們可以將 sketch 序列化成數據,那麼我們就可以在預聚合階段將其持久化,在後續計算 distinct count 近似值時,就能獲得上千倍的性能提升!
另外這個算法還能帶來另一個同樣重要的好處:我們不再限於性能問題向估算精度妥協(大於等於1%的估算偏差)。由於預聚合能夠帶來上千倍的性能提升,我們可以創建估算偏差非常低的 HLL sketch,因爲在上千倍的查詢性能提升面前,我們完全能夠接受預聚合階段2~5倍的計算耗時。這在大數據業務中基本相當於是免費的午餐:帶來巨大性能提升的同時,又不會對大部分業務端的用戶造成負面影響。
Spark-Alchemy 簡介:HLL Native 函數
由於 Spark 沒有提供相應功能,Swoop 開源了高性能的 HLL native 函數工具包,作爲 spark-alchemy 項目的一部分,具體使用示例可以參考 HLL docs。提供了大數據領域最爲齊全的 HyperLogLog 處理工具,超過了 BigQuery 的 HLL 支持。
下圖所示爲 spark-alchemy 處理 initial aggregation (通過 hll_init_agg
), reaggregation (通過 hll_merge
) 和 presentation (通過 hll_cardinality
)。
如果你想了解 HLL sketch 的內存使用量,可以遵循這樣一個準則,HLL cardinality estimation 精度每提升2倍, HLL sketch 所需內存提升4倍。大部分場景下,數據行數的較少所帶來的受益遠超過 HLL sketch 帶來的額外存儲。
error | sketch_size_in_bytes |
---|---|
0.005 | 43702 |
0.01 | 10933 |
0.02 | 2741 |
0.03 | 1377 |
0.04 | 693 |
0.05 | 353 |
0.06 | 353 |
0.07 | 181 |
0.08 | 181 |
0.09 | 181 |
0.1 | 96 |
HyperLogLog 互通性
通過近似計算 distinct count 代替精確計算,並且將 HLL sketch 保存成列式數據,最終的查詢階段可以不再需要處理每一行最細粒度的數據,但是仍舊有一個隱性的需求,那就是使用 HLL 數據的系統需要訪問所有最細粒度的數據,這是因爲目前還沒有工業標準來序列化 HLL 數據結構。大部分實現,例如 BigQuery,使用了不透明的二進制數據,也沒有相關文檔說明,這使得跨系統互通變得困難。這個互通性的問題極大增加了交互式分析系統的成本和複雜度。
交互式分析系統的一個關鍵要求是快速的查詢響應。而這並不是很多諸如 Spark 和 BigQuery 的大數據系統的設計核心,所以很多場景下,交互式分析查詢通過關係型或者 NoSQL 數據庫來實現。如果 HLL sketch 不能實現數據層面的互通性,那我們又將回到原點。
爲了解決這個問題,在 spark-alchemy 項目裏,使用了公開的 存儲標準,內置支持 Postgres 兼容的數據庫,以及 JavaScript。這樣使得 Spark 能夠成爲全局的數據預處理平臺,能夠滿足快速查詢響應的需求,例如 portal 和 dashboard 的場景。這樣的架構可以帶來巨大的受益:
- 99+%的數據僅通過 Spark 進行管理,沒有重複
- 在預聚合階段,99+%的數據通過 Spark 處理
- 交互式查詢響應時間大幅縮短,處理的數據量也大幅較少
總結
總結一下,本文闡述了預聚合這個常用技術手段如何通過 HyperLogLog 數據結構應用到 distinct count 操作,這不僅帶來了上千倍的性能提升,也能夠打通 Apache Spark、RDBM 甚至 JavaScript。雖然有些難以置信,但通過 HLL sketch 以及 Spark 強大的擴展能力,我們確確實實能夠得到這樣一份免費的午餐。
本文作者:開源大數據EMR
本文爲雲棲社區原創內容,未經允許不得轉載。