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


去重分析在企業日常分析中的使用頻率非常高,如何在大數據場景下快速地進行去重分析一直是一大難點。在近期的 Apache Kylin Meetup 北京站上,我們邀請到 Kyligence 大數據研發工程師陶加濤為大家揭開了大數據分析常用去重算法的神秘面紗。

      △ 陶加濤

Apache Kylin 作為目前唯一一個同時支持精確與非精確去重查詢的 OLAP 引擎,非常好地覆蓋了大數據上的去重需求。本次分享講解了 Kylin 這兩種去重方式背后用到的算法,希望能讓大家從源頭上理解為什么 Kylin 的去重查詢有着如此優異的性能。此次分享的回顧將分為兩期,本篇首先為大家介紹精確去重算法 Bitmap 。



 

     △ Meetup 現場視頻



首先,請大家思考一個問題:在大數據處理領域中,什么環節是你最不希望見到的?以我的觀點來看,shuffle 是我最不願意見到的環節,因為一旦出現了非常多的 shuffle,就會占用大量的磁盤和網絡 IO,從而導致任務進行得非常緩慢。而今天我們所討論的去重分析,就是一個會產生非常多 shuffle 的場景,先來看以下場景:



我們有一張商品訪問表,表上有 item 和 user_id 兩個列,我們希望求商品的 UV,這是去重非常典型的一個場景。我們的數據是存儲在分布式平台上的,分別在數據節點 1 和 2 上。

我們從物理執行層面上想一下這句 SQL 背后會發生什么故事:首先分布式計算框架啟動任務, 從兩個節點上去拿數據, 因為 SQL group by 了 item 列, 所以需要以 item 為 key 對兩個表中的原始數據進行一次 shuffle。我們來看看需要 shuffle 哪些數據:因為 select/group by了 item,所以 item 需要 shuffle 。但是,user_id  我們只需要它的一個統計值,能不能不 shuffle 整個 user_id 的原始值呢?

如果只是簡單的求 count 的話, 每個數據節點分別求出對應 item 的 user_id 的 count, 然后只要 shuffle 這個 count 就行了,因為count 只是一個數字, 所以 shuffle 的量非常小。但是由於分析的指標是 count distinct,我們不能簡單相加兩個節點user_id 的 count distinct 值,我們只有得到一個 key 對應的所有 user_id 才能統計出正確的 count distinct值,而這些值原先可能分布在不同的節點上,所以我們只能通過 shuffle 把這些值收集到同一個節點上再做去重。而當 user_id 這一列的數據量非常大的時候,需要 shuffle 的數據量也會非常大。我們其實最后只需要一個 count 值,那么有辦法可以不 shuffle 整個列的原始值嗎?我下面要介紹的兩種算法就提供了這樣的一種思路,使用更少的信息位,同樣能夠求出該列不重復元素的個數(基數)。

精確算法: Bitmap

第一種要介紹的算法是一種精確的去重算法,主要利用了 Bitmap 的原理。Bitmap 也稱之為 Bitset,它本質上是定義了一個很大的 bit 數組,每個元素對應到 bit 數組的其中一位。例如有一個集合[2,3,5,8]對應的 Bitmap 數組是[001101001],集合中的 2 對應到數組 index 為 2 的位置,3 對應到 index 為 3 的位置,下同,得到的這樣一個數組,我們就稱之為 Bitmap。很直觀的,數組中 1 的數量就是集合的基數。追本溯源,我們的目的是用更小的存儲去表示更多的信息,而在計算機最小的信息單位是 bit,如果能夠用一個 bit 來表示集合中的一個元素,比起原始元素,可以節省非常多的存儲。

這就是最基礎的 Bitmap,我們可以把 Bitmap 想象成一個容器,我們知道一個 Integer 是32位的,如果一個 Bitmap 可以存放最多 Integer.MAX_VALUE 個值,那么這個 Bitmap 最少需要 32 的長度。一個 32 位長度的 Bitmap 占用的空間是512 M (2^32/8/1024/1024),這種 Bitmap 存在着非常明顯的問題:這種 Bitmap 中不論只有 1 個元素或者有 40 億個元素,它都需要占據 512 M 的空間。回到剛才求 UV 的場景,不是每一個商品都會有那么多的訪問,一些爆款可能會有上億的訪問,但是一些比較冷門的商品可能只有幾個用戶瀏覽,如果都用這種 Bitmap,它們占用的空間都是一樣大的,這顯然是不可接受的。

升級版 Bitmap: Roaring Bitmap

對於上節說的問題,有一種設計的非常的精巧 Bitmap,叫做 Roaring Bitmap,能夠很好地解決上面說的這個問題。我們還是以存放 Integer 值的 Bitmap 來舉例,Roaring Bitmap 把一個 32 位的 Integer 划分為高 16 位和低 16 位,取高 16 位找到該條數據所對應的 key,每個 key 都有自己的一個 Container。我們把剩余的低 16 位放入該 Container 中。依據不同的場景,有 3 種不同的 Container,分別是 Array Container、Bitmap Container 和 Run Container,下文將一一介紹。



首先第一種,是 Roaring Bitmap 初始化時默認的 Container,叫做 Array Container。Array Container 適合存放稀疏的數據,Array Container 內部的數據結構是一個 short array,這個 array 是有序的,方便查找。數組初始容量為 4,數組最大容量為 4096。超過最大容量 4096 時,會轉換為 Bitmap Container。這邊舉例來說明數據放入一個 Array Container 的過程:有 0xFFFF0000 和 0xFFFF0001 兩個數需要放到 Bitmap 中, 它們的前 16 位都是 FFFF,所以他們是同一個 key,它們的后 16 位存放在同一個 Container 中; 它們的后 16 位分別是 0 和 1, 在 Array Container 的數組中分別保存 0 和 1 就可以了,相較於原始的 Bitmap 需要占用 512M 內存來存儲這兩個數,這種存放實際只占用了 2+4=6 個字節(key 占 2 Bytes,兩個 value 占 4 Bytes,不考慮數組的初始容量)。

第二種 Container 是 Bitmap Container,其原理就是上文說的 Bitmap。它的數據結構是一個 long 的數組,數組容量固定為 1024,和上文的 Array Container 不同,Array Container 是一個動態擴容的數組。這邊推導下 1024 這個值:由於每個 Container 還需處理剩余的后 16 位數據,使用 Bitmap 來存儲需要 8192 Bytes(2^16/8), 而一個 long 值占 8 個 Bytes,所以一共需要 1024(8192/8)個 long 值。所以一個 Bitmap container 固定占用內存 8 KB(1024 * 8 Byte)。當 Array Container 中元素到 4096 個時,也恰好占用 8 k(4096*2Bytes)的空間,正好等於 Bitmap 所占用的 8 KB。而當你存放的元素個數超過 4096 的時候,Array Container 的大小占用還是會線性的增長,但是 Bitmap Container 的內存空間並不會增長,始終還是占用 8 K,所以當 Array Container 超過最大容量(DEFAULT_MAX_SIZE)會轉換為 Bitmap Container。

我們自己在 Kylin 中實踐使用 Roaring Bitmap 時,我們發現 Array Container 隨着數據量的增加會不停地 resize 自己的數組,而 Java 數組的 resize 其實非常消耗性能,因為它會不停地申請新的內存,同時老的內存在復制完成前也不會釋放,導致內存占用變高,所以我們建議把 DEFAULT_MAX_SIZE 調得低一點,調成 1024 或者 2048,減少 Array Container 后期 reszie 數組的次數和開銷。



最后一種 Container 叫做Run Container,這種 Container 適用於存放連續的數據。比如說 1 到 100,一共 100 個數,這種類型的數據稱為連續的數據。這邊的Run指的是Run Length Encoding(RLE),它對連續數據有比較好的壓縮效果。原理是對於連續出現的數字, 只記錄初始數字和后續數量。例如: 對於 [11, 12, 13, 14, 15, 21, 22],會被記錄為 11, 4, 21, 1。很顯然,該 Container 的存儲占用與數據的分布緊密相關。最好情況是如果數據是連續分布的,就算是存放 65536 個元素,也只會占用 2 個 short。而最壞的情況就是當數據全部不連續的時候,會占用 128 KB 內存。

總結:用一張圖來總結3種 Container 所占的存儲空間,可以看到元素個數達到 4096 之前,選用 Array Container 的收益是最好的,當元素個數超過了 4096 時,Array Container 所占用的空間還是線性的增長,而 Bitmap Container 的存儲占用則與數據量無關,這個時候 Bitmap Container 的收益就會更好。而 Run Container 占用的存儲大小完全看數據的連續性, 因此只能畫出一個上下限范圍 [4 Bytes, 128 KB]。



在 Kylin 中的應用

我們再來看一下Bitmap 在 Kylin 中的應用,Kylin 中編輯 measure 的時候,可以選擇 Count Distinct,且Return Type 選為 Precisely,點保存就可以了。但是事情沒有那么簡單,剛才上文在講 Bitmap 時,一直都有一個前提,放入的值都是數值類型,但是如果不是數值類型的值,它們不能夠直接放入 Bitmap,這時需要構建一個全區字典,做一個值到數值的映射,然后再放入 Bitmap 中。



在 Kylin 中構建全局字典,當列的基數非常高的時候,全局字典會成為一個性能的瓶頸。針對這種情況,社區也一直在努力做優化,這邊簡單介紹幾種優化的策略,更詳細的優化策略可以見文末的參考鏈接。



1)當一個列的值完全被另外一個列包含,而另一個列有全局字典,可以復用另一個列的全局字典。



2)當精確去重指標不需要跨 Segment 聚合的時候,可以使用這個列的 Segment 字典代替(這個列需要字典編碼)。在 Kylin 中,Segment 就相當於時間分片的概念。當不會發生跨 Segments 的分析時,這個列的 Segment 字典就可以代替這個全局字典。



3)如果你的 cube 包含很多的精確去重指標,可以考慮將這些指標放到不同的列族上。不止是精確去重,像一些復雜 measure,我們都建議使用多個列族去存儲,可以提升查詢的性能。

另外,作者的個人 Github 地址為: https://github.com/aaaaaaron,點擊下面閱讀原文即可進入。

參考

1) 康凱森,《Apache Kylin 精確去重和全局字典權威指南》,2018-01-07,https://blog.bcmeng.com/post/kylin-distinct-count-global-dict.html

2) Hexiaoqiao,《Apache Kylin精確計數與全局字典揭秘》,2016-11-27,https://hexiaoqiao.github.io/blog/2016/11/27/exact-count-and-global-dictionary-of-apache-kylin/

 

猜你喜歡

歡迎關注本公眾號:iteblog_hadoop:

回復 spark_summit_201806 下載 Spark Summit North America 201806 全部PPT

回復 spark_summit_eu_2018 下載 Spark+AI Summit europe 2018 全部PPT

回復 HBase_book 下載 2018HBase技術總結 專刊

回復 all 獲取本公眾號所有資料

0、回復 電子書 獲取 本站所有可下載的電子書

1、Apache Spark 2.4 回顧以及 3.0 展望

2、重磅 | Apache Spark 社區期待的 Delta Lake 開源了

3、Apache Spark 3.0 將內置支持 GPU 調度

4、分布式原理:一致性哈希算法簡介

5、分布式快照算法: Chandy-Lamport 算法

6、Kafka分區分配策略

7、分布式原理:一文了解 Gossip 協議

8、列式存儲和行式存儲它們真正的區別是什么

9、HBase Rowkey 設計指南

10、HBase 入門之數據刷寫詳細說明

11、更多大數據文章歡迎訪問https://www.iteblog.com及本公眾號( iteblog_hadoop)12、Flink中文文檔:http://flink.iteblog.com13、Carbondata 中文文檔:http://carbondata.iteblog.com


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM