小白理解 Apache Kylin 到源碼學(xué)習(xí)

用戶視角

你是否面對海量數(shù)據(jù)查詢緩慢而備受煎熬 ?受限于查詢速度而只能屈服于數(shù)據(jù)量?想在同一個分析工具下分析不同來源的數(shù)據(jù)?想在不同的 BI 工具下仍具有敏捷的查詢響應(yīng)速度?在 Apache Kylin 面前這些都不是問題,一圖勝千言。

img

好了,你可能想說:“前面的問題你倒是快給我解決啊,這圖我表示看不懂”。別急,“工欲善其事,必先利其器”,對于基本的概念我們得了解,方能壓住這“麒麟”神獸。

根據(jù)文章開頭提出的系列問題能了解到最基本的一點,Apache Kylin 能夠解決海量數(shù)據(jù)查詢的問題。既然要查詢,首先我們需要有個查詢的入口,Kylin 提供了標(biāo)準(zhǔn) SQL 作為對外查詢的接口,你只需要和平時寫 SQL 一樣進(jìn)行數(shù)據(jù)查詢即可,只是換了個查詢的平臺,不用關(guān)心如何解決查詢緩慢的問題,因為 Kylin 在背后幫你解決了這個問題。

“既然不用我了解如何做到海量數(shù)據(jù)亞秒級查詢的,那總得讓我明白怎么把需要查詢的數(shù)據(jù)給 Kylin 吧?”

這個好說,最早期 Kylin 只支持 Hive 作為數(shù)據(jù)源,數(shù)據(jù)源理解為你需要查詢的真實數(shù)據(jù),如今 Kylin 不僅支持 Kafka 消息流作為數(shù)據(jù)源,還支持 RDBMS 等數(shù)據(jù)源,比如 Mysql,下面拿 Hive 舉例講一講數(shù)據(jù)源。

Hive 本身的數(shù)據(jù)源不強(qiáng)制使用特定文件格式,你可以將 csv 文件作為 Hive 數(shù)據(jù)的來源,將 csv 文件中的數(shù)據(jù)存儲到 Hive 中,盡管此時你可以直接通過 Hive 查詢數(shù)據(jù),但面對海量數(shù)據(jù)效率是很低的。于是在 Kylin 中我們僅僅將 Hive 作為數(shù)據(jù)存儲的介質(zhì),提供數(shù)據(jù)的來源,隨后我們會采用一種叫預(yù)計算的方式將要查詢的數(shù)據(jù)存儲到 Cube 中,Kylin 查詢 Cube 中已經(jīng)計算好的數(shù)據(jù)。

以上對于整個 Kylin 有個最淺的認(rèn)識,一句話概括就是只要我們提供數(shù)據(jù)源,Kylin 便可以實現(xiàn)海量數(shù)據(jù)的秒級甚至亞秒級查詢,飛一般的感覺。那么它究竟是如何做到的呢?

原理理解

Kylin 能夠?qū)崿F(xiàn)亞秒級查詢,是運用了預(yù)計算這個概念,預(yù)計算字面上容易理解,就是提前計算好,查詢時直接返回結(jié)果。 對于分析師而言,明細(xì)數(shù)據(jù)基本上用不著,而是需要根據(jù)業(yè)務(wù)邏輯聚合之后的數(shù)據(jù)。比如商家需要根據(jù)某個月份商品的銷量調(diào)整庫存,此時只需要知道該月份該商品的總銷量即可,對于每天的銷量數(shù)據(jù)在這種業(yè)務(wù)場景下沒有利用價值。此時便可以利用預(yù)計算的思想,提前將該月份該商品總銷量計算出來,當(dāng)業(yè)務(wù)分析師進(jìn)行查詢時,直接返回計算好的結(jié)果即可,這就是預(yù)計算的簡單理解。

在 Kylin 中,應(yīng)對的不是這種簡單的計算場景,而是對海量數(shù)據(jù),多維度的計算。既然想要很好的利用預(yù)計算的思想,那如何讓 Kylin 提前準(zhǔn)備好這些計算后的結(jié)果呢?此時就需要建模,通過建模告訴 Kylin 你想要根據(jù)哪些維度計算出什么值。Kylin 根據(jù)定義好的模型,按照不同的維度組合進(jìn)行計算,相當(dāng)于業(yè)務(wù)分析師從數(shù)據(jù)的不同角度提出了不同的計算需求,Kylin 將各種維度組合計算出來,每一個計算結(jié)果保存成一個物化視圖,稱為 Cuboid,所有維度計算的結(jié)果組合在一起,也就是所有 Cuboid 作為一個整體,稱為 Cube。

根據(jù)各種維度聚合之后計算的值已經(jīng)存在 Cube 中了,那么分析師輸入的 SQL 便不需要查數(shù)據(jù)源中的數(shù)據(jù),直接查詢 Cube 中計算好的數(shù)據(jù)即可,當(dāng)然查詢 Cube 還需要將 SQL 查詢轉(zhuǎn)化成 Cube 的查詢,這個過程稱為后計算。此時可以再看一遍文章開頭的架構(gòu)圖,用戶輸入 SQL,通過各種 Rest API、JDBC/ODBC 接口,來到 Rest 服務(wù)層,再轉(zhuǎn)交給查詢引擎,查詢引擎會解析 SQL,生成基于關(guān)系表的邏輯執(zhí)行計劃,然后將其轉(zhuǎn)譯為基于 Cube 的物理執(zhí)行計劃,最后查詢預(yù)計算生成的 Cube 并產(chǎn)生結(jié)果。關(guān)系表是數(shù)據(jù)的輸入形式,符合星型模型或雪花模型,簡單理解為一種關(guān)系模型就好了。

你可能會產(chǎn)生疑問,一直提到 Cube,感覺它只是一個抽象的概念,那它到底存在哪呢?Cube 的存儲實際上由存儲引擎實現(xiàn),Kylin 將其存儲在 HBase 中。Kylin 將存儲引擎進(jìn)行了抽象,可以使用不同的存儲。就如上文提到的數(shù)據(jù)源,它也經(jīng)過抽象,可以實現(xiàn)不同的數(shù)據(jù)源,簡單類比為 Java 中的接口可以有不同的實現(xiàn)類。

從問題層層遞進(jìn)

“Cube 是如何生成的呢?”

上面我們提到 Cube 是 Cuboid 的組合,Cuboid 通過維度組合計算出來,重點是如何計算每個 Cuboid 維度組合的值,當(dāng)沒有值時,Cube 其實就相當(dāng)于是一個骨架,沒有肉。而 Cube 構(gòu)建引擎正是要填充這個骨架,也就是將數(shù)據(jù)源中的數(shù)據(jù)根據(jù)維度聚合,計算出相應(yīng)的值,填充到對應(yīng)的 Cuboid,Cube 采用分層構(gòu)建,先計算出 Base Cuboid,也就是包含所有維度的一個 Cuboid,再逐層構(gòu)建,最后存儲到 HBase 中。實際上 Cube 構(gòu)建遠(yuǎn)不止這么簡單,中間還有很多原理和細(xì)節(jié),并且 Cube 構(gòu)建算法不僅有逐層構(gòu)建,還有 Fast Cubing 算法,需要進(jìn)一步探索。

“如果我又新產(chǎn)生了數(shù)據(jù)該怎么辦呢?再重復(fù)上一次的過程,加載所有數(shù)據(jù)進(jìn)行 Cube 構(gòu)建嗎?”

Cube 有兩種構(gòu)建方式,全量構(gòu)建和增量構(gòu)建。從字面也比較容易理解,全量就是讀取數(shù)據(jù)源中的所有數(shù)據(jù),增量就是讀取數(shù)據(jù)源中相對于某一個時間段新產(chǎn)生的數(shù)據(jù),也就是每次構(gòu)建只讀取一個時間范圍的數(shù)據(jù)。這里又產(chǎn)生一個新的概念,叫 Segment,可以理解為根據(jù)時間段進(jìn)行構(gòu)建的小 Cube,Segment 可以根據(jù)配置以一定的方式進(jìn)行合并,比如每滿兩個月的數(shù)據(jù)進(jìn)行一次合并,全量構(gòu)建實際上就是只有一個 Segment,它沒有根據(jù)時間進(jìn)行分割。由此可以看出全量構(gòu)建通常適用于事實表的數(shù)據(jù)不隨時間增長或事實表的數(shù)據(jù)比較小、更新頻率很低的場景,只有在這樣的場景下,全量構(gòu)建才不會造成大開銷。而現(xiàn)實業(yè)務(wù)中,數(shù)據(jù)會不斷增加,增量構(gòu)建才是應(yīng)用最廣泛構(gòu)建方式。

“Cube 中一定能滿足我所有查詢的情況嗎?”

當(dāng)然不是,Cube 根據(jù)維度覆蓋到的是大多數(shù)查詢情況,但并非所有,所以對于 Cube 中查詢不到的時候,Kylin 也能夠查詢數(shù)據(jù)源中的數(shù)據(jù),這個概念就叫查詢下壓,此時查詢的速度會較慢。

“為什么預(yù)計算有這么明顯的優(yōu)勢,MPP 大規(guī)模并行處理和列式存儲難道不能解決這些問題嗎?”

還真不能。MPP 并行處理是通過增加物理資源,提高并行計算能力,在機(jī)器一定的情況下,當(dāng)數(shù)據(jù)量不斷增加,計算時間也將線性增加,為了計算海量數(shù)據(jù)不斷增加大量機(jī)器那我豈不是得虧本。列式存儲是將數(shù)據(jù)記錄按列存放,可以提高讀取的效率,但查詢性能與數(shù)據(jù)量呈線性相關(guān)仍然無法改變。通過上面的了解你已經(jīng)知道,Kylin 通過預(yù)計算的方式打破查詢時間隨數(shù)據(jù)量呈線性增長的規(guī)律,這就是 Kylin 關(guān)鍵的思想,通過預(yù)計算,用空間換時間。你或許聽過交互式分析這個詞,如果只有 MPP 和列式存儲,面對海量數(shù)據(jù)無法實現(xiàn)交互式,Kylin 則解決了這個問題,你再也不用點擊查詢再去喝一杯咖啡了。

通過以上對 Kylin 的基本了解,再看看文章開頭的圖,相信你會有新的理解。我們來總結(jié)一下對 Apache Kylin 的認(rèn)識,Kylin 通過預(yù)計算思想,利用大數(shù)據(jù)計算引擎,將數(shù)據(jù)源中的海量數(shù)據(jù)根據(jù)維度進(jìn)行計算,并按一定格式存儲到 HBase 中,解決海量數(shù)據(jù)場景下,查詢速度不隨數(shù)據(jù)量增長而線性增長的問題。

進(jìn)一步探索 Cube 構(gòu)建

接下來我們著重了解下 Cube 構(gòu)建的過程。通過以上內(nèi)容的學(xué)習(xí),我們了解到,Cube 構(gòu)建實際上是將 Hive 表中的數(shù)據(jù)轉(zhuǎn)化成 HBase 的存儲結(jié)構(gòu),也就是 Key-Value 鍵值對的形式,HBase 中存儲的數(shù)據(jù)是依據(jù)維度計算好的結(jié)果。下面學(xué)習(xí)下 Cube 構(gòu)建的詳細(xì)流程。

  1. Create Intermediate Flat Hive Table(創(chuàng)建大平表)

當(dāng)我們提交 Cube 構(gòu)建任務(wù)時,Kylin 會創(chuàng)建一張臨時的 Hive 平表,根據(jù)模型中設(shè)置的列,從 Hive 數(shù)據(jù)源中抽取出來并插入到平表中,后續(xù)的構(gòu)建就基于這張中間表進(jìn)行。通過 hive cli 查詢 hive 表可以看到,當(dāng)我們點擊構(gòu)建后,多了一張名為 kylin_intermediate 開頭的中間表。


hive 表

生成中間表
  1. Redistribute Flat Hive Table(重分布數(shù)據(jù))

在第一步會將抽出來形成中間表的數(shù)據(jù)存儲在 hdfs 上,這些數(shù)據(jù)文件大小不均勻,后續(xù) Map 任務(wù)處理時間長短會不一致,通過 Hive 的 INSERT INTO … DISTRIBUTE BY 重分布語句將處理數(shù)據(jù)的任務(wù)均衡。

重分布數(shù)據(jù)
  1. Extract Fact Table Distinct Columns(獲取不同維度的值)
    獲取不同維度的值

    獲取不同維度的值

    ? 這個步驟將啟動 MapReduce 任務(wù)對中間表中每個維度進(jìn)行抽取,用于下一步構(gòu)建維度字典。
  1. Build Dimension Dictionary(構(gòu)建維度字典)

通過上一步的維度值構(gòu)建字典,就是將維度值映射成編碼,可以節(jié)約存儲資源,HBase 的 RowKey 中就不需要使用維度值了,直接使用其對應(yīng)的編碼就可以。

  1. Save Cuboid Statistics(保存 Cuboid 數(shù)據(jù))

保存 Cuboid 的相關(guān)統(tǒng)計信息。

  1. Create HTable(創(chuàng)建 HTable)

創(chuàng)建 HBase 表用于保存之后要生成的 Cube 數(shù)據(jù)。

  1. Build Base Cuboid(構(gòu)建 Base Cuboid)

計算 Base Cuboid(所有維度的組合)

  1. Build N-Dimension Cuboid : level 1... N(構(gòu)建每一層 Cuboid)

計算除 Base Cuboid 之外的 Cuboid。

  1. Build Cube In-Mem

上面兩步是逐層構(gòu)建 Cuboid,這一步是 In-Mem 快速構(gòu)建,對內(nèi)存消耗較高,構(gòu)建速度更快,在具體執(zhí)行時會自動選擇一種構(gòu)建方式,沒被選中的方式會自動跳過。

通過查看任務(wù)詳情,我在本例中該步驟被跳過,使用的是逐層構(gòu)建的方式。


Build Cube In-Mem
  1. Convert Cuboid Data to HFile

HFile 是 HBase 持久化的存儲文件,也就是 HBase 存儲數(shù)據(jù)的文件形式。這一步會將構(gòu)建好的 Cuboid 轉(zhuǎn)換成 HFile。

  1. Load HFile to HBase Table

將 HFile 轉(zhuǎn)成 HBase 中的數(shù)據(jù)。

  1. Update Cube Info

更新 Cube 相關(guān)信息,此時 Cube 已經(jīng)準(zhǔn)備好,更改 Cube 狀態(tài)后可進(jìn)行查詢。

  1. Hive Cleanup

清理 Hive 中間表,通過查看 Hive 的表,發(fā)現(xiàn)第一步生成的中間表已經(jīng)被刪除了。


Hive Cleanup
  1. Garbage Collection on HBase

清理元數(shù)據(jù)信息中的臨時數(shù)據(jù),比如第 3 步中統(tǒng)計的不同維度,已經(jīng)構(gòu)建好字典了,這些數(shù)據(jù)不再需要使用,從 output 中可以看到刪除掉的相關(guān)元數(shù)據(jù)。


Garbage Collection

學(xué)習(xí) Cube 構(gòu)建過程的源碼

通過頁面點擊進(jìn)行構(gòu)建,查看請求 api,查看 Kylin 源碼,可以看到從前端頁面點擊構(gòu)建請求,最終調(diào)用了 CubeControllerrebuild 函數(shù),返回一個 JobInstance 實例

request

/**
 * Build/Rebuild a cube segment
 */
@RequestMapping(value = "/{cubeName}/rebuild", method = { RequestMethod.PUT }, produces = { "application/json" })
@ResponseBody
public JobInstance rebuild(@PathVariable String cubeName, @RequestBody JobBuildRequest req) {
    return buildInternal(cubeName, new TSRange(req.getStartTime(), req.getEndTime()), null, null, null,
            req.getBuildType(), req.isForce() || req.isForceMergeEmptySegment(), req.getPriorityOffset());
}

上一步僅僅是接收請求,獲取部分參數(shù)。

private JobInstance buildInternal(String cubeName, TSRange tsRange, SegmentRange segRange, //
        Map<Integer, Long> sourcePartitionOffsetStart, Map<Integer, Long> sourcePartitionOffsetEnd,
        String buildType, boolean force, Integer priorityOffset) {
    try {
       //Returns the name of this principal,獲取提交該任務(wù)的用戶
        String submitter = SecurityContextHolder.getContext().getAuthentication().getName();
        // 根據(jù) Cube 名稱獲取 Cube 實例
        CubeInstance cube = jobService.getCubeManager().getCube(cubeName);
        
        // 檢測 Cube 構(gòu)建的任務(wù)數(shù)量,默認(rèn)是 10 個,如果超過了則拋出異常,最終調(diào)用了以下配置獲得最大構(gòu)建任務(wù)數(shù)
        //public int getMaxBuildingSegments() {
        //  return Integer.parseInt(getOptional("kylin.cube.max-building-segments", "10"));
    //    }
        checkBuildingSegment(cube);
        
        //提交 Cube 構(gòu)建任務(wù)
        return jobService.submitJob(cube, tsRange, segRange, sourcePartitionOffsetStart, sourcePartitionOffsetEnd,
                CubeBuildTypeEnum.valueOf(buildType), force, submitter, priorityOffset);
    } catch (Throwable e) {
        logger.error(e.getLocalizedMessage(), e);
        throw new InternalErrorException(e.getLocalizedMessage(), e);
    }
}

查看 submitJob 方法。

public JobInstance submitJob(CubeInstance cube, TSRange tsRange, SegmentRange segRange, //
        Map<Integer, Long> sourcePartitionOffsetStart, Map<Integer, Long> sourcePartitionOffsetEnd,
        CubeBuildTypeEnum buildType, boolean force, String submitter, Integer priorityOffset) throws IOException {
    //通過 Cube 實例進(jìn)一步獲取 Project 實例,通過封裝好的權(quán)限檢查工具 AclUtil 進(jìn)行權(quán)限檢查。
    aclEvaluate.checkProjectOperationPermission(cube);
    // 提交任務(wù)
    JobInstance jobInstance = submitJobInternal(cube, tsRange, segRange, sourcePartitionOffsetStart,
            sourcePartitionOffsetEnd, buildType, force, submitter, priorityOffset);

    return jobInstance;
}

查看 submitJobInternal 方法。

public JobInstance submitJobInternal(CubeInstance cube, TSRange tsRange, SegmentRange segRange, //
        Map<Integer, Long> sourcePartitionOffsetStart, Map<Integer, Long> sourcePartitionOffsetEnd, //
        CubeBuildTypeEnum buildType, boolean force, String submitter, Integer priorityOffset) throws IOException {
    Message msg = MsgPicker.getMsg();

    // 檢測 Cube 的狀態(tài)
    if (cube.getStatus() == RealizationStatusEnum.DESCBROKEN) {
        throw new BadRequestException(String.format(Locale.ROOT, msg.getBUILD_BROKEN_CUBE(), cube.getName()));
    }

    // 根據(jù) Cube 的元信息,進(jìn)行簽名校驗,將已有簽名與計算出來的進(jìn)行比較,判斷 Cube 信息的正確性
    checkCubeDescSignature(cube);
    // 檢測 Cube 是否符合構(gòu)建狀態(tài),進(jìn)一步查看源碼可知如果已經(jīng)有處于 pending 狀態(tài)的 segment,則該 Cube 不能進(jìn)行構(gòu)建,拋出異常
    checkAllowBuilding(cube);
    // 檢測是否能并行構(gòu)建
    if (buildType == CubeBuildTypeEnum.BUILD || buildType == CubeBuildTypeEnum.REFRESH) {
        checkAllowParallelBuilding(cube);
    }

    DefaultChainedExecutable job;

    CubeSegment newSeg = null;
    try {
        if (buildType == CubeBuildTypeEnum.BUILD) {
            // 獲取數(shù)據(jù)源類型,目前支持 Hive,JDBC,Kafka
            ISource source = SourceManager.getSource(cube);
            // 獲得構(gòu)建范圍
            SourcePartition src = new SourcePartition(tsRange, segRange, sourcePartitionOffsetStart,
                    sourcePartitionOffsetEnd);
            // 在此填充數(shù)據(jù),個人理解為對于實時數(shù)據(jù),在執(zhí)行完前面的步驟后又產(chǎn)生了時間間隔,將這部分?jǐn)?shù)據(jù)加載進(jìn)來,并添加一個 segment
            src = source.enrichSourcePartitionBeforeBuild(cube, src);
            newSeg = getCubeManager().appendSegment(cube, src);
            // 通過構(gòu)建引擎生成新的 job 任務(wù)
            job = EngineFactory.createBatchCubingJob(newSeg, submitter, priorityOffset);
        } else if (buildType == CubeBuildTypeEnum.MERGE) {
            newSeg = getCubeManager().mergeSegments(cube, tsRange, segRange, force);
            job = EngineFactory.createBatchMergeJob(newSeg, submitter);
        } else if (buildType == CubeBuildTypeEnum.REFRESH) {
            newSeg = getCubeManager().refreshSegment(cube, tsRange, segRange);
            job = EngineFactory.createBatchCubingJob(newSeg, submitter, priorityOffset);
        } else {
            throw new BadRequestException(String.format(Locale.ROOT, msg.getINVALID_BUILD_TYPE(), buildType));
        }

        // 將任務(wù)添加到任務(wù)調(diào)度系統(tǒng)
        getExecutableManager().addJob(job);

    } catch (Exception e) {
        if (newSeg != null) {
            logger.error("Job submission might failed for NEW segment {}, will clean the NEW segment from cube",
                    newSeg.getName());
            try {
                // Remove this segment
                getCubeManager().updateCubeDropSegments(cube, newSeg);
            } catch (Exception ee) {
                // swallow the exception
                logger.error("Clean New segment failed, ignoring it", e);
            }
        }
        throw e;
    }

    // 返回任務(wù)實例信息
    JobInstance jobInstance = getSingleJobInstance(job);

    return jobInstance;
}

查看創(chuàng)建 job 任務(wù) createBatchCubingJob 方法,最終會根據(jù)構(gòu)建引擎,創(chuàng)建相應(yīng)的 builder 實例并調(diào)用 build 方法。

// 該方法用于返回構(gòu)建引擎,根據(jù)下圖中可看出支持 MR 和 Spark
public static IBatchCubingEngine batchEngine(IEngineAware aware) {
        ImplementationSwitch<IBatchCubingEngine> current = engines.get();
        if (current == null) {
            current = new ImplementationSwitch<>(KylinConfig.getInstanceFromEnv().getJobEngines(),
                    IBatchCubingEngine.class);
            engines.set(current);
        }
        return current.get(aware.getEngineType());
}

// 這里是先根據(jù)配置獲得構(gòu)建引擎,創(chuàng)建對應(yīng)的 builder 實例并調(diào)用 build 方法。
public static DefaultChainedExecutable createBatchCubingJob(CubeSegment newSegment, String submitter, Integer priorityOffset) {
    return batchEngine(newSegment).createBatchCubingJob(newSegment, submitter, priorityOffset);
}
implementation

先來看看 MR 構(gòu)建引擎的 build 方法

public CubingJob build() {
    logger.info("MR_V2 new job to BUILD segment " + seg);
    
    // 獲得一個初始化的 Job 實例
    final CubingJob result = CubingJob.createBuildJob(seg, submitter, config);
    final String jobId = result.getId();
    // 獲取 cuboid 的數(shù)據(jù)路徑,以配置的 working-dir 開頭
    final String cuboidRootPath = getCuboidRootPath(jobId);

    // Phase 1: Create Flat Table & Materialize Hive View in Lookup Tables
    inputSide.addStepPhase1_CreateFlatTable(result);

    // Phase 2: Build Dictionary
    result.addTask(createFactDistinctColumnsStep(jobId));

    // 判斷是否是高基維(UHC),如果是則添加新的任務(wù)對高基維進(jìn)行處理
    if (isEnableUHCDictStep()) {
        result.addTask(createBuildUHCDictStep(jobId));
    }

    // 構(gòu)建字典
    result.addTask(createBuildDictionaryStep(jobId));
    
    // 保存 cuboid 統(tǒng)計數(shù)據(jù)
    result.addTask(createSaveStatisticsStep(jobId));

    // add materialize lookup tables if needed
    LookupMaterializeContext lookupMaterializeContext = addMaterializeLookupTableSteps(result);

    // 創(chuàng)建 HTable
    outputSide.addStepPhase2_BuildDictionary(result);
        
    if (seg.getCubeDesc().isShrunkenDictFromGlobalEnabled()) {
        result.addTask(createExtractDictionaryFromGlobalJob(jobId));
    }

    // Phase 3: Build Cube
    addLayerCubingSteps(result, jobId, cuboidRootPath); // layer cubing, only selected algorithm will execute
    addInMemCubingSteps(result, jobId, cuboidRootPath); // inmem cubing, only selected algorithm will execute
    outputSide.addStepPhase3_BuildCube(result);

    // Phase 4: Update Metadata & Cleanup
    result.addTask(createUpdateCubeInfoAfterBuildStep(jobId, lookupMaterializeContext));
    inputSide.addStepPhase4_Cleanup(result);
    outputSide.addStepPhase4_Cleanup(result);
    
    // Set the task priority if specified
    result.setPriorityBasedOnPriorityOffset(priorityOffset);
    result.getTasks().forEach(task -> task.setPriorityBasedOnPriorityOffset(priorityOffset));

    return result;
}

從源碼中可以看到,該流程與頁面上 Cube 構(gòu)建的任務(wù)流程基本一致,并且在 Phase 3:Build Cube 處有兩種構(gòu)建算法,只有被選中的算法才會被最終執(zhí)行。

再看看 Spark 的 build 方法實現(xiàn),Spark 的 build 方法流程和 MR 的基本上一致,不再做注釋,但構(gòu)建算法處有一些不同,Spark 構(gòu)建引擎中只使用了分層構(gòu)建算法,至于這兩種算法的具體原理以及在兩種構(gòu)建引擎中選用的區(qū)別,將在之后的文章中做進(jìn)一步探討。

public CubingJob build() {
    logger.info("Spark new job to BUILD segment " + seg);

    final CubingJob result = CubingJob.createBuildJob(seg, submitter, config);
    final String jobId = result.getId();
    final String cuboidRootPath = getCuboidRootPath(jobId);

    // Phase 1: Create Flat Table & Materialize Hive View in Lookup Tables
    inputSide.addStepPhase1_CreateFlatTable(result);

    // Phase 2: Build Dictionary
    KylinConfig config = KylinConfig.getInstanceFromEnv();
    if (config.isSparkFactDistinctEnable()) {
        result.addTask(createFactDistinctColumnsSparkStep(jobId));
    } else {
        result.addTask(createFactDistinctColumnsStep(jobId));
    }

    if (isEnableUHCDictStep()) {
        result.addTask(createBuildUHCDictStep(jobId));
    }

    result.addTask(createBuildDictionaryStep(jobId));
    result.addTask(createSaveStatisticsStep(jobId));

    // add materialize lookup tables if needed
    LookupMaterializeContext lookupMaterializeContext = addMaterializeLookupTableSteps(result);

    outputSide.addStepPhase2_BuildDictionary(result);

    // Phase 3: Build Cube
    addLayerCubingSteps(result, jobId, cuboidRootPath); // layer cubing, only selected algorithm will execute
    outputSide.addStepPhase3_BuildCube(result);

    // Phase 4: Update Metadata & Cleanup
    result.addTask(createUpdateCubeInfoAfterBuildStep(jobId, lookupMaterializeContext));
    inputSide.addStepPhase4_Cleanup(result);
    outputSide.addStepPhase4_Cleanup(result);

    // Set the task priority if specified
    result.setPriorityBasedOnPriorityOffset(priorityOffset);
    result.getTasks().forEach(task -> task.setPriorityBasedOnPriorityOffset(priorityOffset));

    return result;
}

本文從 Apache Kylin 最基本的認(rèn)識到源碼分析,一步步探索了 Kylin 關(guān)鍵部分的流程,但是本文還處于比較表層的理解,還有非常多可以深入學(xué)習(xí)的點以及算法有待進(jìn)一步學(xué)習(xí)。

文章內(nèi)容可能不嚴(yán)謹(jǐn)或有錯誤,歡迎指出。

參考來源:

《Apache Kylin 權(quán)威指南》

https://kyligence.io/zh/blog/optimize-kylin-cube-build-performance/

https://blog.csdn.net/zengrui_ops/article/details/85858860

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • “麒麟出沒,必有祥瑞?!薄?中國古諺語 前言 隨著移動互聯(lián)網(wǎng)、物聯(lián)網(wǎng)等技術(shù)的發(fā)展,近些年人類所積累的數(shù)據(jù)正在呈爆...
    柴詩雨閱讀 32,831評論 12 86
  • 1. Apache Kylin 是什么? Apache Kylin?是一個開源的分布式分析引擎,提供Hadoop之...
    ZanderXu閱讀 19,906評論 0 22
  • KYLIN是什么? 上古神獸,直接上圖 - 可擴(kuò)展超快OLAP引擎: Kylin是為減少在Hadoop上百億規(guī)模數(shù)...
    老鯧魚閱讀 2,602評論 0 3
  • 總目錄 Kylin系列(一)—— 入門Kylin系列(二)—— Cube 構(gòu)造算法 [TOC] 因為平常只會使用k...
    vernwang閱讀 5,348評論 0 50
  • 派出所院子里跑進(jìn)來一只小豬,把民警種的菜給吃了,所長讓人把小豬抓住先關(guān)起來,再找豬的主人,把吃了的菜給賠了,可找了...
    雨木目_b4b0閱讀 140評論 0 0