本文来自网易云社区

作者:汪胜

相关概念介绍

为了了解Kylin存储和查询的分片问题,需要先介绍两个重要概念:segment和cuboid。相信大数据行业的相关同学都不陌生。Kylin每次提交一个新的build任务都会生成一个新的segment,而用户一般都是每天构建一次。那么,这种情况下,每天都会生成一个新的segment,用来保存昨天的数据。 Kylin的核心思想是预聚合,就是将用户预先定义的维度组合计算出来,然后保存到HBase中。这样查询的时候就可以直接查询预先计算好的结果,速度非常快。这里的维度组合就是cuboid。Kylin在构建过程中,会产生很多的cuboid数据(每一种cuboid都对应着一种维度组合),这些数据最终都会以HFile的形式存储在HBase中。Kylin对于每一个cuboid都会有一个唯一的id(一个cube的所有segment都有着相同的cuboid和cuboid id)。而这个id就是根据用户在定义cube时,维度列的排序来确定的。下面来举一个简单的例子。假设表一共有三列ABC,那么所有的cuboid组合就是:

cuboid cuboid_id
ABC 7(111)
AB 6(110)
BC 5(101)
AC 4(100)
A 3(011)
B 2(010)
C 1(001)

其中,cube的维度列顺序为A,B,C,括号里面的是id对应的二进制,用户可以在构建cube的时候进行排序。最终数据在HBase中存储的时候,rowkey也就是按这个顺序将这些维度值组合起来(rowkey还包含其他一些成员,这里不展开)。一般推荐将用户经常使用或者基数很大的维度放在前面,这样在查询的时候有利用提高扫描效率。

存储分片问题

Kylin在build过程中,每一个cuboid的数据都会被分到若干个分片中(这里的分片就对应HBase中的region)。对于每个segment都会保存cuboidShardNums和totalShards成员。如下所示:

//key表示cuboid的id,value表示该cuboid占用的region数private Map<Long, Short> cuboidShardNums = Maps.newHashMap();//该segment占用的region总数private int totalShards = 0;

请注意,一个region可能会存储多个cuboid数据,因此cuboid和region之间是多对多的关系。 Kylin可以通过下面三个配置项来控制生成build过程中生成的region相关信息:

//单个region的大小kylin.storage.hbase.region-cut-gb//region最小数量kylin.storage.hbase.min-region-count//region最大数量kylin.storage.hbase.max-region-count

通过上面这三个配置项,我们就可以控制每个build过程中生成的region数量和大小,从而进行相应的优化。segment的分片信息也会收到这几个参数的影响。具体如下:

float cut = cubeDesc.getConfig().getKylinHBaseRegionCut();int nRegion = Math.round((float) (totalSizeInM / (cut * 1024L)));
nRegion = Math.max(kylinConfig.getHBaseRegionCountMin(), nRegion);
nRegion = Math.min(kylinConfig.getHBaseRegionCountMax(), nRegion);//省略余下部分代码

其中,cut就是通过kylin.storage.hbase.region-cut-gb来设置的region分割阈值,totalSizeInM是本次build过程中生成的数据大小(所有cuboid数据之和),这样就可以求出每个segment对应的totalShards大小,即nRegion。再通过如下代码便可以求出每个cuboid所占用的分片数:

int mbPerRegion = (int) (totalSizeInM / nRegion);for (long cuboidId : allCuboids) {  double estimatedSize = cubeSizeMap.get(cuboidId);  double magic = 23;  int shardNum = (int) (estimatedSize * magic / mbPerRegion + 1);  if (shardNum < 1) {
    shardNum = 1;
  }  //省略余下部分代码}

首先求出每个region的实际大小mbPerRegion,然后根据每个cuboid的数据大小estimatedSize就可以求出每个cuboid所占的region数,即shardNum。这里使用了一个magic,这是为了将cuboid数据尽量分散到多个region中,这样在查询的时候就可以多个region并行扫描,提高查询效率。 搞定cuboidShardNums和totalShards之后,还需要确定每个cuboid存储数据的起始region(再通过region数shardNum便可以确定指定cuboid的所有数据分布的位置)。这里主要就是根据cuboid id和region总数来获取每个cuboid存储起始region id,具体不再展开,有兴趣的同学可以自行查看源(ShardingHash.java)。

short startShard = ShardingHash.getShard(cuboidId, nRegion);

Segment使用cuboidBaseShards成员来保存cuboid id和起始region id的映射,如下所示:

private Map<Long, Short> cuboidBaseShards = Maps.newConcurrentMap();

这样一来,就基本搞定了Kylin build过程中,segment的存储分片问题。

查询分片问题

当新的segment生成之后,我们就可以查询其中的数据了。从上面的分析中我们得知,每一个segment的构建结果其实就是多个cuboid的数据集合。那么,当我们进行查询的时候,Kylin会根据sql中的列来获取到最佳匹配的cuboid(join情况下可能会存在多个匹配的cuboid)。然后根据筛选出来的cuboid id去对应的segment中进行扫描。Kylin对于每一个待扫描的segment都会生成一个CubeSegmentScanner。在对每个segment进行扫描的时候,首先需要根据筛选到的cuboid id去获取相应的region信息(主要是起始region id和region数)。主要处理逻辑如下所示:

//传入的三个参数都可以通过cuboid id去相应的segment中获取private List<Pair<byte[], byte[]>> getEPKeyRanges(short baseShard, short shardNum, int totalShards) {  if (shardNum == 0) {    return Lists.newArrayList();
  }  if (shardNum == totalShards) {    //该cuboid的数据分布在所有的region中
    return Lists.newArrayList(Pair.newPair(getByteArrayForShort((short) 0),
               getByteArrayForShort((short) (shardNum - 1))));
  } else if (baseShard + shardNum <= totalShards) {    //该cuboid的数据分布在id连续的region中
    return Lists.newArrayList(Pair.newPair(getByteArrayForShort(baseShard),   
               getByteArrayForShort((short) (baseShard + shardNum - 1))));
  } else {    //0,1,2,3,4 存储在 4,0这种情况
    return Lists.newArrayList(Pair.newPair(getByteArrayForShort(baseShard),    
               getByteArrayForShort((short) (totalShards - 1))), 
               Pair.newPair(getByteArrayForShort((short) 0), 
               getByteArrayForShort((short) (baseShard + shardNum - totalShards - 1))));
  }
}private byte[] getByteArrayForShort(short v) {  byte[] split = new byte[Bytes.SIZEOF_SHORT];
  BytesUtil.writeUnsigned(v, split, 0, Bytes.SIZEOF_SHORT);  return split;
}

这样就可以获取每个segment需要扫描的region,由于Kylin目前的数据都存储在HBase当中,因此扫描的过程都在HBase中进行。对于每个region,kylin都会启动一个线程来向HBase发送扫描请求,然后将所有扫描的结果返回,聚合之后再返回上一层。为了加快扫描效率,Kylin还使用了HBase的coprocessor来对每个region的扫描结果进行预聚合。关于coprocessor的相关知识这里就不再介绍,可参考源码(CubeHBaseEndpointRPC.java和CubeVisitService.java)。
到这里,关于Kylin存储和查询的分片问题就整理的差不多了,本文省略了一些Kylin在使用HBase进行存储时的一些相关细节,后续会陆续补充上来,有感兴趣的同学可以一起交流学习。

网易云免费体验馆,0成本体验20+款云产品!

更多网易研发、产品、运营经验分享请访问网易云社区

相关文章:
【推荐】 3分钟掌握一个有数小技能:收入贡献分析
【推荐】 【专家坐堂】四种并发编程模型简介

Kylin存储和查询的分片问题的更多相关文章

  1. Redis 存储、查询

    [TOC] 数据存储 假设我们在MySQL数据库中有这样一张表: mysql> desc user_info; Field Type Null Key Default Extra id int( ...

  2. Elasticsearch集群搭建及使用Java客户端对数据存储和查询

    本次博文发两块,前部分是怎样搭建一个Elastic集群,后半部分是基于Java对数据进行写入和聚合统计. 一.Elastic集群搭建 1. 环境准备. 该集群环境基于VMware虚拟机.CentOS ...

  3. 编写高质量代码改善C#程序的157个建议——建议26:使用匿名类型存储LINQ查询结果

    建议26:使用匿名类型存储LINQ查询结果 从.NET3.0开始,C#开始支持一个新特性:匿名类型.匿名类型有var.赋值运算符和一个非空初始值(或以new开头的初始化项)组成.匿名类型有如下基本特性 ...

  4. ID和Phone高压缩比存储和查询

    ID和Phone高压缩比存储和查询的简单例子, 无多线程处理 运行环境JDK8+maven 0. 模块分割 1. 基本思路 源文件BCP每一行都转为一个全局的RowID,可以直接映射到FileName ...

  5. 分布式文档存储数据库之MongoDB分片集群

    前文我们聊到了mongodb的副本集以及配置副本集,回顾请参考https://www.cnblogs.com/qiuhom-1874/p/13953598.html:今天我们来聊下mongodb的分片 ...

  6. 最新IP数据库 存储优化 查询性能优化 每秒解析上千万

    高性能IP数据库格式详解 每秒解析1000多万ip  qqzeng-ip-ultimate.dat 3.0版 编码:UTF8     字节序:Little-Endian 返回规范字段(如:亚洲|中国| ...

  7. 「SQL归纳」树形结构表的存储与查询功能的实现——通过路径方法(非递归)

    一.树形结构例子分析: 以360问答页面为例:http://wenda.so.com/c/ 我们通过观察URL,可以明确该页面的数据以树形结构存储,下面三块模块分别为: ①根节点 ②根节点的第一层子节 ...

  8. .NET+MVC+ORACLE存储分页查询一后端实现

    MemberController:public ActionResult UserList() { UserBll userBll = new UserBll(); string keyWords = ...

  9. sql server 表变量存储临时查询数据

    对于使用sql server 编写存储过程或者类似的sql 查询的时候我们使用表变量进行临时数据的存储,可以方便我们进行下来的数据处理 表变量的使用类似如下: declare @userinfo ta ...

随机推荐

  1. json、pickle\shelve模块(超级好用~!)讲解

    json.pickle模块讲解 见我前面的文章:http://www.cnblogs.com/itfat/p/7456054.html shelve模块讲解(超级好用~!) json和pickle的模 ...

  2. 16c550芯片编写的优化

    参考了 <Altera FPGA/CPLD 设计>高级篇, 关于状态机的推荐写法实现的功能是一样的但是编译使用的逻辑门如下图: 下图是我自己编的状态机需要的逻辑: 下图是使用推荐的有限状态 ...

  3. Python之select模块解析

    首先列一下,sellect.poll.epoll三者的区别 select select最早于1983年出现在4.2BSD中,它通过一个select()系统调用来监视多个文件描述符的数组,当select ...

  4. Java的I/O流问题

    一.流的概念        流(stream)的概念源于UNIX中管道(pipe)的概念.在UNIX中,管道是一条不间断的字节流,用来实现程序或进程间的通信,或读写外围设备.外部文件等.        ...

  5. 让你的javascript代码高大上

    1,创造简短的写法 你可以这么写: var slice = Array.prototype.slice; slice.call(arguments); //转化成数组 也可以这么写:(ie9+) va ...

  6. Ubantu下安装adobe flash player插件

    用火狐看视频,要打开Adobe官网下载xxxx,太麻烦. 可以在Terminal下输入: apt-get install flashplugin-nonfree 好了.

  7. PL/SQL查询设计器

    被微软惯坏的我,在使用PL/SQL进行oracle多表连接查询操作时候经常挠头. 今天无意间发现了PL/SQL也有查询设计器,虽然没有sqlserver的强大好用,但足够用了. 在菜单栏 工具---& ...

  8. [JBPM3.2]TaskNode的signal属性详解

    TaskNode节点的signal属性决定了任务完成时对流程执行继续的影响,共有六种取值:unsynchronized,never,first,first-wait,last,last-wait.默认 ...

  9. 强大的HTML5开发工具推荐

    HTML5被看做是Web开发者创建流行Web应用的利器,增加了对视频和Canvas 2D的支持.HTML5的诞生还让人们重新审视浏览器专用多媒体插件的未来,如Adobe的Flash和微软的Silver ...

  10. History - BOM对象

    History 对象 History 对象包含用户(在浏览器窗口中)访问过的 URL. History 对象是 window 对象的一部分,可通过 window.history 属性对其进行访问. 注 ...