第3章
Cube優化
Apache Kylin的核心思想是根據使用者的資料模型和查詢樣式對資料進行預計算,并在查詢時直接利用預計算結果傳回查詢結果。
相比普通的大規模并行處了解決方案,Kylin具有響應時間快、查詢時資源需求小、吞吐量大等優點。使用者的資料模型包括次元、度量、分區列等基本資訊,也包括使用者通過Cube優化工具賦予其的額外的模型資訊。
例如,層級(Hierarchy)是用來描述若幹個次元之間存在層級關系的一種優化工具,提供層級資訊有助于預計算跳過多餘的步驟,減少預計算的工作量,最終減少存儲引擎所需要存儲的Cube資料的大小。
資料模型是資料固有的屬性,除此之外,查詢的樣式如果相對固定,有助于Cube優化。例如,如果知道用戶端的查詢總是會帶有某個次元上的過濾(Filter)條件,或者總是會按照這個次元進行聚合(Group By),那麼所有的不帶這個次元的場景下的預計算都可以跳過,因為即使為這些場景進行了預計算,這些預計算結果也不會被用到。
總的來說,在建構Cube之前,Cube的優化手段提供了更多與資料模型或查詢樣式相關的資訊,用于指導建構出體積更小、查詢速度更快的Cube。可以看到Cube的優化目标始終有兩個大方向:空間優化和查詢時間優化。
3.1 Cuboid剪枝優化
3.1.1 次元的組合
由之前的章節可以知道,在沒有采取任何優化措施的情況下,Kylin會對每一種次元的組合進行聚合預計算,次元的一種排列組合的預計算結果稱為一個Cuboid。如果有4個次元,結合簡單的數學知識可知,總共會有24=16種次元組合,即最終會有24=16個Cuboid需要計算,如圖3-1所示。其中,最底端的包含所有次元的Cuboid稱為Base Cuboid,它是生成其他Cuboid的基礎。

在現實應用中,使用者的次元數量一般遠遠大于4個。假設使用者有10個次元,那麼沒做任何優化的Cube總共會存在210=1024個Cuboid,而如果使用者有20個次元,那麼Cube中總共會存在220=1048576個Cuboid!雖然每個Cuboid的大小存在很大差異,但是僅Cuboid的數量就足以讓人意識到這樣的Cube對建構引擎、存儲引擎來說會形成巨大的壓力。是以,在建構次元數量較多的Cube時,尤其要注意進行Cube的剪枝優化。
3.1.2 檢查Cuboid數量
Apache Kylin提供了一種簡單的工具供使用者檢查Cube中哪些Cuboid最終被預計算了,将其稱為被物化(materialized)的Cuboid。同時,這種工具還能給出每個Cuboid所占空間的估計值。該工具需要在Cube建構任務對資料進行一定的處理之後才能估算Cuboid的大小,具體來說,就是在建構任務完成“Save Cuboid Statistics”這一步驟後才可以使用該工具。
由于同一個Cube的不同Segment之間僅是輸入資料不同,模型資訊和優化政策都是共享的,是以不同的Segment中被物化的Cuboid是相同的。是以,隻要Cube中至少有一個Segment完成了“Save Cuboid Statistics”這一步驟的建構,那麼就能使用如下的指令行工具去檢查這個Cube中的Cuboid的物化狀态:
bin/kylin.sh org.apache.kylin.engine.mr.common.CubeStatsReader CUBE_NAME
CUBE_NAME 想要檢視的Cube的名稱
該指令的輸出如圖3-2所示。
在該指令的輸出中,會依次列印出每個Segment的分析結果,不同Segment的分析結果基本趨同。在上面的例子中Cube隻有一個Segment,是以隻有一份分析結果。對于該結果,自上而下來看,首先能看到Segment的一些整體資訊,如估計Cuboid大小的精度(hll precision)、Cuboid的總數、Segment的總行數估計、Segment的大小估計等。
Segment的大小估算是建構引擎自身用來指導後續子步驟的,如決定mapper和 reducer數量、資料分片數量等的依據,雖然有的時候對Cuboid的大小的估計存在誤差(因為存儲引擎對最後的Cube資料進行了編碼或壓縮,是以無法精确預估資料大小),但是整體來說,對于不同Cuboid的大小估計可以給出一個比較直覺的判斷。由于沒有編碼或壓縮時的不确定性因素,是以Segment中的行數估計會比大小估計來得更加精确一些。
在分析結果的下半部分可以看到,所有的Cuboid及其分析結果以樹狀的形式列印了出來。在這棵樹中,每個節點代表一個Cuboid,每個Cuboid的ID都由一連串1或0的數字組成,數字串的長度等于有效次元的數量,從左到右的每個數字依次代表Cube的Rowkeys設定中的各個次元。如果數字為0,則代表這個Cuboid中不存在相應的次元,如果數字為1,則代表這個Cuboid中存在相應的次元。
除了最頂端的Cuboid之外,每個Cuboid都有一個父Cuboid,且都比父Cuboid少了一個“1”。其意義是這個Cuboid是由它的父節點減少一個次元聚合得來的(上卷,即roll up操作)。最頂端的Cuboid稱為Base Cuboid,它直接由源資料計算而來。Base Cuboid中包含了所有的次元,是以它的數字串中所有的數字均為1。
每行Cuboid的輸出除了0和1的數字串以外,後面還有每個Cuboid的具體資訊,包括該Cuboid行數的估計值、該Cuboid大小的估計值,以及該Cuboid的行數與其父節點的對比(Shrink)。所有的Cuboid的行數的估計值之和應該等于Segment的行數估計值。同理,所有的Cuboid的大小估計值之和等于該Segment的大小估計值。
每個Cuboid都是在它的父節點的基礎上進一步聚合産生的,是以理論上來說每個Cuboid無論是行數還是大小都應該小于它的父Cuboid。但是,由于這些數值都是估計值,是以偶爾能夠看到有些Cuboid的行數反而還超過其父節點、Shrink值大于100%的情況。在這棵“樹”中,可以觀察每個節點的Shrink值,如果該值接近100%,說明這個Cuboid雖然比它的父Cuboid少了一個次元,但是并沒有比它的父Cuboid少很多行資料。換言之,即使沒有這個Cuboid,在查詢時使用它的父Cuboid,也不會花費太大的代價。
關于這方面的詳細内容将在後續3.1.4節中詳細展開。
3.1.3 檢查Cube大小
還有一種更為簡單的方法可以幫助我們判斷Cube是否已經足夠優化。在Web GUI的“Model”頁面中選擇一個READY狀态的Cube,當把光标移到該Cube的“Cube Size”列時,Web GUI會提示Cube的源資料大小,以及目前Cube的大小與源資料大小的比例,稱之為膨脹率(Expansion Rate),如圖3-3所示。
一般來說,Cube的膨脹率應該為0%~1000%,如果一個Cube的膨脹率超過1000%,Cube管理者應當開始挖掘其中的原因。通常,膨脹率高有以下幾個方面的原因:
- Cube中的次元數量較多,且沒有進行很好的Cuboid剪枝優化,導緻Cuboid數量極多;
- Cube中存在較高基數的次元,導緻包含這類次元的每一個Cuboid占用的空間都很大,這些Cuboid累積造成整體Cube體積過大;
-
存在比較占用空間的度量,如Count Distinct這樣的度量需要在Cuboid的每一行中都儲存一個較大的寄存器,最壞的情況會導緻Cuboid中每一行都有數十千位元組,進而造成整個Cube的體積過大;
……
是以,遇到Cube的膨脹率居高不下的情況,管理者需要結合實際資料進行分析,可靈活地運用本章接下來介紹的優化方法對Cube進行優化。
3.1.4 空間與時間的平衡
理論上所有能用Cuboid處理的查詢請求,都可以使用Base Cuboid來處理,就好像所有能用Base Cuboid處理的查詢請求都能夠通過直接讀取源資料的方式來處理一樣。但是Kylin之是以在Cube中物化這麼多的Cuboid,就是因為不同的Cuboid有各自擅長的查詢場景。
面對一個特定的查詢,使用精确比對的Cuboid就好像是走了一條捷徑,能幫助Kylin最快地傳回查詢結果,因為這個精确比對的Cuboid已經為此查詢做了最大程度的預先聚合,查詢引擎隻需要做很少的運作時聚合就能傳回結果。每個Cuboid在技術上代表着一種次元的排列組合,在業務上代表着一種查詢的樣式;為每種查詢樣式都做好精确比對是理想狀态,但那會導緻很高的膨脹率,進而導緻很長的建構時間。是以在實際的Cube設計中,我們會考慮犧牲一部分查詢樣式的精确比對,讓它們使用不是完全精确比對的Cuboid,在查詢進行時再進行後聚合。這個不精确比對的Cuboid可能是3.1.2節中提到的Cuboid的父Cuboid,甚至如果它的父Cuboid也沒有被物化,Kylin可能會一路追溯到使用Base Cuboid來回答查詢請求。
使用不精确比對的Cuboid比起使用精确比對的Cuboid需要做更多查詢時的後聚合計算,但是如果Cube優化得當,查詢時的後聚合計算的開銷也沒有想象中的那麼恐怖。以3.1.2節中Shrink值接近100%的Cuboid為例,假設排除了這樣的Cuboid,那麼隻要它的父Cuboid被物化,從它的父Cuboid進行後聚合的開銷也不大,因為父Cuboid沒有比它多太多行的記錄。
從這個角度來說,Kylin的核心優勢在于使用額外的空間存儲預計算的結果,來換取查詢時間的縮減。而Cube的剪枝優化,則是一種試圖減少額外空間的方法,使用這種方法的前提是不會明顯影響查詢時間的縮減。在做剪枝優化的時候,需要選擇跳過那些“多餘”的Cuboid:有的Cuboid因為查詢樣式永遠不會被查詢到,是以顯得多餘;有的Cuboid的能力和其他Cuboid接近,是以顯得多餘。但是Cube管理者不是上帝,無法提前甄别每一個Cuboid是否多餘,是以Kylin提供了一系列簡單工具來幫助完成Cube的剪枝優化。
3.2 剪枝優化工具
3.2.1 使用衍生次元
首先觀察下面這個次元表,如圖3-4所示。
這是一個常見的時間次元表,裡面充斥着各種用途的時間次元,如每個日期對應的星期,每個日期對應的月份等。這些次元可以被分析師用來靈活地進行各個時間粒度上的聚合分析,而不需要進行額外的上卷操作。但是如果為了這個目的一下子引入這麼多元度,會導緻Cube中Cuboid的總數量呈現爆炸式的增長,往往得不償失。
在實際使用中,可以在次元中隻放入這個次元表的主鍵(在底層實作中,我們更偏向使用事實表上的外鍵,因為在Inner Join的情況下,事實表外鍵和次元表主鍵是一緻的,而在Left Join的情況下事實表外鍵是次元表主鍵的超集),也就是隻物化按日期(CAL_DT)聚合的Cuboid。當使用者需要在更高的粒度如按周、按月來進行聚合時,在查詢時會擷取按日期聚合的Cuboid資料,并在查詢引擎中實時地進行上卷操作,那麼就達到了犧牲一部分運作時性能來節省Cube空間占用的目的。
Kylin将這樣的理念包裝成一個簡單的優化工具—衍生次元。将一個次元表上的次元設定為衍生次元,則這個次元不會參與預計算,而是使用次元表的主鍵(其實是事實表上相應的外鍵)來替代它。Kylin會在底層記錄次元表主鍵與次元表其他次元之間的映射關系,以便在查詢時能夠動态地将次元表的主鍵“翻譯”成這些非主鍵次元,并進行實時聚合。雖然聽起來有些複雜,但是使用起來其實非常簡單,在建立Cube的Cube designer第二步添加次元的時候,選擇“Derived”而非“Normal”,如圖3-5所示。
衍生次元在Cube中不參加預計算,事實上如果前往Cube Designer的Advanced Setting,在Aggregation Groups和Rowkeys部分也完全看不到這些衍生次元,甚至在這些地方也找不到次元表KYLIN_CAL_DT的主鍵,因為如前所述,Kylin實際上是用事實表上的外鍵作為這些衍生次元背後真正的有效次元的,在前面的例子中,事實表與KYLIN_CAL_DT通過以下方式連接配接:
Join Condition:
DEFAULT.KYLIN_SALES.PART_DT = DEFAULT.KYLIN_CAL_DT.CAL_DT
是以,在Advanced Setting的Rowkeys部分就會看到PART_DT而看不到CAL_DT,更看不到那些KYLIN_CAL_DT表上的衍生次元,如圖3-6所示。
雖然衍生次元具有非常大的吸引力,但也并不是說所有的次元表上的次元都得變成衍生次元,如果從次元表主鍵到某個次元表次元所需的聚合工作量非常大,如從CAT_DT到YEAR_BEG_DT基本上需要365 : 1的聚合量,那麼将YERR_BEG_DT作為一個普通的次元,而不是衍生次元可能是一種更好的選擇。這種情況下,YERR_BEG_DT會參與預計算,也會有一些包含YERR_BEG_DT的Cuboid被生成。
3.2.2 聚合組
聚合組(Aggregation Group)是一個強大的剪枝工具,可以在Cube Designer的Advanced Settings裡設定不同的聚合組。聚合組将一個Cube的所有次元根據業務需求劃分成若幹組(當然也可以隻有一個組),同一個組内的次元更可能同時被同一個查詢用到,是以表現出更加緊密的内在關聯。不同組之間的次元在絕大多數業務場景裡不會用在同一個查詢裡,是以隻有在很少的Cuboid裡它們才有聯系。是以如果一個查詢需要同時使用兩個聚合組裡的次元,一般從一個較大的Cuboid線上聚合得到結果,這通常也意味着整個查詢會耗時較長。
每個分組的次元集合是Cube的所有次元的一個子集,分組之間可能有相同的次元,也可能完全沒有相同的次元。每個分組各自獨立地根據自身的規則産生一批需要被物化的Cuboid,所有分組産生的Cuboid的并集就形成了Cube中全部需要物化的Cuboid。不同的分組有可能會貢獻出相同的Cuboid,建構引擎會察覺到這點,并且保證每一個Cuboid無論在多少個分組中出現,都隻會被物化一次,如圖3-7所示。
舉例來說,假設有四個次元A、B、C、D,如果知道業務使用者隻會進行次元AB的組合查詢或次元CD的組合查詢,那麼該Cube 可以被設計成兩個聚合組,分别是聚合組AB和聚合組 CD。如圖3-8所示,生成的Cuboid的數量從24=16個縮減成8個。
假設建立了一個分析交易資料的Cube,它包含以下次元:顧客 ID(buyer_id)、交易日期(cal_dt)、付款的方式(pay_type)和買家所在的城市(city)。有時分析師需要通過分組聚合 city 、cal_dt和pay_type 來獲知不同消費方式在不同城市的情況;有時分析師需要通過聚合city、cal_dt和buyer_id,來檢視不同城市的顧客的消費行為。在上述執行個體中,推薦建立兩個聚合組,包含的次元和方式如圖3-9所示。
聚合組1:包含次元 [cal_dt, city, pay_type]
聚合組2:包含次元 [cal_dt, city, buyer_id]
可以看到,這樣設定聚合組後,組之間會有重合的Cuboid(上圖淺灰色部分),對于這些Cuboid隻會建構一次。在不考慮其他幹擾因素的情況下,這樣的聚合組設定将節省不必要的3個Cuboid: [pay_type, buyer_id]、[city, pay_type, buyer_id]和[cal_dt, pay_type, buyer_id],這樣就節省了存儲資源和建構的執行時間。
在執行查詢時,分幾種情況進行讨論:
情況1(分組次元在同一聚合組中):
SELECT cal_dt, city, pay_type, count(*) FROM table GROUP BY cal_dt, city, pay_type
将從Cuboid [cal_dt, city, pay_type]中擷取資料。
情況2(分組次元在兩個聚合組交集中):
SELECT cal_dt, city count(*) FROM table GROUP BY cal_dt, city
将從Cuboid [cal_dt, city]中擷取資料,可以看到這個Cuboid同時屬于兩個聚合組,這對查詢引擎是透明的。
情況3 如果有一條不常用的查詢(分組次元跨越了兩個聚合組):
SELECT pay_type, buyer_id, count(*) FROM table GROUP BY pay_type, buyer_id
沒有現成的完全比對的Cuboid,此時,Kylin會先找到包含這兩個次元的最小的Cuboid,這裡是Base Cuboid [pay_type,cal_dt,city,buyer_id],通過線上聚合的方式,從Case Cuboid中計算出最終結果,但會花費較長的時間,甚至有可能造成查詢逾時。
3.2.3 必需次元
如果某個次元在所有查詢中都會作為group by或者where中的條件,那麼可以把它設定為必需次元(Mandatory),這樣在生成Cube時會使所有Cuboid都必須包含這個次元,Cuboid的數量将減少一半。
通常而言,日期次元在大多數場景下可以作為必需次元,因為一般進行多元分析時都需要設定日期範圍。
再次,如果某個查詢不包含必需次元,那麼它将基于某個更大的Cuboid進行線上計算以得到結果。
3.2.4 層級次元
如果次元之間有層級關系,如國家–省–市這樣的層級,我們可以在Cube Designer的Advanced Settings裡設定層級次元。注意,需要按從大到小的順序選擇次元。
查詢時通常不會抛開上級節點單獨查詢下級節點,如國家–省–市的次元組合,查詢的組合一般是「國家」「國家,省」「國家,省,市」。因為城市會有重名,是以不會出現「國家,市」或者[市]這樣的組合。是以将國家(Country)、省(Province)、市(City)這三個次元設為層級次元後,就隻會保留Cuboid[Country, Province,City],[Country, Province],[Country]這三個組合,這樣能将三個次元的Cuboid組合數從 8個減至3個。
層級次元的适用場景主要是一對多的層級關系,如地域層級、機構層級、管道層級、産品層級。
如果一個查詢沒有按照設計來進行,如select Country,City,count(*) from table group by Country,City,那麼這裡不能回答這個查詢的Cuboid會從最接近的Cuboid[Country, Province, City]進行線上計算。顯而易見,由于[Country, Province, City]和[Country, City]之間相差的記錄數不多,這裡線上計算的代價會比較小。
3.2.5 聯合次元
聯合次元(Joint Dimension)一般用在同時查詢幾個次元的場景,它是一個比較強力的次元剪枝工具,往往能把Cuboid的總數降低幾個數量級。
舉例來說,如果使用者的業務場景中總是同時進行A、B、C三個次元的查詢分析,而不會出現聚合A、B或者聚合C這些更上卷的次元組合,那麼這類場景就是聯合次元所适合的。可以将次元 A、B和C定義為聯合次元,Kylin就僅僅會建構Cuboid [A,B,C],而Cuboid A,B [A]等都不會被生成。最終的Cube結果如圖3-10所示,Cuboid的數量從16個減至4個。
假設建立一個交易資料的Cube,它具有很多普通的次元,像是交易日期cal_dt、交易的城市city、顧客性别sex_id和支付類型pay_type等。分析師常用的分析總是同時聚合交易日期cal_dt、交易的城市city 和顧客性别sex_id,有時可能希望根據支付類型進行過濾,有時又希望看到所有支付類型下的結果。那麼,在上述執行個體中,推薦設立一組聚合組,并建立一組聯合次元,所包含的次元群組合方式下:
聚合組(Aggregation Group):[cal_dt, city, sex_id,pay_type]
聯合次元(Joint Dimension): [cal_dt, city, sex_id]
情況1(查詢包含所有的聯合次元):
SELECT cal_dt, city, sex_id, count(*) FROM table GROUP BY cal_dt, city, sex_id
它将從Cuboid [cal_dt, city, sex_id]中直接擷取資料。
情況2(如果有一條不常用的查詢,隻聚合了部分聯合次元):
SELECT cal_dt, city, count(*) FROM table GROUP BY cal_dt, city
沒有現成的完全比對的 Cuboid,Kylin會通過線上計算的方式,從現有的 Cuboid [cal_dt, city, sex_id中計算出最終結果。
聯合次元的适用場景:
- 次元經常同時在查詢where或group by條件中同時出現,甚至本來就是一一對應的,如customer_id和customer_name,将它們組成一個聯合次元。
- 将若幹個低基數(建議每個次元基數不超過10,總的基數叉乘結果小于10000)的次元合并組成一個了聯合次元,可以大大減少Cuboid的數量,利用線上計算能力,雖然會在查詢時多耗費有限的時間,但相比能減少的存儲空間和建構時間而言是值得的。
- 必要時可以将兩個有強關系的高基次元組成一個聯合次元,如合同日期和入賬日期。
- 可以将查詢時很少使用的若幹次元組成一個聯合次元,在少數查詢場景中承受線上計算的額外時間消耗,但能大大減少存儲空間和建構時間。
以上這些次元剪枝操作都可以在Cube Designer的Advanced Setting中的Aggregation Groups區域完成,如圖3-11所示。
從圖3-11中可以看到,目前Cube中隻有一個分組,點選左下角的 “New Aggregation Group”按鈕可以添加一個新的分組。在某一分組内,首先需要指定這個分組包含(Include)哪些次元,然後才可以進行必需次元、層級次元和聯合次元的建立。除了“Include”選項,其他三項都是可選的。此外,還可以設定“Max Dimension Combination”(預設為0,即不加限制),該設定表示對聚合組的查詢最多包含幾個次元,注意一組層級次元或聯合次元計為一個次元。在生成聚合組時會不生成超過“Max Dimension Combination”中設定的數量的Cuboid,是以可以有效減少Cuboid的總數。
聚合組的設計非常靈活,甚至可以用來描述一些極端的設計。假設我們的業務需求非常單一,隻需要某幾個特定的Cuboid,那麼可以建立多個聚合組,每個聚合組代表一個Cuboid。具體的方法是在聚合組中先包含某個Cuboid所需的所有次元,然後把這些次元都設定為強制次元。這樣目前的聚合組就隻包含我們想要的那一個Cuboid了。
再如,有時我們的Cube中有一些基數非常大的次元,如果不做特殊處理,它會和其他次元進行各種組合,進而産生大量包含它的Cuboid。所有包含高基數次元的Cuboid在行數和體積上都會非常龐大,這會導緻整個Cube的膨脹率過大。如果根據業務需求知道這個高基數的次元隻會與若幹個次元(而不是所有次元)同時被查詢,那麼就可以通過聚合組對這個高基數次元做一定的“隔離”。
我們把這個高基數的次元放入一個單獨的聚合組,再把所有可能會與這個高基數次元一起被查詢到的其他次元也放進來。這樣,這個高基數的次元就被“隔離”在一個聚合組中了,所有不會與它一起被查詢到的次元都不會和它一起出現在任何一個分組中,也就不會有多餘的Cuboid産生。這大大減少了包含該高基數次元的Cuboid的數量,可以有效地控制Cube的膨脹率。
3.3 并發粒度優化
當Segment中的某一個Cuboid 的大小超出一定門檻值時,系統會将該Cuboid的資料分片到多個分區中,以實作Cuboid資料讀取的并行化,進而優化Cube的查詢速度。具體的實作方式如下。
建構引擎根據Segment估計的大小,以及參數“kylin.hbase.region.cut”的設定決定Segment在存儲引擎中總共需要幾個分區來存儲,如果存儲引擎是HBase,那麼分區數量就對應HBase中的Region的數量。kylin.hbase.region.cut的預設值是5.0,機關是吉位元組(GB),也就是說,對于一個大小估計是50GB的Segment,建構引擎會給它配置設定10個分區。使用者還可以通過設定kylin.hbase.region.count.min(預設為1)和kylin.hbase.region.count.max(預設為500)兩個配置來決定每個Segment最少或最多被劃分成多少個分區。
由于每個Cube的并發粒度控制不盡相同,建議在Cube Designer的Configuration Overwrites中為每個Cube量身定制控制并發粒度的參數。在下面的例子中,将把目前Cube的kylin.hbase.region.count.min設定為2,把kylin.hbase.region.count.max設定為100,如圖3-12所示。這樣,無論Segment的大小如何變化,它的分區數量最小不會低于2,最大不會超過100。相應地,這個Segment背後的存儲引擎(HBase)為了存儲這個Segment,也不會使用小于2個或者超過100個分區(Region)。我們将kylin.hbase.region.cut調整為1,這樣,50GB的Segment基本上會被配置設定到50個分區,相比預設設定,我們的Cuboid可能最多會獲得5倍的并發量。
3.4 Rowkey優化
前面章節的側重點是減少Cube中Cuboid的數量,以優化Cube的存儲空間和建構性能,統稱以減少Cuboid的數量為目的的優化為Cuboid剪枝。在本節中,将重點通過對Cube的Rowkey的設定來優化Cube的查詢性能。
Cube的每個Cuboid中都包含大量的行,每個行又分為Rowkey和Measure兩個部分。每行Cuboid資料中的Rowkey都包含目前Cuboid中所有次元的值的組合。Rowkey中的各個次元按照Cube Designer→Advanced Setting→RowKeys中設定的順序和編碼進行組織,如圖3-13所示。
在Rowkeys設定頁面中,每個次元都有幾項關鍵的配置,下面将一一道來。
3.4.1 調整Rowkey順序
在Cube Designer→Advanced Setting→Rowkeys部分,可以上下拖動每一個次元來調節次元在Rowkey中的順序。這種順序對于查詢非常重要,因為目前在實作中,Kylin會把所有的次元按照顯示的順序黏合成一個完整的Rowkey,并且按照這個Rowkey升序排列Cuboid中所有的行,參照前一章的圖2-16。
不難發現,對排序靠前的次元進行過濾的效果會非常好,比如在圖2-16中的Cuboid中,如果對D1進行過濾,它是嚴格按照順序進行排列的;如果對D3進行過濾,它僅是在D1相同時在組内順序排列的。
如果在一個比較靠後的次元進行過濾,那麼這個過濾的執行就會非常複雜。以目前的HBase存儲引擎為例,Cube的Rowkey就對應HBase中的Rowkey,是一段位元組數組。我們目前沒有建立單獨的每個次元上的反向索引,是以對于在比較靠後的次元上的過濾條件,隻能依靠HBase的Fuzzy Key Filter來執行。盡管HBase做了大量相應的優化,但是在對靠後的位元組運用Fuzzy Key Filter時,一旦前面次元的基數很大,Fuzzy Key Filter的尋找代價就會很高,執行效率就會降低。是以,在調整Rowkey的順序時需要遵循以下幾個原則:
-
有可能在查詢中被用作過濾條件的次元,應當放在其他次元的前面。
a) 對于多個可能用作過濾條件的次元,基數高的(意味着用它進行過濾時,較多的行被過濾,傳回的結果集較小)更适合放在前面;
b) 總體而言,可以用下面這個公式給次元打分,得分越高的次元越應該放在前排:
排序評分=次元出現在過濾條件中的機率*用該次元進行過濾時可以過濾掉的記錄數。
- 将經常出現在查詢中的次元,放在不經常出現的次元的前面,這樣,在需要進行後聚合的場景中查詢效率會更高。
- 對于不會出現在過濾條件中的次元,按照其基數的高低,優先将低基數的次元放在Rowkey的後面。這是因為在逐層建構Cuboid、确定Cuboid的生成樹時,Kylin會優先選擇Rowkey後面的次元所在的父Cuboid來生成子Cuboid,那麼基數越低的次元,包含它的父Cuboid的行數就越少,聚合生成子Cuboid的代價就越小。
3.4.2 選擇合适的次元編碼
2.4.3節介紹過,Apache Kylin 支援多種次元編碼方式,使用者可以針對資料特征,選擇合适的編碼方式,進而減小資料的存儲空間。在具體使用過程中,如果用錯了編碼方式,可能會導緻建構和查詢的一系列問題。這裡要注意的事項包括:
- 字典(Dictionary)編碼(預設的編碼)不适用于高基數次元(基數值在300萬以上)。主要原因是,字典需要在單節點記憶體中建構,并在查詢的時候加載到Kylin記憶體;過大的字典不但會使得建構變慢,還會在查詢時占用很多記憶體,導緻查詢緩慢或失敗,是以應該避免對高基數次元使用字典編碼。如果實際中遇到高基數次元,首先思考此次元是否要引入Cube中,是否應該先對其進行泛化(Generalization),使其變成一個低基數次元;其次,如果一定要使用,那麼可以使用Fixed_length編碼,或Integer(如果這列的值是整型)編碼。
- Fixed_length編碼是最簡單的編碼,它通過補上空字元(如果次元值長度小于指定長度))或截斷(如果次元值長度大于指定長度),進而将所有值都變成等長,然後拼接到Rowkey中。它比較适合于像身份證号、手機号這樣的等長值次元。如果某個次元長度變化區間比較大,那麼你需要選擇一個合适的長度:長度過短會導緻資料截斷進而失去準确性,長度過長則導緻空間浪費。
3.4.3 按次元分片
在3.3節中介紹過,系統會對Cuboid中的資料在存儲時進行分片處理。預設情況下,Cuboid的分片政策是對于所有列進行哈希計算後随機配置設定的。也就是說,我們無法控制Cuboid的哪些行會被分到同一個分片中。這種預設的方法固然能夠提高讀取的并發程度,但是它仍然有優化的空間。按次元分片提供了一種更加高效的分片政策,那就是按照某個特定次元進行分片(Shard By Dimension)。簡單地說,當你選取了一個次元用于分片後,如果Cuboid中的某兩行在該次元上的值相同,那麼無論這個Cuboid最終被劃分成多少個分片,這兩行資料必然會被配置設定到同一個分片中。
這種分片政策對查詢有着極大的好處。我們知道,Cuboid的每個分片會被配置設定到存儲引擎的不同實體機器上。Kylin在讀取Cuboid資料的時候會向存儲引擎的若幹機器發送讀取的RPC請求。在RPC請求接收端,存儲引擎會讀取本機的分片資料,并在進行一定的預處理後發送RPC回應(如圖3-14所示)。以HBase存儲引擎為例,不同的Region代表不同的Cuboid分片,在讀取Cuboid資料的時候,HBase會為每個Region開啟一個Coprocessor執行個體來處理查詢引擎的請求。查詢引擎将查詢條件和分組條件作為請求參數的一部分發送到Coprocessor中,Coprocessor就能夠在傳回結果之前對目前分片的資料做一定的預聚合(這裡的預聚合不是Cube建構的預聚合,是針對特定查詢的深度的預聚合)。
如果按照次元劃分分片,假設是按照一個基數比較高的次元seller_id進行分片的,那麼在這種情況下,每個分片承擔一部分seller_id,各個分片不會有相同的seller_id。所有按照seller_id分組(group by seller_id)的查詢都會變得更加高效,因為每個分片預聚合的結果會更加專注于某些seller_id,使得分片傳回結果的數量大大減少,查詢引擎端也無須對各個分片的結果做分片間的聚合。按次元分片也能讓過濾條件的執行更加高效,因為由于按次元分片,每個分片的資料都更加“整潔”,便于查找和索引。
3.5 Top_N度量優化
在生活中我們總能看到“世界500強公司” “銷量最好的十款汽車”等标題的新聞報道,Top_N 分析是資料分析場景中常見的需求。在大資料時代,由于明細資料集越來越大,這種需求越來越明顯。在沒有預計算的情況下,得到一個分布式大資料集的 Top_N 結果需要很長時間,導緻點對點查詢的效率很低。
Kylin v1.5以後的版本中引入了 Top_N 度量,意在進行Cube 建構的時候預計算好需要的 Top_N,在查詢階段就可以迅速地擷取并傳回Top_N記錄。這樣,查詢性能就遠遠高于沒有Top_N預計算結果的Cube,友善分析師對這類資料進行查詢。
這裡的 Top_N 度量是一個近似的實作,如你想要了解其近似度,需要在本節之後的内容中更多地了解 Top_N 背後的算法和資料分布結構。
讓我們用Kylin中通過sample.sh生成的項目“learn_kylin” 對Top_N進行說明。我們将重點使用其中的事實表“kylin_sales”。
這張樣例表 “default.kylin_sales” 模拟了線上集市的交易資料,内含多個次元和度量列。這裡僅用其中的四列即可:“PART_DT” “LSTG_SITE_ID” “SELLER_ID”和“PRICE”。表3-1所示為這些列的内容和基數簡介,顯而易見 “SELLER_ID” 是一個高基列。
假設電商公司需要查詢特定時段内,特定站點交易額最高的100位賣家。查詢語句如下:
SELECT SELLER_ID, SUM(PRICE) FROM KYLIN_SALES
WHERE
PART_DT >= date'2012-02-18' AND PART_DT < date'2013-03-18'
AND LSTG_SITE_ID in (0)
group by SELLER_ID
order by SUM(PRICE) DESC limit 100
方法1:在不設定Top_N度量的情況下,為了支援這個查詢,在建立Cube時設計如下:定義“PART_DT”“LSTG_SITE_ID”“SELLER_ID”作為次元,同時定義 “SUM(PRICE) ”作為度量。Cube 建構完成之後,Base Cuboid 如表3-2所示。
假設這些次元是彼此獨立的,則Base Cuboid中行數為各次元基數的乘積:730 50 1 million = 36.5 billion = 365億。其他包含“SELLER_ID”字段的Cuboid也至少有百萬行。由此可知,由“SELLER_ID”作為次元會使得Cube的膨脹率很高,如果次元更多或基數更高,則情況更糟。但真正的挑戰不止如此。
我們可能還會發現上面那個SQL查詢并不能正常執行,或者需要花費特别長的時間,原因是這個查詢擁有太大的線上計算量。假設你想查30天内site_00銷售額排前100名的賣家,則查詢引擎會從存儲引擎中讀取約3000萬行記錄,然後按銷售額進行線上排序計算(排序無法用到預計算結果),最終傳回排前100名的賣家。由于其中的關鍵步驟沒有進行預計算,是以雖然最終結果隻有100行,但計算耗時非常長,且記憶體和其中的控制器都在查詢時被嚴重消耗了。
反思以上過程,業務關注的隻是銷售額最大的那些賣家,而我們存儲了所有的(100萬)賣家,且在存儲時是根據賣家ID而不是業務需要的銷售額進行排序的,是以線上計算量非常大,因而此處有很大的優化空間。
方法2:為了得到同一查詢結果。如果在建立Cube時,對需要的Top_N進行了預計算,則查詢會更加高效。如果在建立Cube時設計如下:不定義“SELLER_ID”為次元,僅定義“PART_DT”“LSTG_SITE_ID”為次元,同時定義一個Top_N度量,如圖3-15所示。
“PRICE”定義在“ORDER|SUM by Column”,“SELLER_ID”定義在“Group by Column”。
新Cube的Base Cuboid 如表3-3所示,Top_N度量的單元格中存儲了按seller_id進行聚合且按sum(price) 倒序排列的seller_id和sum(price)的組合。
現在Base Cuboid中隻有730 * 50 = 36500行。在度量的單元格中,預計算的Top_N結果以倒序的方式存儲在一個容器中,而序列尾端的記錄已經被過濾掉。
現在,對于上面那個Top_100 seller的查詢語句,隻需要從記憶體中讀取30行,Kylin将會從Top_N Measure的容器中抽出“SELLER_ID”和“SUM(PRICE)”,然後将其傳回用戶端(并且是已經完成排序的)。現在查詢結果就能以亞秒級傳回了。
一般來說,Kylin在Top_N的單元格中會存儲100倍的Top_N定義的傳回類型的記錄數,如對于Top100,就存儲10000條seller00xxxx:xx.xx記錄。這樣一來,對于Base Cuboid的Top_N查詢總是精确的,不精确的情況會出現在對于其他Cuboid的查詢上。舉例來說,對于Cuboid[PART_DT],Kylin會将所有日期相同而站點不同的TOP_N單元格進行合并,這個合并後的結果會是近似的,尤其是在各個站點的前幾名賣家相差較多的情況下。比如,如果site_00中排第100名的賣家在其他站點中都排在第10000名以後,那麼它的Top_N記錄在其他站點都會被舍去,Kylin在合并TOP_N Measure時發現其他站點裡沒有這個賣家的值,于是會賦予這個賣家在其他站點中的sum(price)一個估計值,這個估計值既可能比實際值高,也可能比實際值低,最後它在Cuboid[PART_DT]中的Top_N單元格中存儲的是一個近似值。
3.6 Cube Planner優化
Kylin自v2.3.0以後的版本引入了Cube Planner功能,自動地對Cube的結構進行優化。如圖3-16所示,在使用者定義的Aggregation Group等手動優化基礎上,Cube Planner能根據每個Cuboid的大小和它對整個Cube産生的查詢增益,結合曆史查詢資料對Cuboid進行進
一步剪枝。Cube Planner使用貪心算法和基因算法排除不重要和不必要的Cuboid,不對這些Cuboid進行預計算,進而大大減少計算量、節約存儲空間,進而提高查詢效率。
Cube Planner優化分為兩個階段。第一階段發生在初次建構Cube時:Cube Planner會利用在“Extract Fact Table Distinct Columns”步驟中得到的采樣資料,預估每個Cuboid的大小,進而計算出每個Cuboid的效益比(該Cuboid的查詢成本 / 對應次元組合物化後對整個Cube的所有查詢能減少的查詢成本)。Cube Planner隻會對那些效益比更高的次元組合進行預計算,而舍棄那些效益比更低的次元組合。第二階段作用于已經運作一段時間的Cube。在這一階段,Cube Planner會從System Cube中擷取該Cube的查詢統計資料,并根據被查詢命中的機率給Cuboid賦予一定權重。當使用者觸發對Cube的優化操作時,那些幾乎不被查詢命中的Cuboid會被删除,而那些被頻繁查詢卻尚未被預計算出的Cuboid則會被計算并更新到Cube中。
在Kylin v2.5.0及以後的版本中,Cube Planner預設開啟,第一階段的過程對使用者透明,而使用第二階段則需要事先配置System Cube并由使用者手動觸發優化。關于Cube Planner的具體實作原理、使用方法及相關配置會在本書第6章中詳述。
3.7 其他優化
3.7.1 降低度量精度
有一些度量有多種精度可供選擇,但是精度較高的度量往往需要付出額外的代價,這就意味着更大的空間占用和更多的運作及建構成本。以近似值的Count Distinct度量為例,Kylin提供多種精度的選擇,讓我們選擇其中幾種進行對比,如表3-4所示。
從表3-4中可以看出,HLLC 16類型占用的空間是精度最低的類型的64倍!而即使是精度最低的Count Distinct度量也已經非常占用空間了。是以,當業務可以接受較低精度時,使用者應當考慮Cube空間方面的影響,盡量選擇低精度的度量。
3.7.2 及時清理無用Segment
第4章提及,随着增量建構出來的Segment慢慢累積,Cube的查詢性能将會下降,因為每次跨Segment查詢都需要從存儲引擎中讀取每一個Segment的資料,并且在查詢引擎中對不同Segment的資料進行再聚合,這對于查詢引擎和存儲引擎來說都是巨大的壓力。從這個角度來說,及時使用第4章介紹的Segment碎片清理方法,有助于優化Cube的使用效率。
3.8 小結
本章從多個角度介紹了Cube的優化方法:從Cuboid剪枝的角度、從并發粒度控制的角度、從Rowkey設計的角度,還有從度量精度選擇的角度。總的來說,Cube優化需要Cube管理者對Kylin有較為深刻的了解和認識,這也無形中提高了使用和管理Kylin的門檻。對此,我們在較新的Kylin版本中通過對資料分布和查詢樣式的曆史進行分析,自動化一部分優化操作,幫助使用者更加友善地管理Kylin中的資料,詳見第6章。長度“12”