Kylin原始碼解析——Cube構建過程中如何實現降維
-維度簡述
Kylin中Cube的描述類CubeDesc有兩個欄位,rowkey和aggregationGroups。
@JsonProperty("rowkey")
private RowKeyDesc rowkey;
@JsonProperty("aggregation_groups")
private List<AggregationGroup> aggregationGroups;
其中rowkey描述的是該Cube中所有維度,在將統計結果儲存到HBase中,各維度在rowkey中的排序情況,如下是rowkey的一個樣例,包含6個維度。在描述一種維度組合時,是通過二進位制來表示。
如這6個維度,都包含時,是 111111。
如 111001,則表示只包含INSERT_DATE、VISIT_MONTH、VISIT_QUARTER、IS_CLICK這四個維度。
二進位制從左到右表示的就是rowkey_columns中各個維度的包含與否,1包含,0不包含。
這樣的一個二進位制組合就是一個cuboid,用long整型表示。
"rowkey": {
"rowkey_columns": [
{
"column": "DW_OLAP_CPARAM_INFO_VERSION2.INSERT_DATE",
"encoding": "dict",
"isShardBy": false
},
{
"column": "DW_OLAP_CPARAM_INFO_VERSION2.VISIT_MONTH",
"encoding": "dict",
"isShardBy" : false
},
{
"column": "DW_OLAP_CPARAM_INFO_VERSION2.VISIT_QUARTER",
"encoding": "dict",
"isShardBy": false
},
{
"column": "DW_OLAP_CPARAM_INFO_VERSION2.BUSINESS_TYPE",
"encoding": "dict",
"isShardBy" : false
},
{
"column": "DW_OLAP_CPARAM_INFO_VERSION2.SHOP_TYPE",
"encoding": "dict",
"isShardBy": false
},
{
"column": "DW_OLAP_CPARAM_INFO_VERSION2.IS_CLICK",
"encoding": "dict",
"isShardBy": false
},
]
}
而aggregationGroups則描述的是這些維度的分組情況,也就是在一個Cube中的所有維度,可以分成多個分組,每個分組就是一個AggregationGroup,各AggregationGroup之間是相互獨立的。
對於所有的維度為什麼要做分組?
在Kylin中會預先把所有維度的各種組合下的統計結果原先計算出來,假設維度有N個,那麼維度的組合就有2^N中組合,比如N=6,則總的維度組合就有2^6=64種。
如果能夠根據實際查詢的需求,發現某些維度之間是不會有交叉查詢的,那其實把這些維度組合的統計結果計算出來,也是浪費,因為後續的查詢中,壓根不會用到,這樣既浪費了計算資源,更浪費了儲存資源,所有可以按實際的查詢需求,將維度進行分組,比如6個維度,分成2組,一組4個維度,一組2個維度,則總的維度組合則是2^4+2^2=20,比64小了很多,這裡的分組這是舉例說明分組,可以有效的減少維度組合,從而縮減儲存空間,另外各個分組之間是可以有共享維度的,比如6個維度,可以分成兩組,一組4個,另一組3個,兩個分組中的共享維度,在後續計算中,其對應的統計結果不會被計算兩次,只會計算一次,這也是Kylin聰明的地方。
一個AggragationGroup中包含includes和selectRule兩個欄位,其中includes就是該分組中包含了哪些維度,是一個字串陣列。
@JsonProperty("includes")
private String[] includes;
@JsonProperty("select_rule")
private SelectRule selectRule;
如下是隻有一個分組的樣例。
"aggregation_groups": [
{
"includes": [
"DW_OLAP_AD_NORMAL_CONTRAST_VERSION2.FCID_SEARCH",
"DW_OLAP_AD_NORMAL_CONTRAST_VERSION2.BUSINESS_TYPE",
"DW_OLAP_AD_NORMAL_CONTRAST_VERSION2.FCID0”,
"DW_OLAP_AD_NORMAL_CONTRAST_VERSION2.FCID1”,
"DW_OLAP_AD_NORMAL_CONTRAST_VERSION2.FCID2”,
"DW_OLAP_AD_NORMAL_CONTRAST_VERSION2.FCID3",
"DW_OLAP_AD_NORMAL_CONTRAST_VERSION2.FCNAME0",
"DW_OLAP_AD_NORMAL_CONTRAST_VERSION2.FCNAME1",
"DW_OLAP_AD_NORMAL_CONTRAST_VERSION2.FCNAME2",
"DW_OLAP_AD_NORMAL_CONTRAST_VERSION2.FCNAME3",
"DW_OLAP_AD_NORMAL_CONTRAST_VERSION2.INSERT_DATE",
"DW_OLAP_AD_NORMAL_CONTRAST_VERSION2.SHOP_TYPE”
"DW_OLAP_AD_NORMAL_CONTRAST_VERSION2.USERID",
"DW_OLAP_AD_NORMAL_CONTRAST_VERSION2.SHOPID"
],
"select_rule": {
"hierarchy_dims": [
[
"DW_OLAP_AD_NORMAL_CONTRAST_VERSION2.FCID0",
"DW_OLAP_AD_NORMAL_CONTRAST_VERSION2.FCID1",
"DW_OLAP_AD_NORMAL_CONTRAST_VERSION2.FCID2",
"DW_OLAP_AD_NORMAL_CONTRAST_VERSION2.FCID3"
],
[
"DW_OLAP_AD_NORMAL_CONTRAST_VERSION2.FCNAME0",
"DW_OLAP_AD_NORMAL_CONTRAST_VERSION2.FCNAME1",
"DW_OLAP_AD_NORMAL_CONTRAST_VERSION2.FCNAME2",
"DW_OLAP_AD_NORMAL_CONTRAST_VERSION2.FCNAME3"
]
],
"mandatory_dims": [
"DW_OLAP_AD_NORMAL_CONTRAST_VERSION2.INSERT_DATE",
"DW_OLAP_AD_NORMAL_CONTRAST_VERSION2.BUSINESS_TYPE"
],
"joint_dims": [
"DW_OLAP_AD_NORMAL_CONTRAST_VERSION2.USERID",
"DW_OLAP_AD_NORMAL_CONTRAST_VERSION2.SHOPID"
]
}
}
]
-cuboid的有效性判斷
在進行降維分析之前,先簡單減少一下,給定的一個cuboid的,比如 110011 ,這樣一個cuboid,如何判斷在一個AggregationGroup中是否是有效的?判斷邏輯在Cuboid類的isValid方法中,就是用來判斷給定的一個cuboidID,在一個AggregationGroup中是否是一個合法有效的cuboidID。
static boolean isValid(AggregationGroup agg, long cuboidID) {
// 前面說明,一個cuboidID就是一組維度的組合,1位包含,0為不包含,所以cuboidID必定大於0
if (cuboidID <= 0) {
return false; //cuboid must be greater than 0
}
// 一個cuboidID在一個AggregationGroup中是否有效的前提,是它包含的維度必須都要是該AggregationGroup中的維度才行
// agg.getPartialCubeFullMask()獲取的就是該AggregationGroup中所有維度組成的一個掩碼
if ((cuboidID & ~agg.getPartialCubeFullMask()) != 0) {
return false; //a cuboid's parent within agg is at most partialCubeFullMask
}
// 接下來則分別進行了強制維度、層級維度、聯合維度的校驗,都校驗通過時,才能算是有效合法的
return checkMandatoryColumns(agg, cuboidID) && checkHierarchy(agg, cuboidID) && checkJoint(agg, cuboidID);
}
從上面的邏輯可以看出,判斷一個cuboidID在一個AggregationGroup中是否合法有效的邏輯很清晰,首先該cuboidID要至少包含一個維度,然後包含的維度需要是該AggregationGroup中維度的子集,最後就是在進行強制維度、層級維度、聯合維度的規則校驗。
強制維度的校驗邏輯,簡單說就是cuboidID中需要包含強制維度的所有維度,另外當,cuboidID中只包含強制維度的維度時,則根據配置中是否允許這種情況,進行判斷,具體邏輯如下:
private static boolean checkMandatoryColumns(AggregationGroup agg, long cuboidID) {
// agg.getMandatoryColumnMask() 獲取的是所有強制維度組成的二進位制
long mandatoryColumnMask = agg.getMandatoryColumnMask();
// 如果沒有包含所有強制維度,則返回false
if ((cuboidID & mandatoryColumnMask) != mandatoryColumnMask) {
return false;
} else {
// 如果包含了整個cube的所有維度,則總是返回true的
if (cuboidID == getBaseCuboidId(agg.getCubeDesc())) {
return true;
}
// 如果配置中允許該cuboidID中的維度都是強制維度,則返回true
// 如果不允許全部,則cuboidID中需要包含除強制維度以為的維度
return agg.isMandatoryOnlyValid() || (cuboidID & ~mandatoryColumnMask) != 0;
}
}
層級維度的校驗邏輯,校驗邏輯簡單明瞭,只要cuboidID中包含某個層級維度中的維度,則必須與該層級維度的某個具體的組合相匹配才行,否則就是無效的。
比如省、市、縣這樣一個層級維度,當cuboidID中包含省、市、縣這三個維度中的某些維度的時候,也即是cuboidID & hierarchyMasks.fullMask 大於0的時候,則cuboidID中包含的這個層級維度的組合只能是 《省》、《省、市》、《省、市、縣》這三種組合,如果包含的是《省、縣》或者《市、縣》或者其他組合,則都是無效的。具體邏輯如下。
private static boolean checkHierarchy(AggregationGroup agg, long cuboidID) {
List<HierarchyMask> hierarchyMaskList = agg.getHierarchyMasks();
// if no hierarchy defined in metadata
if (hierarchyMaskList == null || hierarchyMaskList.size() == 0) {
return true;
}
hier: for (HierarchyMask hierarchyMasks : hierarchyMaskList) {
// 如果包含了某個層級維度組中的維度,則就需要包含該層級維度組中的某種具體組合才行
long result = cuboidID & hierarchyMasks.fullMask;
if (result > 0) {
for (long mask : hierarchyMasks.allMasks) {
if (result == mask) {
continue hier;
}
}
return false;
}
}
return true;
}
聯合維度的校驗邏輯,聯合維度顧名思義,就是連在一起的,要麼一起出現,要麼都不出現,校驗邏輯如下:
private static boolean checkJoint(AggregationGroup agg, long cuboidID) {
for (long joint : agg.getJoints()) {
long common = cuboidID & joint;
// 如果包含了某個聯合組中的維度,則就必須包含該聯合組中的全部維度
if (!(common == 0 || common == joint)) {
return false;
}
}
return true;
}
上述分析了判斷一個cuboidID在一個AggregationGroup中是否有效的判斷,那判斷一個cuboidID在一個Cube中是否有效,就是判斷這個cuboidID在該Cube的所有AggregationGroup中都是有效的,邏輯如下:
public static boolean isValid(CubeDesc cube, long cuboidID) {
//base cuboid is always valid
if (cuboidID == getBaseCuboidId(cube)) {
return true;
}
// 就是這個迴圈,遍歷了所有的AggregationGroup
for (AggregationGroup agg : cube.getAggregationGroups()) {
if (isValid(agg, cuboidID)) {
return true;
}
}
return false;
}
-降維邏輯
對於維度的升降操作主要在類CuboidScheduler中,對應的方法則是
public Set<Long> getPotentialChildren(long parent) {
...
}
public long getParent(long child) {
...
}
首先來看getPotentialChildren這個方法,就是給定一個cuboid,找出其所有的潛在的子cuboid,這裡的子cuboid就是說parent通過減少一個或者多個維度,得到的新的cuboid。
public Set<Long> getPotentialChildren(long parent) {
// Cuboid.getBaseCuboid(cubeDesc).getId() 獲取的就是該Cube的所有維度都存在的cuboid,比如6個維度,則111111
// Cuboid.isValid(cubeDesc, parent) 是判斷parent這個cuboid是不是一個有效的cuboid
// 這裡就是判斷給的parent這個cuboid是否是一個有效的cuboid
if (parent != Cuboid.getBaseCuboid(cubeDesc).getId() && !Cuboid.isValid(cubeDesc, parent)) {
throw new IllegalStateException();
}
HashSet<Long> set = Sets.newHashSet();
if (Long.bitCount(parent) == 1) {
// 如果parent中只包含一個維度了,則就不需要在進一步降維了,再降維就是空了
return set;
}
// 如果parent包含了Cube中的所有維度
if (parent == Cuboid.getBaseCuboidId(cubeDesc)) {
//那麼這個時候,parent的子cuboidID中,就應該包含Cube中的所有AggregationGroup的BaseCuboidID
for (AggregationGroup agg : cubeDesc.getAggregationGroups()) {
long partialCubeFullMask = agg.getPartialCubeFullMask();
if (partialCubeFullMask != parent && Cuboid.isValid(agg, partialCubeFullMask)) {
set.add(partialCubeFullMask);
}
}
}
// Cuboid.getValidAggGroupForCuboid(cubeDesc, parent)就是找出Cube中,parent在其中合法的AggregationGroup
// 然後依次遍歷這些AggregationGroup
for (AggregationGroup agg : Cuboid.getValidAggGroupForCuboid(cubeDesc, parent)) {
// 對於普通的維度,就是除去強制維度、層級維度、聯合維度之後,還剩下的維度
for (long normalDimMask : agg.getNormalDims()) {
long common = parent & normalDimMask;
long temp = parent ^ normalDimMask;
// 對於每一個普通維度
// 如果在parent中存在,則將其從parent中移除後降維得到的temp,如果在該group中,仍然是一個有效的cuboidID,則算一個parent的child
if (common != 0 && Cuboid.isValid(agg, temp)) {
set.add(temp);
}
}
// 特別注意一下,這裡為了簡單理解,所以假設的parent和層級維度的取值,都是順序的,
// dims一次為00000100、00000010、00000001,
// 真實的情況是dims的取值可能為 00000001、10000000、00010000,這裡的順序都是反映了該維度在rowkey中的順序 *
// 針對層級維度的降維
// 建設parent為 11111111
// 層級維度為 fullMask 00000111 , allMasks 為 00000100、00000110、00000111, dims為 00000100、00000010、00000001
// for (int i = hierarchyMask.allMasks.length - 1; i >= 0; i--)這層迴圈,allMasks[i]遍歷順序為 00000111、00000110、00000100
// 比如第一次迴圈allMasks[i]取00000111,與parent與操作,就是判斷allMasks[i]中的維度是否都包含在parent中,如果都包含在parent中,進入if條件
// 這時候取出allMasks[i]為00000111,這個組合中的最低階的維度為00000001,然後判斷該維度是否是聯合維度的一員,如果不是,進入if條件
// 然後將層級維度的最末一級去掉,這裡就是去掉00000001這一維度,去掉後的cuboidID為 11111111^00000001=11111110
// 然後判斷11111110是否在該group中是一個有效的cuboidID,如果是,則作為parent的child
for (AggregationGroup.HierarchyMask hierarchyMask : agg.getHierarchyMasks()) {
for (int i = hierarchyMask.allMasks.length - 1; i >= 0; i--) {
// 只有當層級維度中的某個組合中的維度都在parent中時,才進入if條件
if ((parent & hierarchyMask.allMasks[i]) == hierarchyMask.allMasks[i]) {
// 所有聯合維度中都不包含當前層級維度組合中的最低維度時,進入if條件
if ((agg.getJointDimsMask() & hierarchyMask.dims[i]) == 0) {
if (Cuboid.isValid(agg, parent ^ hierarchyMask.dims[i])) {
//only when the hierarchy dim is not among joints
set.add(parent ^ hierarchyMask.dims[i]);
}
}
break; //if hierarchyMask 111 is matched, won't check 110 or 100 }
}
}
//joint dim section
// 聯合維度相對比較簡單,如果包含某個聯合維度,則將其全部去除,再判斷其有效性,如果有效,則加入parent的child佇列
for (long joint : agg.getJoints()) {
if ((parent & joint) == joint) {
if (Cuboid.isValid(agg, parent ^ joint)) {
set.add(parent ^ joint);
}
}
}
}
return set;
}
降維操作主要是就是針對3類維度進行降維操作,普通維度(一個AggregationGroup的所有維度除去強制維度、層級維度、聯合維度之後還剩餘的維度)、層級維度、聯合維度。
普通維度的降維就是首先判斷parent是否包含該普通維度,如果包含,則將其從parent中移除,然後判斷移除後的cuboidID在該AggregationGroup中是否有效合法;
層級維度的降維,首先parent中需要包含某個層級維度的某種組合,然後再將該層級維度組合中的最末級的維度移除,得到的cuboidID再去校驗合法性;
聯合維度的降維最直接明瞭,包含就全部去除,然後校驗合法性。
以上就是通過一個給定的cuboidID,獲取所有可能的子cuboidID的邏輯,也就是降維的過程。
-升維邏輯
那既然進行降維操作已經有了,為什麼還要有一個getParent方法呢?其實從方法名中可以一探一二,getPotentialChildren獲取可能的孩子,這就是說getPotentialChildren方法的邏輯獲取的所有child只是說,可能是parent的child,但未必真的是,所以在getSpanningCuboid方法中,先通過getPotentialChildren獲取了所以潛在的child,然後又對每一個potential,都去獲取其對應的父親,看是否與給定的這個parent一致,如果一致,才說明父子相認,也就是父親認了兒子,同時也需要兒子認了父親才行。
public List<Long> getSpanningCuboid(long cuboid) {
if (cuboid > max || cuboid < 0) {
throw new IllegalArgumentException("Cuboid " + cuboid + " is out of scope 0-" + max);
}
List<Long> result = cache.get(cuboid);
if (result != null) {
return result;
}
result = Lists.newArrayList();
Set<Long> potentials = getPotentialChildren(cuboid);
for (Long potential : potentials) {
if (getParent(potential) == cuboid) {
result.add(potential);
}
}
cache.put(cuboid, result);
return result;
}
接著看下getParent的邏輯,getParent方法的邏輯與getPotentialChildren的邏輯剛好反過來,是一個升維的過程。
public long getParent(long child) {
List<Long> candidates = Lists.newArrayList();
long baseCuboidID = Cuboid.getBaseCuboidId(cubeDesc);
// 如果該child等於fullMask 或者 該child不是有效的cuboidID,則拋異常
// 這也好理解,fullMask是不可能存在父親的,因為它就是所有cuboidID的老祖宗
if (child == baseCuboidID || !Cuboid.isValid(cubeDesc, child)) {
throw new IllegalStateException();
}
// 這裡與getPotentialChildren一樣,也是首選找出所有可能的AggregationGroup,然後開始遍歷
for (AggregationGroup agg : Cuboid.getValidAggGroupForCuboid(cubeDesc, child)) {
// thisAggContributed 這個變數標識 當前該AggregationGroup是否已經貢獻出了一個parent
boolean thisAggContributed = false;
// 這裡也好理解,如果child就是該AggregationGroup的基cuboidID,那麼它的父親只能是Cube的基cuboidID
if (agg.getPartialCubeFullMask() == child) {
return baseCuboidID;
}
//+1 dim
//add one normal dim (only try the lowest dim)
// 這裡只會新增lowest維度,是跟最後的Collections.min有呼應的
// 因為最後只會選擇所有滿足條件中的維度數最少,在相同維度數中,值最小的那個候選者,
// 所以這裡就沒有必要把高位的維度新增進去,反正最後也會被過濾掉
// 這一點在後面的升維中都會有所體現
long normalDimsMask = (agg.getNormalDimsMask() & ~child);
if (normalDimsMask != 0) {
candidates.add(child | Long.lowestOneBit(normalDimsMask));
thisAggContributed = true;
}
// 開始層級維度的升維
for (AggregationGroup.HierarchyMask hierarchyMask : agg.getHierarchyMasks()) {
if ((child & hierarchyMask.fullMask) == 0) {
// 這裡只加入最高階的那個維度,其他維度不繼續處理的原因,也是跟最後的排序,只取維度最少有關
candidates.add(child | hierarchyMask.dims[0]);
thisAggContributed = true;
} else {
for (int i = hierarchyMask.allMasks.length - 1; i >= 0; i--) {
// 只有與層級維度的某個組合匹配時,才會進入if條件
if ((child & hierarchyMask.allMasks[i]) == hierarchyMask.allMasks[i]) {
if (i == hierarchyMask.allMasks.length - 1) {
// 感覺這裡應該用break,而不是continue,雖然這裡用contine也不會有問題
// 如果某個層級維度的所有維度都已經在child中,則child無法再新增維度來形成parent了
// 比如省、市、縣,如果child中已經包含了省、市、縣,則沒法再進一步新增這個層級的維度了
continue;//match the full hierarchy }
if ((agg.getJointDimsMask() & hierarchyMask.dims[i + 1]) == 0) {
// 如果是 省、市,則可以新增一個 縣 維度進來,如果是省,則可以新增一個 市 維度進來
if ((child & hierarchyMask.dims[i + 1]) == 0) {
//only when the hierarchy dim is not among joints
candidates.add(child | hierarchyMask.dims[i + 1]);
thisAggContributed = true;
}
}
// 這裡的break,就是說,如果已經有一個多維層級組合滿足要求了,就無需進一步檢查少維度的層級組合了
// 比如已經 省、市,這個組合已經滿足了,就沒必要再去檢查 省 這個維度組合了。
break;//if hierarchyMask 111 is matched, won't check 110 or 100 }
}
}
}
// 如果經過上面的普通維度和層級維度,新增維度操作後,已經找到了候選parent,則無需再進行聯合維度的操作
// 因為聯合維度至少會加2個維度進來,根據最後的Collections.min,會優先選維度數少的
if (thisAggContributed) {
//next section is going to append more than 2 dim to child
//thisAggContributed means there's already 1 dim added to child
//which can safely prune the 2+ dim candidates.
continue;
}
//2+ dim candidates
// 聯合維度的很簡單,如果沒有包含,則直接全部加入
for (long joint : agg.getJoints()) {
if ((child & joint) == 0) {
candidates.add(child | joint);
}
}
}
if (candidates.size() == 0) {
throw new IllegalStateException();
}
// 這裡的Collections.min就是上述很多地方可以提前結束的原因
return Collections.min(candidates, Cuboid.cuboidSelectComparator);
}
這個升維的過程,在進入AggregationGroup遍歷後,主要通過增加一個維度的升維,和增加2個或以上維度的升維,主要也即是聯合維度了。
對於增加1個維度的升維:
對於普通維度,則從所有普通維度中,選擇一個在rowkey中排在最後面的那個維度,然後新增到child中;
對於層級維度,如果是該層級維度中的維度都不包含,則取該層級維度中最高階的那個維度新增到child中;如果是child只包含了該層級維度中所有維度的部分維度,比如對於省、市、縣這個層級維度,只包含了省或者省市,則可以新增一個市或者縣到child中;
如果在1個維度的升維中已經找到了一個候選的parent,則聯合維度就不需在進行了,因為聯合維度至少會加入兩個維度。
再來看一下getParent方法的最後一句程式碼,就明白為什麼升維的過程中,很多潛在的parent可以直接忽略掉。
Cuboid.cuboidSelectComparator的實現如下。
也就是對於任何兩個cuboidID,先從中選出包含維度少的那個cuboidID,如果兩個cuboidID包含的維度數相同,則在進一步比較,值小的為所需要的cuboidID。
也即是getParent獲取的所有候選parent的集合candidates,經過這個比較器排序後,最小的那個cuboidID,就是包含維度最少,且在相同緯度的不同cuboidID中,值是最小的那個。
//smaller is better
public final static Comparator<Long> cuboidSelectComparator = new Comparator<Long>() {
@Override
public int compare(Long o1, Long o2) {
return ComparisonChain.start().compare(Long.bitCount(o1), Long.bitCount(o2)).compare(o1, o2).result();
}
};