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

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

      △ 陶加濤git

Apache Kylin 做爲目前惟一一個同時支持精確與非精確去重查詢的 OLAP 引擎,很是好地覆蓋了大數據上的去重需求。本次分享講解了 Kylin 這兩種去重方式背後用到的算法,但願能讓你們從源頭上理解爲何 Kylin 的去重查詢有着如此優異的性能。這次分享的回顧將分爲兩期,本篇首先爲你們介紹精確去重算法 Bitmap 。github



 

     △ Meetup 現場視頻算法



首先,請你們思考一個問題:在大數據處理領域中,什麼環節是你最不但願見到的?以個人觀點來看,shuffle 是我最不肯意見到的環節,由於一旦出現了很是多的 shuffle,就會佔用大量的磁盤和網絡 IO,從而致使任務進行得很是緩慢。而今天咱們所討論的去重分析,就是一個會產生很是多 shuffle 的場景,先來看如下場景:apache



咱們有一張商品訪問表,表上有 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 來表示集合中的一個元素,比起原始元素,能夠節省很是多的存儲。dom

這就是最基礎的 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、回覆 電子書 獲取 本站全部可下載的電子書

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

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

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

四、分佈式原理:一致性哈希算法簡介

五、分佈式快照算法: Chandy-Lamport 算法

六、Kafka分區分配策略

七、分佈式原理:一文了解 Gossip 協議

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

九、HBase Rowkey 設計指南

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

十一、更多大數據文章歡迎訪問https://www.iteblog.com及本公衆號( iteblog_hadoop)十二、Flink中文文檔:http://flink.iteblog.com1三、Carbondata 中文文檔:http://carbondata.iteblog.com
相關文章
相關標籤/搜索