Apache Kylin Cube構建算法

逐層算法

在介紹快速Cube算法之前,我們先簡單回顧一下現有的算法,也稱之爲“逐層算法”(By Layer Cubing)。

我們知道,一個N維的完全Cube,是由:1個N維子立方體(Cuboid), N個(N-1)維Cuboid, N*(N-1)/2個(N-2)維Cuboid …, N個1維Cuboid, 1個0維Cuboid,總共2^N個子立方體組成的;在“逐層算法”中,按維度數逐漸減少來計算,每個層級的計算(除了第一層,它是從原始數據聚合而來),是基於它上一層級的結果來計算的。

舉例子來說,[Group by A, B]的結果,可以基於[Group by A, B, C]的結果,通過去掉C後聚合得來的;這樣可以減少重複計算;當 0維度Cuboid計算出來的時候,整個Cube的計算也就完成了。

圖1展示了用該算法計算一個四維Cube的流程。

圖1 逐層算法

此算法的Mapper和Reducer都比較簡單。Mapper以上一層Cuboid的結果(Key-Value對)作爲輸入。由於Key是由各維度值拼接在一起,從其中找出要聚合的維度,去掉它的值成新的Key,然後把新Key和Value輸出,進而Hadoop MapReduce對所有新Key進行排序、洗牌(shuffle)、再送到Reducer處;Reducer的輸入會是一組有相同Key的Value集合,對這些Value做聚合計算,再結合Key輸出就完成了一輪計算。

每一輪的計算都是一個MapReduce任務,且串行執行; 一個N維的Cube,至少需要N次MapReduce Job。

算法優點

  • 此算法充分利用了MapReduce的能力,處理了中間複雜的排序和洗牌工作,故而算法代碼清晰簡單,易於維護;
  • 受益於Hadoop的日趨成熟,此算法對集羣要求低,運行穩定;在內部維護Kylin的過程中,很少遇到在這幾步出錯的情況;即便是在Hadoop集羣比較繁忙的時候,任務也能完成。

算法缺點

  • 當Cube有比較多維度的時候,所需要的MapReduce任務也相應增加;由於Hadoop的任務調度需要耗費額外資源,特別是集羣較龐大的時候,反覆遞交任務造成的額外開銷會相當可觀;
  • 由於Mapper不做預聚合,此算法會對Hadoop MapReduce輸出較多數據; 雖然已經使用了Combiner來減少從Mapper端到Reducer端的數據傳輸,所有數據依然需要通過Hadoop MapReduce來排序和組合才能被聚合,無形之中增加了集羣的壓力;
  • 對HDFS的讀寫操作較多:由於每一層計算的輸出會用做下一層計算的輸入,這些Key-Value需要寫到HDFS上;當所有計算都完成後,Kylin還需要額外的一輪任務將這些文件轉成HBase的HFile格式,以導入到HBase中去;
  • 總體而言,該算法的效率較低,尤其是當Cube維度數較大的時候;時常有用戶問,是否能改進Cube算法,縮短時間。

快速Cube算法

快速Cube算法(Fast Cubing)是麒麟團隊對新算法的一個統稱,它還被稱作“逐段”(By Segment) 或“逐塊”(By Split) 算法。

該算法的主要思想是,對Mapper所分配的數據塊,將它計算成一個完整的小Cube 段(包含所有Cuboid);每個Mapper將計算完的Cube段輸出給Reducer做合併,生成大Cube,也就是最終結果;圖2解釋了此流程。

圖2逐塊Cube算法

Mapper的預聚合

與舊算法相比,快速算法主要有兩點不同:

  • Mapper會利用內存做預聚合,算出所有組合;Mapper輸出的每個Key都是不同的,這樣會減少輸出到Hadoop MapReduce的數據量,Combiner也不再需要;
  • 一輪MapReduce便會完成所有層次的計算,減少Hadoop任務的調配。

我們看一個例子:某個Cube有四個維度:A、B、C、D;每個Mapper分配到的數據塊有約一百萬條記錄;在這一百萬條記錄中,每個維度的基數(Cardinality)分別是Card(A), Card(B), Card(C), Card(D)。

當從原始數據計算四維Cuboid(ID: 1111)的時候:舊算法的Mapper會簡單地對每條記錄去除不相關的維度,然後輸出到Hadoop,所以輸出量依然是一百萬條;新算法的Mapper,由於做了聚合,它只輸出[count distinct A, B, C, D]條記錄到Hadoop,此數目肯定小於原始條數;在很多時候下,它會是原來的1/10甚至1/1000。

當從四維Cuboid 1111計算三維Cuboid如0111的時候,維度A會被聚合掉;假定A維度的值均勻分佈,那麼聚合後的記錄數會是四維Cuboid記錄數的1/ Card(A),;而舊算法的Mapper輸出數跟四維Cuboid記錄數相同。

可以看到,在Cuboid的推算過程中的每一步,新算法都會比舊算法產生更少數據;總的加起來,新算法中的Mapper對Hadoop的輸出,會比老算法少一個或幾個數量級,具體數字取決於用戶數據的特性;越少的數據,意味着越少的I/O和CPU,從而使得性能得以提升。

子立方體生成樹的遍歷

值得一提的還有一個改動,就是子立方體生成樹(Cuboid Spanning Tree)的遍歷次序;在舊算法中,Kylin按照層級,也就是廣度優先遍歷(Broad First Search)的次序計算出各個Cuboid;在快速Cube算法中,Mapper會按深度優先遍歷(Depth First Search)來計算各個Cuboid。深度優先遍歷是一個遞歸方法,將父Cuboid壓棧以計算子Cuboid,直到沒有子Cuboid需要計算時纔出棧並輸出給Hadoop;最多需要暫存N個Cuboid,N是Cube維度數。

採用DFS,是爲了兼顧CPU和內存:

  • 從父Cuboid計算子Cuboid,避免重複計算;
  • 只壓棧當前計算的Cuboid的父Cuboid,減少內存佔用。

圖3子立方體生成樹的遍歷

圖3是一個四維Cube的完整生成樹;按照DFS的次序,在0維Cuboid 輸出前的計算次序是 ABCD -> BCD -> CD -> D -> *, ABCD, BCD, CD和D需要被暫存;在*被輸出後,D可被輸出,內存得到釋放;在C被計算並輸出後,CD就可以被輸出; ABCD最後被輸出。

採用DFS,Mapper的輸出會是排序的(某些特殊情況除外):Cube行鍵(row key)是由[Cuboid ID + 維度值]組成;DFS訪問的結果,恰好是按照Cuboid ID從小到大輸出;而在同一個Cuboid內,維度值也是升序排序;所以總的輸出是排序的,請看如下示例。

0000 
0001[D0] 
0001[D1] 
.... 
0010[C0] 
0010[C1] 
.... 
0011[C0][D0] 
0011[C0][D1] 
.... 
.... 
1111[A0][B0][C0][D0] 
....

注: 這裏[D0]代表D維度的最小值,[D1]代表次小值,以此類推。

由於每個Mapper的輸出都是排序的,Hadoop對這些輸出進行歸併排序的效率也會更高。

OutOfMemory error

在新算法的開發和測試初期,我們發現Mapper常常會遇到OutOfMemory而異常終止;總結下來,以下情況往往會導致該異常:

a) Hadoop Mapper所分配的堆內存較小;­­­­­­­

b) Cube中使用了"Distinct count" (HyperLogLog會佔用較大內存);

c) Cube的維度較多,導致生成樹較深;

d) 分配到Mapper的數據塊過大;

簡單的增大Mapper的JVM heap size可以暫時解決該問題;但是不是每個用戶的Hadoop機器都有大內存;算法需要足夠的健壯性和適應性,否則用戶會很頭疼;我們花了不少努力來優化該算法,例如主動探測OOM的發生,將堆棧中的Cuboid緩存到本地磁盤等;這一系列優化在eBay內部測試的結果非常好,OOM的發生率大大降低,而性能沒有明顯的下降。

下面我們對快速Cube算法做一個總結。

算法優點

  • 比老算法性能更好;下圖是一個新老算法在兩個案例上的所耗時間對比(分鐘),能減少約30%到50%;

  • Mapper內的Cube計算邏輯可以被其它Cube引擎重用,例如流數據(Streaming)和Spark; 實際上Kylin已經在這麼做了。

算法缺點

  • 新算法略複雜,學習曲線更陡;
  • 雖然新算法會在內存不足時會把數據暫存到本地磁盤,要獲取最佳性能,最好給Mapper以足夠內存,用戶要在輸入數據塊大小、Mapper配置、Cube複雜度之間找到平衡,需具備更多知識和經驗。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章