1. 程式人生 > >Kylin原始碼解析——Cube構建過程中如何實現降維

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();
    }
};