下面通过对kylin构建cube流程的分析来介绍cube优化思路。
创建hive中间表
kylin会在cube构建的第一步先构建一张hive的中间表,该表关联了所有的事实表和维度表,也就是一张宽表。
hive表分区,在构建宽表的时候,kylin需要遍历hive表,事实表和维度表如果是分区表,那么会减少遍历时间
hive相关配置调整,join相关配置,mapreduce相关配置等
创建完成后,为了防止文件大小不一致的情况,kylin又基于hive做了一次重均衡操作,
`kylin.engine.mr.mapper-input-rows=1000000`,默认每个文件包含100w的数据量
代码 `CreateFlatHiveTableStep`
找出所有维度的基数通过HyperLogLog 算法找出去重后的维度列,如果某个的技术很大,那么这种维度为被称为ultra high cardinality column(UHC),也就是超高基数维度。
业务层处理UHC比如时间戳维度基础可能是亿级的,可以转成为日期,基数降到几十万.
技术层处理UHC
kylin通过mapreduce进行此步骤,在reduce端,一个维度用一个reduce去重,因此当某个维度的基数很大时,会导致该维度所在的reduce运行很慢,甚至内存溢出,为了应对这种场景,kylin提供了两种解决方案
1. 全局唯一维度,也就是在count_dintinct中选择0错误率的统计分析。
2. 需要被shard by的维度,在rowkey构建时配置的维度。
接着可以通过配置`kylin.engine.mr.uhc-reducer-count=1`来声明这些列需要被分割成多少个reducer执行
当然,kylin也支持基于cuboid个数来进行reducer个数的分配,`kylin.engine.mr.hll-max-reducer-number=1`,默认情况下kylin不开启此功能,可以修改配置来提高最小个数;然后通过配置`kylin.engine.mr.per-reducer-hll-cuboid-number`来调整具体的reduce数量
`
int nCuboids = cube.getCuboidScheduler().getAllCuboidIds().size();
int shardBase = (nCuboids - 1) / cube.getConfig().getHadoopJobPerReducerHLLCuboidNumber()
+ 1;
int hllMaxReducerNumber = cube.getConfig().getHadoopJobHLLMaxReducerNumber();
if (shardBase > hllMaxReducerNumber) {
shardBase = hllMaxReducerNumber;
}
`
最终的reducer数量由UHC和cuboids两个部分相加得到,具体代码参考
`FactDistinctColumnsReducerMapping`构造函数
# 配置UHC增加另外步骤,需要配置zk的地址(作为全局分布式锁使用)
# 因为在跑mapreduce的过程中,kylin没有将hbase-site.xml等配置上传到yarn,所以只能在kylin.properties中额外配置一遍
kylin.engine.mr.build-uhc-dict-in-additional-step=true
kylin.env.zookeeper-connect-string=host:port,host:port
代码 `FactDistinctColumnsJob`, `UHCDictionaryJob`
构建维度字典找出所有维度的基数后,kyin为每个维度构建一个数据字典,字典的metadata存储在hdfs上,实际数据存储在hbase
字典在hdfs的路径规则为
kylin/kylin_meta_data/kylin-$jobid/%cubeid/metadata/dict/$catalog.$table/$dimension/$uuid.dict
字典数据在hbase的rowkey规则为
/dict/$catalog.$table/$dimension/$uuid.dict
rowkey长度
过长的rowkey会占用很大的存储空间,所以需要对rowkey长度进行控制。
当前kylin直接在当前进程内做了字典编码,也就是把string映射成int,如果维度列的基数很大,那么可能会出现内存溢出的情况(当列的基础大于1kw),这时候就需要考虑更改维度列的编码方式,改用`fixed_length`等
rowkey构建
对rowkey的构建也有一定的要求,一般而言,需要把基数大的字段放在前面,这样可以在scan的过程中尽可能的跳过更多的rowkey。
另一方面将基数小的列放在rowkey的后面,可以减少构建的重复计算,有些cuboid可以通过一个以上的父cuboid聚合而成,在这种情况下,Kylin将会选择最小的父cuboid。例如,AB能够通过ABC(id:1110)和ABD(id:1101)聚合生成,因此ABD会被作为父cuboid使用,因为它的id比ABC要小。基于以上处理,如果D的基数很小,那么此次聚合操作就会花费很小的代价。因此,当设计cube的rowkey顺序的时候,请记住,将低基数的维度列放在尾部。这不仅对cube的构建过程有好处,而且对cube查询也有好处,因为后聚合(应该是指在HBase查找对应cuboid的过程)也遵循这个规则。
构建cube 构建引擎
可以选择spark或者mapreduce来构建cube,通常来说,构建引擎的选择方式是这样的
内存消耗型的cube选择mapreduce,例如Count Distinct, Top-N
简单的cube选择spark,例如SUM/MIN/MAX/COUNT
spark引擎
spark构建引擎采用` by-layer`算法,也就是分层计算
比如有3个维度ABC,cube会构建A,B,C,AB,AC,ABC6种组合,这里就有3层,
第1层:A,B,C
第2层:AB,AC
第3层:ABC
每一层在计算对于spark而言都是一个action,并且该层计算的rdd会依赖其上一层的结果继续计算,这样避免了很大重复性计算工作。
代码` SparkCubingByLayer`
设计模式参考《kylin介绍》中的cube设计模式
数据转换为HFile
kylin将生成的cube通过生成HFile的方式导入到hbase,这个优化点可以配置hbase的相关参数。
region数量默认是1,如果数据量大的话可以提高region数量
region大小默认是5GB,也就是hbae官方建议的大小;如果cube大小比这个值小太多,可以减小单region的大小
hfile文件大小,默认是1GB,由于是通过mapreduce写入的,小文件意味着写入快,但是读取慢,大文件意味着写入慢,读取快
代码`CubeHFileJob`
cleanup清理hive中的中间表,
清理hbase表
清理hdfs数据
清理命令
# 查看需要清理的数据
./bin/kylin.sh org.apache.kylin.tool.StorageCleanupJob --delete false
# 清理
./bin/kylin.sh org.apache.kylin.tool.StorageCleanupJob --delete true
// clean参考
总结
基于kylin的ui,可以看到kylin在构建cube时各个流程的耗时,可以依据这些耗时做相应的优化,常见的,可以从耗时最长的步骤开始优化,比如:
遇到创建hive中间表时间很长,考虑对hive表进行分区处理,对表中的文件格式更改,使用orc,parquet等高性能的文件格式
遇到cube构建时间过长,查看cube设计是否合理,维度的组合关系是否可以再减少,构建引擎是否可以优化
优化的思路还是以cube为中心,优化cube的整个生命周期,其中涉及到的所有组件都是优化点,具体情况还是要和实际的数据维度和业务结合起来。
参考
// 官方文档
// 官方文档,cube性能优化