本文主要介绍了Apache Kylin是如何将Hive表中的数据转化为HBase的KV结构,并简单介绍了Kylin的SQL查询是如何转化为HBase的Scan操作。
Apache Kylin 是什么
Apache Kylin是一个开源的、基于Hadoop生态系统的OLAP引擎(OLAP查询引擎、OLAP多维分析引擎),能够通过SQL接口对十亿、甚至百亿行的超大数据集实现秒级的多维分析查询。
Apache Kylin 核心:Kylin OLAP引擎基础框架,包括元数据引擎,查询引擎,Job(Build)引擎及存储引擎等,同时包括REST服务器以响应客户端请求。
OLAP 是什么
即联机分析处理:以复杂的分析型查询为主,需要扫描,聚合大量数据。
Kylin如何实现超大数据集的秒级多维分析查询
预计算
对于超大数据集的复杂查询,既然现场计算需要花费较长时间,那么根据空间换时间的原理,我们就可以提前将所有可能的计算结果计算并存储下来,从而实现超大数据集的秒级多维分析查询。
Kylin的预计算是如何实现的
将数据源Hive表中的数据按照指定的维度和指标 由计算引擎MapReduce离线计算出所有可能的查询结果(即Cube)存储到HBase中。
Cube 和 Cuboid是什么
简单地说,一个cube就是一个Hive表的数据按照指定维度与指标计算出的所有组合结果。
其中每一种维度组合称为cuboid,一个cuboid包含一种具体维度组合下所有指标的值。
如下图,整个立方体称为1个cube,立方体中每个网格点称为1个cuboid,图中(A,B,C,D)和(A,D)都是cuboid,特别的,(A,B,C,D)称为Base cuboid。cube的计算过程是逐层计算的,首先计算Base cuboid,然后计算维度数依次减少,逐层向下计算每层的cuboid。
图1
Build引擎Cube构建流程
BatchCubingJobBuilder2.build方法逻辑如下:
public CubingJob build() {
logger.info("MR_V2 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
// 根据事实表和维表抽取需要的维度和度量,创建一张宽表或平表,并且进行文件再分配(执行Hive命令行来完成操作)
inputSide.addStepPhase1_CreateFlatTable(result);
// Phase 2: Build Dictionary
// 创建字典由三个子任务完成,由MR引擎完成,分别是抽取维度值(包含抽样统计)、创建维度字典和保存统计信息
result.addTask(createFactDistinctColumnsStep(jobId));
result.addTask(createBuildDictionaryStep(jobId));
result.addTask(createSaveStatisticsStep(jobId));
// add materialize lookup tables if needed
LookupMaterializeContext lookupMaterializeContext = addMaterializeLookupTableSteps(result);
// 创建HTable
outputSide.addStepPhase2_BuildDictionary(result);
// Phase 3: Build Cube
// 构建Cube,包含两种Cube构建算法,分别是逐层算法和快速算法,在执行时会根据源数据的统计信息自动选择一种算法(各个Mapper的小Cube的行数之和 / reduce后的Cube行数 > 7,重复度高就选逐层算法,重复度低就选快速算法)
addLayerCubingSteps(result, jobId, cuboidRootPath); // layer cubing, only selected algorithm will execute
addInMemCubingSteps(result, jobId, cuboidRootPath); // inmem cubing, only selected algorithm will execute
// 构建HFile文件及把HFile文件BulkLoad到HBase
outputSide.addStepPhase3_BuildCube(result);
// Phase 4: Update Metadata & Cleanup
// 更新Cube元数据,其中需要更新的包括cube是否可用、以及本次构建的数据统计,包括构建完成的时间,输入的record数目,输入数据的大小,保存到Hbase中数据的大小等,并将这些信息持久到元数据库中
// 以及清理临时数据,是在整个执行过程中产生了很多的垃圾文件,其中包括:1、临时的hive表,2、因为hive表是一个外部表,存储该表的文件也需要额外删除,3、fact distinct 这一步将数据写入到HDFS上为建立词典做准备,这时候也可以删除了,4、rowKey统计的时候会生成一个文件,此时可以删除。
result.addTask(createUpdateCubeInfoAfterBuildStep(jobId, lookupMaterializeContext));
inputSide.addStepPhase4_Cleanup(result);
outputSide.addStepPhase4_Cleanup(result);
return result;
}
一、 根据事实表和维表抽取需要的维度和度量,创建一张宽表或平表,并且进行文件再分配
1.1 生成Hive宽表或平表(Create Intermediate Flat Hive Table)(执行Hive命令行)
这一步的操作是根据cube的定义生成原始数据,这里会新创建一个hive外部表,然后再根据cube中定义的星状模型,查询出维度(对于DERIVED类型的维度使用的是外键列)和度量的值插入到新创建的表中,这个表是一个外部表,表的数据文件(存储在HDFS)作为下一个子任务的输入,它首先根据维度中的列和度量中作为参数的列得到需要出现在该表中的列,然后执行三步hive操作,这三步hive操作是通过hive -e的方式执行的shell命令。
1. drop TABLE IF EXISTS xxx
2. CREATE EXTERNAL TABLE IF NOT EXISTS xxx() ROW FORMAT DELIMITED FIELDS TERMINATED BY '\177' STORED AS SEQUENCEFILE LOCATION xxxx,其中表名是根据当前的cube名和segment的uuid生成的,location是当前job的临时文件,只有当insert插入数据的时候才会创建,注意这里每一行的分隔符指定的是'\177'(目前是写死的,十进制为127)。
3. 插入数据,在执行之前需要首先设置一些配置项,这些配置项通过hive的SET命令设置,是根据这个cube的job的配置文件(一般是在kylin的conf目录下)设置的,最后执行的是INSERT OVERWRITE TABLE xxx SELECT xxxx语句,SELECT子句中选出cube星状模型中事实表与维度表按照设置的方式join之后的出现在维度或者度量参数中的列(特殊处理derived列),然后再加上用户设置的where条件和partition的时间条件(根据输入build的参数)。
需要注意的是这里无论用户设置了多少维度和度量,每次join都会使用事实表和所有的维度表进行join,这可能造成不必要的性能损失(多一个join会影响hive性能,毕竟要多读一些文件)。这一步执行完成之后location指定的目录下就有了原始数据的文件,为接下来的任务提供了输入。
JoinedFlatTable.generateDropTableStatement(flatDesc);
JoinedFlatTable.generateCreateTableStatement(flatDesc, jobWorkingDir);
JoinedFlatTable.generateInsertDataStatement(flatDesc);
二、 提取纬度值、创建维度字典和保存统计信息
2.1 提取事实表维度去重值(Extract Fact Table Distinct Columns)(执行一个MapReduce任务,包含抽取纬度值及统计各Mapper间的重复度两种任务)
在这一步是根据上一步生成的hive表计算出还表中的每一个出现在事实表中的维度的distinct值,并写入到文件中,它是启动一个MR任务完成的,MR任务的输入是HCatInputFormat,它关联的表就是上一步创建的临时表,这个MR任务的map阶段首先在setup函数中得到所有维度中出现在事实表的维度列在临时表的index,根据每一个index得到该列在临时表中在每一行的值value,然后将<index+value,EMPTY_TEXT>作为mapper的输出,通过index决定由哪个Reduce处理(而Reduce启动的时候根据ReduceTaskID如0000,0001来初始化决定处理哪个index对应的维度列),该任务还启动了一个combiner,它所做的只是对同一个key(维度值)进行去重(同一个mapper的结果),reducer所做的事情也是进行key(维度值)去重(所有mapper的结果),然后在Reduce中将该维度列去重后的维度值一行行的写入到以列名命名的文件中(注意kylin实现的方式,聚合的key是纬度值,而不是index)。
提取事实表维度列的唯一值是通过FactDistinctColumnsJob这个MapReduce来完成,核心思想是每个Reduce处理一个维度列,然后每个维度列Reduce单独输出该维度列对应的去重后的数据文件(output written to baseDir/colName/-r-00000,baseDir/colName2/-r-00001 or 直接输出字典 output written to baseDir/colName/colName.rldict-r-00000)。另外会输出各Mapper间重复度统计文件(output written to baseDir/statistics/statistics-r-00000,baseDir/statistics/statistics-r-00001)
FactDistinctColumnsJob
FactDistinctColumnsMapper
FactDistinctColumnPartitioner
FactDistinctColumnsCombiner
FactDistinctColumnsReducer
org.apache.kylin.engine.mr.steps.FactDistinctColumnsMapper
org.apache.kylin.engine.mr.steps.FactDistinctColumnsReducer
在FactDistinctColumnsMapper中输出维度值或通过HHL近似算法统计每个Mapper中各个CuboID的去重行数
public void doMap(KEYIN key, Object record, Context context) throws IOException, InterruptedException {
Collection<String[]> rowCollection = flatTableInputFormat.parseMapperInput(record);
for (String[] row : rowCollection) {
context.getCounter(RawDataCounter.BYTES).increment(countSizeInBytes(row));
for (int i = 0; i < allCols.size(); i++) {
String fieldValue = row[columnIndex[i]];
if (fieldValue == null)
continue;
final DataType type = allCols.get(i).getType();
if (dictColDeduper.isDictCol(i)) {
if (dictColDeduper.add(i, fieldValue)) {
// 输出维度值,KEY=COLUMN_INDEX+COLUME_VALUE,VALUE=EMPTY_TEXT
writeFieldValue(context, type, i, fieldValue);
}
} else {
DimensionRangeInfo old = dimensionRangeInfoMap.get(i);
if (old == null) {
old = new DimensionRangeInfo(fieldValue, fieldValue);
dimensionRangeInfoMap.put(i, old);
} else {
old.setMax(type.getOrder().max(old.getMax(), fieldValue));
old.setMin(type.getOrder().min(old.getMin(), fieldValue));
}
}
}
// 抽样统计,KEY=CUBOID,VALUE=HLLCount
if (rowCount % 100 < samplingPercentage) {
putRowKeyToHLL(row);
}
if (rowCount % 100 == 0) {
dictColDeduper.resetIfShortOfMem();
}
rowCount++;
}
}
protected void doCleanup(Context context) throws IOException, InterruptedException {
ByteBuffer hllBuf = ByteBuffer.allocate(Buffer