大數據分析常用去重算法分析『HyperLogLog 篇』

上篇文章中,Kyligence 大數據工程師陶加濤爲大家介紹了利用 Roaring Bitmap 來進行精確去重。雖然這種算法能大大地減少存儲開銷,但是隨着數據量的增大,它依然面臨着存儲上的壓力。在本篇文章中將要介紹的 HyperLogLog(下稱 HLL)是一種非精確的去重算法,它的特點是具有非常優異的空間複雜度(幾乎可以達到常數級別)。

 

HLL 算法需要完整遍歷所有元素一次,而非多次或採樣;該算法只能計算集合中有多少個不重複的元素,不能給出每個元素的出現次數或是判斷一個元素是否之前出現過;多個使用 HLL 統計出的基數值可以融合。

HLL 算法有着非常優異的空間複雜度,可以看到它的空間佔用隨着基數值的增長並沒有變化。HLL 後面不同的數字代表着不同的精度,數字越大,精度越高,佔用的空間也越大,可以認爲 HLL 的空間佔用只和精度成正相關。

HLL 算法原理感性認知

HLL 算法的原理會涉及到比較多的數學知識,這邊對這些數學原理和證明不會展開。舉一個生活中的例子來幫助大家理解 HLL 算法的原理:比如你在進行一個實驗,內容是不停地拋硬幣,記錄你連續拋到正面的次數(這是數學中的伯努利過程,感興趣同學可以自行研究下);如果你最多的連拋正面記錄是 3 次,那可以想象你並沒有做這個實驗太多次,如果你最長的連拋正面記錄是 20 次,那你可能進行了這個實驗上千次。

一種理論上存在的情況是,你非常幸運,第一次進行這個實驗就連拋了 20 次正面,我們也會認爲你進行了很多次這個實驗纔得到了這個記錄,這就會導致錯誤的預估;改進的方式是請 10 位同學進行這項實驗,這樣就可以觀察到更多的樣本數據,降低出現上述情況的概率。這就是 HLL 算法的核心思想。

HLL 算法具體實現

HLL 會通過一個 hash 函數來求出集合中所有元素的 hash 值(二進制表示的 hash 值,就可以理解爲一串拋硬幣正反面結果的序列),得到一個 hash 值的集合,然後找出該 hash 值集合中,第一個 1 出現的最晚的位置。例如有集合爲 [010, 100, 001], 集合中元素的第一個 1 出現的位置分別爲 2, 1, 3,可以得到裏面最大的值爲 3,故該集合中第一個 1 出現的最晚的位置爲 3。因爲每個位置上出現 1 的概率都是 1/2,所以我們可以做一個簡單的推斷,該集合中有 8 個不重複的元素。

可以看到這種簡單的推斷計算出來集合的基數值是有較大的偏差的,那如何來減少偏差呢?正如我上面的例子裏說的一樣,HLL 通過多次的進行試驗來減少誤差。那它是如何進行多次的實驗的呢?這裏 HLL 使用了分桶的思想,上文中我們一直有提到一個精度的概念,比如說 HLL(10),這個 10 代表的就是取該元素對應 Hash 值二進制的後 10 位,計算出記錄對應的桶,桶中會記錄一個數字,代表對應到該桶的 hash 值的第一個 1 出現的最晚的位置。如上圖,該 hash 值的後 10 位的 hash 值是 0000001001,轉成 10 進制是 9,對應第 9 號桶,而該 hash 值第一個 1 出現的位置是第 6 位,比原先 9 號桶中的數字大,故把 9 號桶中的數字更新爲 6。可以看到桶的個數越多,HLL 算法的精度就越高,HLL(10) 有 1024(2^10) 個桶,HLL(16) 有 65536(2^16) 個桶。同樣的,桶的個數越多,所佔用的空間也會越大。

剛纔的例子我們省略了一些細節,爲了讓大家不至於迷失在細節中而忽視了重點,真實的 HLL 算法的完整描述見上圖,這邊的重點是計算桶中平均數時使用調和平均數。調和平均數的優點是可以過濾掉不健康的統計值,使用算術平均值容易受到極值的影響(想想你和馬雲的平均工資),而調和平均數的結果會傾向於集合中比較小的元素。HLL 論文中還有更多的細節和參數,這邊就不一一細舉,感興趣的同學可以自己閱讀下論文。

HLL 評估

HLL 的誤差分佈服從正態分佈,它的空間複雜度: O(m log2log2N), N 爲基數, m 爲桶個數。這邊給大家推導一下它的空間複雜度,我有 264 個的不重複元素 (Long. MAX_VALUE),表達爲二進制一個數是 64 位,這是第一重 log2, 那麼第一個 1 最晚可能出現在第 64 位。64 需要 6 個 bit (2^6=64) 就可以存儲,這是第二重 log2。如果精度爲 10,則會有 1024 個桶,所以最外面還要乘以桶的個數。由於需要完整的遍歷元素一遍,所以它的時間複雜度是一個線性的時間複雜度。

在 Kylin 中的應用

在 Kylin 中使用 HLL 非常簡單,在編輯度量的頁面選擇 COUNT DISTINCT,Return Type 選爲非 Precisely 的其他選項,大家根據自己的需求選擇不同的精度就可以愉快地使用了。

總結

我們回到最開始的去重場景,看看使用了 Bitmap 和 HLL 會給我們帶來什麼增益:無優化 case 下,每個 item 對應的 user_id 就可以看成存儲原始值的一個集合;在使用 Bitmap 優化的 case 下,每個 item 對應的 user_id 就可以看成一個 Bitmap 實例,同理 HLL 就是一個 HLL 的實例,Bitmap/HLL 實例佔用的空間都會比直接存儲原始值的集合要小,這就達到了我們開始提的減少 shuffle 數據量的需求。

Q&A

Q1:您好,問一下關於精確去重的問題, 我選擇了非精確去重,最後的誤差率有時候會比界面上提示的值要高一些,這是爲什麼?

A1:首先 HLL 的誤差分佈服從正態分佈,也就是說是在 99% 的情況下是這個誤差,同時 HLL 對於基數比較低的情況,誤差會偏高。如果你的基數比較低的話,我推薦使用精確去重。

Q2:我想要了解一下 Bitmap 在 Kylin 中,它最終落盤在 HBase 裏面是什麼樣子的?

A2:在 HBase 中存儲的當然都是 Bytes。這個問題其實就是 Bitmap 的序列化的形式,Roaring Bitmap 提供了序列化和反序列化的實現,你也可以寫自己的序列化 / 反序列化的實現。

Q3:Roaring Bitmap 裏這些 container 要我們自己手動的指定嗎?

A3:不需要,Roaring Bitmap 會自動選擇使用哪個 Container。

 

作者簡介:陶加濤 (wechatID:245915794),Kyligence 大數據研發工程師,主要負責 Kyligence Enterprise 存儲與查詢計算部分。GitHub ID: https://github.com/aaaaaaron。

 

5月25日,Kylin Meetup 首次空降成都~戳此處報名!

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