Kylin源码分析系列二—Cube构建

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/zengrui_ops/article/details/85858860

Kylin源码分析系列二—Cube构建

注:Kylin源码分析系列基于Kylin的2.5.0版本的源码,其他版本可以类比。

1.构建流程

前面一篇文章介绍了Kylin中的任务调度服务,本篇文章正式介绍Kylin的核心内容Cube,主要讲述Cube构建的过程。下面的构建过程选择使用spark构建引擎来说明(MR引擎自行类比阅读相关源码)。

首先介绍下Cube构建的整体流程,看下kylin web页面上展示的构建过程:

 

主要有如下几个步骤:

  1. 首先创建一个大平表(Flat Hive Table),该表的数据是将创建cube涉及到的维度从原有的事实表和维度表中查询出来组成一条完整的数据插入到一个新的hive表中;后续的cube构建就是基于这个表的。抽取数据的过程使用的是Hive命令,Kylin使用conf/kylin_hive_conf.xml配置文件中的配置项,用户可以根据需求修改和添加相关配置项。
  2. 经过第一步后,Hive会在HDFS目录下生成一些数据文件,这些数据文件可能大小不一,这就会导致后续的任务执行不均衡,有些任务执行很快,有些可能会很慢。为了是这些数据分布更均匀,Kylin增加了该步骤来重新分配各个数据文件中的数据。执行如下hive命令:

        

     3. 接着Kylin获取维度列的distinct值(即维度基数),用于后面一步进行字典编码。

     4. 这一步就根据前面获得的维度的distinct值来构建字典,通常这一步会很快,但是如果distinct值的集合很大,Kylin可能会报           错,例如,“Too high cardinality is not suitable for dictionary”。对于UHC(超大维度基数)列,请使用其他编码方式,例             如“fixed_length”“integer”等。

     5. 这步操作很简单,只是保存cube的一些相关统计数据,比如有多少cuboid,每个cuboid有多少行数据等。

     6. 这一步是创建保存cube数据的hbase表,目前的版本cube数据只支持保存到hbase中,kylin社区目前正在开发将cube数据            直接保存为parquet格式文件(适用于云上环境);这里有一点需要说明一下,在建表的时候启用了hbase协处理的功能              (endpoint模式),需要将协处理器的相关jardeploy到对应的hbase表上,后面会详细介绍,这样做是为了提升Kylin的查             询性能。

     7. 这里就是真正的创建cube了,本文的描述是基于spark构建引擎的,使用的by layer的方式构建的,即先构建Base                         Cuboid,然后一层一层的往上聚合,得到其他的cuboid的数据;当使用MR引擎的时候,可以配置cube构建算法,通过                 kylin.cube.algorithm来配置,值有[“auto”, “layer”, “inmem”],默认值为auto,用户根据环境的资源情况来进行配置,使用             auto的时候,kylin会根据系统资源情况来选择layer还是inmemlayer算法是一层一层的计算,需要的资源较少,但是花费           的时间可能会更长,而使用inmem算法则构建的更快,但是会消耗更多的内存,具体可以参考                                                       https://blog.csdn.net/sunnyyoona/article/details/52318176

     8. 这一步将Cuboid数据转化为HFile文件。

     9. 将转化后的HFile文件直接loadHBase里面供后续查询使用。

   10. 更新Cube的相关信息。

   11. 清理Hive中的临时数据。

2.源码分析

下面从源码来看Cube的构建过程:

Kylin页面上点击build后,触发的是一个任务提交的流程,该任务提交的流程简要介绍下:

1.页面点击Submit按钮,通过js触发rebuild事件,发送restful请求:

 

rebuild的具体处理源码在webapp/app/js/controllers/cubes.js中:

最终调用restful api接口/kylin/api/cubes/{cubeName}/rebuild将请求发送至服务端,CubeService定义在webapp/app/js/services/cubes.js

2.Rest Server服务端接收到restful请求,根据请求的URL将请求分发到对应的控制器进行处理(使用了Spring@Controller@RequestMapping注解),这里的Cube构建请求最终被分发到CubeController控制器由rebuild函数进行处理:

/** Build/Rebuild a cube segment */
/**
 * 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());
}

然后看buildInternal函数:

private JobInstance buildInternal(String cubeName, TSRange tsRange, SegmentRange segRange, //
        Map<Integer, Long> sourcePartitionOffsetStart, Map<Integer, Long> sourcePartitionOffsetEnd,
        String buildType, boolean force) {
    try {
        //获取提交任务的用户的用户名
        String submitter = SecurityContextHolder.getContext().getAuthentication().getName();
        //获取Cube实例
        CubeInstance cube = jobService.getCubeManager().getCube(cubeName);
        //检测有多少个处于即将构建的状态的job,默认只能同时提10个job,大于则会抛异常,提交失败
        checkBuildingSegment(cube);
        //通过jobService来提交任务,即为上篇文章介绍的Cube任务调度服务
        return jobService.submitJob(cube, tsRange, segRange, sourcePartitionOffsetStart, sourcePartitionOffsetEnd,
                CubeBuildTypeEnum.valueOf(buildType), force, submitter);
    } catch (Throwable e) {
        logger.error(e.getLocalizedMessage(), e);
        throw new InternalErrorException(e.getLocalizedMessage(), e);
    }
}

然后看JobService中的submitJob,该函数只是做了权限认证,然后直接调用了submitJobInternal

public JobInstance submitJobInternal(CubeInstance cube, TSRange tsRange, SegmentRange segRange, //
        Map<Integer, Long> sourcePartitionOffsetStart, Map<Integer, Long> sourcePartitionOffsetEnd, //
        CubeBuildTypeEnum buildType, boolean force, String submitter) throws IOException {
. . .
        try {
        if (buildType == CubeBuildTypeEnum.BUILD) {
            //获取数据源类型(HiveSource、JdbcSource、KafkaSource)
            ISource source = SourceManager.getSource(cube);
            //数据范围
            SourcePartition src = new SourcePartition(tsRange, segRange, sourcePartitionOffsetStart,
                    sourcePartitionOffsetEnd);
            //kafka数据源确定start offset和endoffset
            src = source.enrichSourcePartitionBeforeBuild(cube, src);
            //添加segment
            newSeg = getCubeManager().appendSegment(cube, src);
            //通过构建引擎来构建Job
            job = EngineFactory.createBatchCubingJob(newSeg, submitter);
        } 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);
        } else {
            throw new BadRequestException(String.format(msg.getINVALID_BUILD_TYPE(), buildType));
        }
        //提交任务,可以参考前面任务调度的文章了解任务具体是怎么执行的
        getExecutableManager().addJob(job);
    } catch (Exception e) {
      . . . 
    }
    JobInstance jobInstance = getSingleJobInstance(job);
    return jobInstance;
}

接着看EngineFactory.createBatchCubingJob方法,根据cube实例中配置的引擎类型来确定使用什么引擎,目前有mapreducespark两种引擎,开发者也可以添加自己的构建引擎(通过kylin.engine.provider加入)。下面以spark引擎来继续分析,后面直接到SparkBatchCubingJobBuilder2build,这个函数就是cube构建任务的核心:

public CubingJob build() {
    logger.info("Spark new job to BUILD segment " + seg);
    //构建job任务(DefaultChainedExecutable类型,是一个任务链)
    final CubingJob result = CubingJob.createBuildJob(seg, submitter, config);
    final String jobId = result.getId();
    //获取cuboid在hdfs上的数据目录
    final String cuboidRootPath = getCuboidRootPath(jobId);
    // Phase 1: Create Flat Table & Materialize Hive View in Lookup Tables
    inputSide.addStepPhase1_CreateFlatTable(result);
    // Phase 2: Build Dictionary
    // 获取维度列的distinct值(即维度基数)
    result.addTask(createFactDistinctColumnsSparkStep(jobId));
    // 针对高基数维度(Ultra High Cardinality)单独起MR任务来构建字典,主要是ShardByColumns
    // 和GlobalDictionaryColumns
    if (isEnableUHCDictStep()) {
        result.addTask(createBuildUHCDictStep(jobId));
    }
    // 创建维度字典
    result.addTask(createBuildDictionaryStep(jobId));
    // 保存一些统计数据
    result.addTask(createSaveStatisticsStep(jobId));
    // add materialize lookup tables if needed
    LookupMaterializeContext lookupMaterializeContext = addMaterializeLookupTableSteps(result);
    // 创建hbase表
    outputSide.addStepPhase2_BuildDictionary(result);
    // Phase 3: Build Cube
    addLayerCubingSteps(result, jobId, cuboidRootPath); // layer cubing, only selected algorithm will execute
    //将上一步计算后的cuboid文件转换成hfile,然后将hfile load到hbase的表中
    outputSide.addStepPhase3_BuildCube(result);
    // Phase 4: Update Metadata & Cleanup
    result.addTask(createUpdateCubeInfoAfterBuildStep(jobId, lookupMaterializeContext));
    inputSide.addStepPhase4_Cleanup(result);
    outputSide.addStepPhase4_Cleanup(result);

    return result;
}

上述代码中的流程与页面上的构建过程基本一致,下面详细看下Cube计算这个步骤的实现过程,即addLayerCubingSteps(result, jobId, cuboidRootPath)

protected void addLayerCubingSteps(final CubingJob result, final String jobId, final String cuboidRootPath) {
    final SparkExecutable sparkExecutable = new SparkExecutable();
    // 设置cube计算的类
    sparkExecutable.setClassName(SparkCubingByLayer.class.getName());
    // 配置spark任务,主要为数据来源和cuboid数据保存位置
    configureSparkJob(seg, sparkExecutable, jobId, cuboidRootPath);
    // task加入到job中
    result.addTask(sparkExecutable);
}

接着看SparkCubingByLayer中的execute方法,最终任务调度服务调度执行job中的该task时,是调用execute方法来执行的,具体的调用过程可以参考上一篇任务调度的文章:

protected void execute(OptionsHelper optionsHelper) throws Exception {
    String metaUrl = optionsHelper.getOptionValue(OPTION_META_URL);
    String hiveTable = optionsHelper.getOptionValue(OPTION_INPUT_TABLE);
    String inputPath = optionsHelper.getOptionValue(OPTION_INPUT_PATH);
    String cubeName = optionsHelper.getOptionValue(OPTION_CUBE_NAME);
    String segmentId = optionsHelper.getOptionValue(OPTION_SEGMENT_ID);
    String outputPath = optionsHelper.getOptionValue(OPTION_OUTPUT_PATH);
    Class[] kryoClassArray = new Class[] { Class.forName("scala.reflect.ClassTag$$anon$1") };
    SparkConf conf = new SparkConf().setAppName("Cubing for:" + cubeName + " segment " + segmentId);
    //serialization conf
    conf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer");
    conf.set("spark.kryo.registrator", "org.apache.kylin.engine.spark.KylinKryoRegistrator");
    conf.set("spark.kryo.registrationRequired", "true").registerKryoClasses(kryoClassArray);
    KylinSparkJobListener jobListener = new KylinSparkJobListener();
    JavaSparkContext sc = new JavaSparkContext(conf);
    sc.sc().addSparkListener(jobListener);
    // 清空cuboid文件目录
    HadoopUtil.deletePath(sc.hadoopConfiguration(), new Path(outputPath));
    SparkUtil.modifySparkHadoopConfiguration(sc.sc()); // set dfs.replication=2 and enable compress
    final SerializableConfiguration sConf = new SerializableConfiguration(sc.hadoopConfiguration());
    KylinConfig envConfig = AbstractHadoopJob.loadKylinConfigFromHdfs(sConf, metaUrl);

    final CubeInstance cubeInstance = CubeManager.getInstance(envConfig).getCube(cubeName);
    final CubeDesc cubeDesc = cubeInstance.getDescriptor();
    final CubeSegment cubeSegment = cubeInstance.getSegmentById(segmentId);

    logger.info("RDD input path: {}", inputPath);
    logger.info("RDD Output path: {}", outputPath);

    final Job job = Job.getInstance(sConf.get());
    SparkUtil.setHadoopConfForCuboid(job, cubeSegment, metaUrl);

    int countMeasureIndex = 0;
    for (MeasureDesc measureDesc : cubeDesc.getMeasures()) {
        if (measureDesc.getFunction().isCount() == true) {
            break;
        } else {
            countMeasureIndex++;
        }
    }
    final CubeStatsReader cubeStatsReader = new CubeStatsReader(cubeSegment, envConfig);
    boolean[] needAggr = new boolean[cubeDesc.getMeasures().size()];
    boolean allNormalMeasure = true;
    for (int i = 0; i < cubeDesc.getMeasures().size(); i++) {
        // RawMeasureType这里为true,其他均为false
        needAggr[i] = !cubeDesc.getMeasures().get(i).getFunction().getMeasureType().onlyAggrInBaseCuboid();
        allNormalMeasure = allNormalMeasure && needAggr[i];
    }
    logger.info("All measure are normal (agg on all cuboids) ? : " + allNormalMeasure);
    StorageLevel storageLevel = StorageLevel.fromString(envConfig.getSparkStorageLevel());
    // 默认为true
    boolean isSequenceFile = JoinedFlatTable.SEQUENCEFILE.equalsIgnoreCase(envConfig.getFlatTableStorageFormat());
    // 从hive数据源表中构建出RDD,hiveRecordInputRDD得到格式为每行数据的每列的值的
    // RDD(JavaRDD<String[]>),maptoPair是按照basecubiod(每个维度都包含),计算出格式为 
    // rowkey(shard id+cuboid id+values)和每列的值的RDD encodedBaseRDD
    final JavaPairRDD<ByteArray, Object[]> encodedBaseRDD = SparkUtil.hiveRecordInputRDD(isSequenceFile, sc, inputPath, hiveTable)
            .mapToPair(new EncodeBaseCuboid(cubeName, segmentId, metaUrl, sConf));

    Long totalCount = 0L;
    // 默认为false
    if (envConfig.isSparkSanityCheckEnabled()) {
    // 数据总条数
        totalCount = encodedBaseRDD.count();
    }
    // 聚合度量值的具体方法
    final BaseCuboidReducerFunction2 baseCuboidReducerFunction = new BaseCuboidReducerFunction2(cubeName, metaUrl, sConf);
    BaseCuboidReducerFunction2 reducerFunction2 = baseCuboidReducerFunction;
    // 度量没有RAW的为true
    if (allNormalMeasure == false) {
        reducerFunction2 = new CuboidReducerFunction2(cubeName, metaUrl, sConf, needAggr);
    }

    final int totalLevels = cubeSegment.getCuboidScheduler().getBuildLevel();
    JavaPairRDD<ByteArray, Object[]>[] allRDDs = new JavaPairRDD[totalLevels + 1];
    int level = 0;
    int partition = SparkUtil.estimateLayerPartitionNum(level, cubeStatsReader, envConfig);

    // aggregate to calculate base cuboid
    allRDDs[0] = encodedBaseRDD.reduceByKey(baseCuboidReducerFunction, partition).persist(storageLevel);
    // 数据保存到hdfs上
    saveToHDFS(allRDDs[0], metaUrl, cubeName, cubeSegment, outputPath, 0, job, envConfig);
    // 根据base cuboid上卷聚合各个层级的数据,改变数据的rowKey,去掉相应的维度
       PairFlatMapFunction flatMapFunction = new CuboidFlatMap(cubeName, segmentId, 
       metaUrl, sConf);
    // aggregate to ND cuboids
    for (level = 1; level <= totalLevels; level++) {
        partition = SparkUtil.estimateLayerPartitionNum(level, cubeStatsReader, envConfig);
        // flatMapToPair得到上卷聚合后的数据,reduceByKey再进一步根据新的rowKey进行聚合操作, 
           因为进行flatMapToPair操作后会有部分数据的rowKey值相同
        allRDDs[level] = allRDDs[level - 1].flatMapToPair(flatMapFunction).reduceByKey(reducerFunction2, partition)
                .persist(storageLevel);
        allRDDs[level - 1].unpersist();
        if (envConfig.isSparkSanityCheckEnabled() == true) {
            sanityCheck(allRDDs[level], totalCount, level, cubeStatsReader, countMeasureIndex);
        }
        saveToHDFS(allRDDs[level], metaUrl, cubeName, cubeSegment, outputPath, level, job, envConfig);
    }
    allRDDs[totalLevels].unpersist();
    logger.info("Finished on calculating all level cuboids.");
    logger.info("HDFS: Number of bytes written=" + jobListener.metrics.getBytesWritten());
    //HadoopUtil.deleteHDFSMeta(metaUrl);
}

        Cube在构建完所有的cuboid,原始的cuboid文件会存到hdfs目录下(例:/kylin/kylin_metadata/kylin-43be1d7f-4a50-b3a8-6dea-b998acec2d7b/kylin_sales_cube/cuboid),后面的createConvertCuboidToHfileStep任务会将cuboid文件转换成hfile文件保存到/kylin/kylin_metadata/kylin-43be1d7f-4a50-b3a8-6dea-b998acec2d7b/kylin_sales_cube/hfile目录下,最后会由createBulkLoadStep任务将hfile文件loadhbase表中(后面hfile目录会被删除),这样就完成了Cube的构建。这里需要注意的是cuboid文件在Cube构建完成后不会被删除,因为后面做Cube Segmentmerge操作时是直接用已有的cuboid文件,而不需要重新进行计算,加快合并的速度,如果你确认后面不会进行segment的合并操作,cuboid文件可以手动删除掉以节省hdfs的存储空间。

展开阅读全文

没有更多推荐了,返回首页