用戶視角
你是否面對海量數(shù)據(jù)查詢緩慢而備受煎熬 ?受限于查詢速度而只能屈服于數(shù)據(jù)量?想在同一個分析工具下分析不同來源的數(shù)據(jù)?想在不同的 BI 工具下仍具有敏捷的查詢響應(yīng)速度?在 Apache Kylin 面前這些都不是問題,一圖勝千言。
好了,你可能想說:“前面的問題你倒是快給我解決啊,這圖我表示看不懂”。別急,“工欲善其事,必先利其器”,對于基本的概念我們得了解,方能壓住這“麒麟”神獸。
根據(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ì)流程。
- 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 開頭的中間表。
- Redistribute Flat Hive Table(重分布數(shù)據(jù))
在第一步會將抽出來形成中間表的數(shù)據(jù)存儲在 hdfs 上,這些數(shù)據(jù)文件大小不均勻,后續(xù) Map 任務(wù)處理時間長短會不一致,通過 Hive 的 INSERT INTO … DISTRIBUTE BY 重分布語句將處理數(shù)據(jù)的任務(wù)均衡。
-
Extract Fact Table Distinct Columns(獲取不同維度的值)
獲取不同維度的值
獲取不同維度的值
? 這個步驟將啟動 MapReduce 任務(wù)對中間表中每個維度進(jìn)行抽取,用于下一步構(gòu)建維度字典。
- Build Dimension Dictionary(構(gòu)建維度字典)
通過上一步的維度值構(gòu)建字典,就是將維度值映射成編碼,可以節(jié)約存儲資源,HBase 的 RowKey 中就不需要使用維度值了,直接使用其對應(yīng)的編碼就可以。
- Save Cuboid Statistics(保存 Cuboid 數(shù)據(jù))
保存 Cuboid 的相關(guān)統(tǒng)計信息。
- Create HTable(創(chuàng)建 HTable)
創(chuàng)建 HBase 表用于保存之后要生成的 Cube 數(shù)據(jù)。
- Build Base Cuboid(構(gòu)建 Base Cuboid)
計算 Base Cuboid(所有維度的組合)
- Build N-Dimension Cuboid : level 1... N(構(gòu)建每一層 Cuboid)
計算除 Base Cuboid 之外的 Cuboid。
- Build Cube In-Mem
上面兩步是逐層構(gòu)建 Cuboid,這一步是 In-Mem 快速構(gòu)建,對內(nèi)存消耗較高,構(gòu)建速度更快,在具體執(zhí)行時會自動選擇一種構(gòu)建方式,沒被選中的方式會自動跳過。
通過查看任務(wù)詳情,我在本例中該步驟被跳過,使用的是逐層構(gòu)建的方式。
- Convert Cuboid Data to HFile
HFile 是 HBase 持久化的存儲文件,也就是 HBase 存儲數(shù)據(jù)的文件形式。這一步會將構(gòu)建好的 Cuboid 轉(zhuǎn)換成 HFile。
- Load HFile to HBase Table
將 HFile 轉(zhuǎn)成 HBase 中的數(shù)據(jù)。
- Update Cube Info
更新 Cube 相關(guān)信息,此時 Cube 已經(jīng)準(zhǔn)備好,更改 Cube 狀態(tài)后可進(jìn)行查詢。
- Hive Cleanup
清理 Hive 中間表,通過查看 Hive 的表,發(fā)現(xiàn)第一步生成的中間表已經(jīng)被刪除了。
- Garbage Collection on HBase
清理元數(shù)據(jù)信息中的臨時數(shù)據(jù),比如第 3 步中統(tǒng)計的不同維度,已經(jīng)構(gòu)建好字典了,這些數(shù)據(jù)不再需要使用,從 output 中可以看到刪除掉的相關(guān)元數(shù)據(jù)。
學(xué)習(xí) Cube 構(gòu)建過程的源碼
通過頁面點擊進(jìn)行構(gòu)建,查看請求 api,查看 Kylin 源碼,可以看到從前端頁面點擊構(gòu)建請求,最終調(diào)用了 CubeController
的 rebuild
函數(shù),返回一個 JobInstance
實例
/**
* 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);
}
先來看看 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/