全球预见者分享平台、领先的数据科学社区媒体和职业发展平台

168大数据

 找回密码
 立即注册

QQ登录

只需一步,快速开始

1 2 3 4 5
开启左侧

[Kudu] Kudu的Schema表结构设计

[复制链接]
发表于 2019-1-4 16:55:46 | 显示全部楼层 |阅读模式
本帖最后由 168主编 于 2019-1-4 16:57 编辑

Kudu有着和MySQL等传统RDBMS类似的存储结构。表结构的设计对性能和稳定性的起着决定性的作用。本文把Kudu官网的表结构设计做了少许整理,结合微店自身业务做了些许的实践和测试。
宏观来看,Kudu的表结构设计有三个重要概念:列设计、主键设计和切片设计。其中列设计、主键设计和传统的数据库类似,切片设计是分布式数据库特有的概念,切片设计把数据分布在不同的机器上,对于不同的业务场景,不同的读写分布,其切片设计可能千差万别。
Schema设计目标一个比较好的Schema设计应当有如下的目标:
  • 数据的随机读写均匀分布在不同的切片上,充分利用分布式多机资源,使得总吞吐量达到最大。
  • 切片后的数据(Tablets)应当均匀增长,每个切片的大小相当,并且每个切片的负载相当。
  • SQL对应的Scan操作应当读取最少的数据。这是影响Kudu之上BI分析最关键的一步,甚至有时候要放弃写性能来达到最大的Scan性能。通常情况下影响Scan性能的是主键和切片设计,两者在不同的场景下作用不一。
列设计
和传统的数据库类似,Kudu表都由不同的列组成。Kudu支持的列类型如下(除了被设置为主键外,如下类型均可包含null):
  • boolean
  • 8-bit signed integer(Kudu只支持有符号整数)
  • 16-bit signed integer
  • 32-bit signed integer
  • 64-bit signed integer
  • unixtime_micros (unix时间戳)
  • single-precision (32-bit) IEEE-754 floating-point number
  • double-precision (64-bit) IEEE-754 floating-point number
  • UTF-8 encoded string (压缩前最大64K)
  • binary (压缩前最大64K)
相比于Hbase的所有皆是字节的设计方案,Kudu对于结构化的数据类型有着更好的存储和检索效率,Kudu要求用户在表的创建时设置好每列所对应的数据类型,Kudu除了支持每列不同类型的设计之外,还可以针对每一列设置不同的压缩方式。
列编码设计
针对不同的类型,Kudu默认会选择不同的列编码方式,以达到最大的存储和检索效率。
列类型支持的编码方式默认
int8, int16, int32plain, bitshuffle, run lengthbitshuffle
int64, unixtime_microsplain, bitshuffle, run lengthbitshuffle
float, doubleplain, bitshufflebitshuffle
boolplain, run lengthrun length
string, binaryplain, prefix, dictionarydictionary
Plain Encoding平铺的编码方式数据用原本的结构来存储,如int32类型固定的占用4个字节。
Bitshuffle EncodingBitshuffle压缩算法原始论文发表在这里。Bitshuffle的开源实现在这里。该编码算法大概原理为按照数据出现频次来对比特重新分布,最终采用LZ4压缩落地。对于重复数据出现比较多的列,或者主键排序之后相邻单元差异较小,Bitshuffle是一个不错的选择。
Run Length Encoding该编码方式把相邻的相同元素按长度进行编码存储。比如原始数据为aaabbbbc,编码后变为a3b4c1。如某列出现的值类别很少,比如性别、国家等。用该编码方式较合适。
Dictionary Encoding字典的编码方式把输入的字符串转换为[0-n)的整数,n为所有字符串去重后的个数。显而易见,n越小,字典的编码方式效率越高。另外当n比较大时,Kudu会自动的进行降级处理,编码方式自动降级为平铺的编码(Plain Encoding)
Prefix Encoding前缀编码,可以近似认为内部使用Trie(字典树)进行存储,当数据前缀相同部分较多时比较适合采用该编码方式。另外主键中的第一列是按前缀进行字典序排序,此时也可采用前缀编码。
列压缩Kudu允许的落地压缩算法为LZ4、Snappy和zlib(gz)。默认情况下,Kudu不压缩数据。通常情况下压缩算法会提高空间利用率,但是会降低Scan性能。
LZ4和Snappy比较类似,空间和时间有着很好地均衡,zlib有着较高的压缩比,但是Scan性能最差。
需要注意的是Bitshuffle Encoding已经在最后采用了LZ4,所以对于采用这种编码方式的列,无需再指定额外的压缩算法。
主键设计每一个Kudu的表都有且仅有一个主键,主键可以包含多个列,同时要求每一列的值都不能为空(non-nullable),另外bool和浮点数也不能作为主键。
跟MySQL类似,主键具有唯一性,同一个主键只能对应一行数据,对于主键重复的数据insert会触发duplicate key error。
跟MySQL等传统数据库不一样的是,Kudu目前并不支持自增主键。不过主键是Kudu表结构最重要的设计,对于Kudu而言,自增主键通常也不是很好地选择,有的同学为了方便甚至随机生成一个ID作为主键写入Kudu,该方式对Kudu Scan性能的提升没有任何帮助,放弃主键的这种设计对Kudu而言是极大的浪费。
对于切片之后的每片数据,可以近似认为是按照主键有序存储的,主键字典序相近的数据会放在一起,充分利用这个特性,可以极大的提高Scan性能。
比如我们要检索一个店铺的所有商品,那么把店铺ID作为主键的第一列,可以极大的提高Scan的性能,这是因为店铺ID作为主键第一列,一个店铺下的所有商品变会有序放在一起。
我们知道磁盘的随机读性能要远低于顺序读,如果一个店铺的所有商品集中放在一起,Scan操作只需要顺序读一次,如果店铺下的商品是随机存储在n个位置,Scan操作则需要随机读n次。

切片设计(partitioning)作为一个分布式的数据存储引擎,切片是最基本也是最重要的设计之一。对于每个数据库表(table)而言,Kudu会把一个table按照切片规则分成多个partition,一个partition存储在tablet服务之中。每个tablet都有一个一主多从的tablet服务,每条数据属于且仅属于一个tablet,数据和tablet的从属关系规则,由切片规则决定。
优化一个数据库表的切片规则需要考虑随机读、随机写、扫描三种操作,需根据业务场景的不同侧重来最终决定。
随机写压力场景对于写压力比较大的业务场景,最重要的一点是把写压力均匀分担到不同的tablet之中,这种场景下切片设计通常采用hash partitioning,hash切片拥有良好的随机性。
相比Hbase而言,Kudu的架构可以轻松应对随机写的场景。

随机读压力场景对于随机读压力比较大的业务场景并不是很建议使用Kudu,通常情况下Hbase是一个更好的选择,不过Kudu也拥有不错的随机读性能。Kudu官方的性能测试,在读压力分布符合齐夫定律时,Hbase有读性能优势,随机分布下,Kudu和Hbase的的随机读性能相当。不过通常情况下业务场景的读分布符合齐夫定律,也就是我们常说的28原则,80%的读集中在20%的数据上。
如果用Kudu的业务场景确实随机读压力较大,则通常采用hash partitioning。

小范围Scan场景对于拥有大量小范围Scan的业务场景,比如扫描一个店铺的所有商品,比如找到一个用户看过的所有商品,诸如此类的业务场景最好将同一个Scan所需要的数据放置在同一个tablet里面。比如按店铺id做hash,可以把同一个店铺的所有信息放置在同一个tablet里。按userid做hash,可以把一个用户的所有信息放置在同一个tablet里面。
大范围Scan如果业务场景的Scan所需要扫描的数据量比较大,又想这类Scan跑的快,则需要把这类Scan所需要的数据分布到多个tablets里面,充分利用多机分布式计算能力。假设我们有一个表存储了最近12个月的数据,一个设计方案是按照月来切片,一共12个tablet,但如果大部分BI查询对应的Scan只需要最近1个月的数据,则这种设计便不合理,因为Scan的压力全部集中到了一个tablet之中。
这种情况下一个更好的设计方案是按月切片再按hash切片,具体方案后续再详细分析。

Range切片Range的切片方式把数据按照范围进行分类,每个partition会分配一个固定的范围,每个数据只会属于一个切片,不同partition的范围不能有重叠。切片在表的创建阶段配置,后续不可修改,但是可以删除和新增,如果数据找不到所属的切片,会插入失败。
range的切片方式通常与时间有关系,值得注意的是,老的切片可以删掉,同时可以增加新的切片,意味着与时间强相关的数据可以按照这种方式来切片,老的数据可以通过删除切片的方式删除。同时Kudu对此类操作的支持非常高效,完全不用担心删除或者新增切片会影响数据读写。上文有讲到,单片数据过大会影响Kudu的性能,配置为时间相关的Range切片方式,可以很好地控制每片数据的总大小。比如有日志型数据,每秒平均有100条数据写入,配置每天一个切片,则单片数据量规模约为864w。如果配置为hash的切片方式,则单片数据会随着时间推移越来越多大。
切片的设计对Scan性能的影响至关重要,比如对于时间序列类型的数据而言,往往查询的是近期的数据,如果按时间进行切片,则Scan操作可以跳过大部分数据,如果单纯按照默认的hash方式切片,Scan操作则需要扫描全表。
Hash切片
Hash切片把每行数据hash之后分配到对应的tablet。hash切片在设计上相对简单,通常情况下只需要配置计算hash值的列,比如前文所列举的例子,如果你需要查询一个店铺的所有商品,则把shopid作为hash列是比较恰当的选择。相同shopid的hash值是相同的,相同hash值的数据肯定会被分配到同一个partition之中。
123957488_1_20180205085852214.png
需要注意的是hash的切片方式是不可修改的,所以随着数据量的增长,hash的切片方式会造成单片的数据量过大,甚至超过单个tablet服务所能承受的数据量。
hash的切片方式对于随机读写友好,对于写操作而言,hash的切片方式会均匀的把写入压力分担到多个切片之中。对于随机读而言,按照主键进行hash之后Kudu可以提前预知读操作所对应的切片,避免每个切片都查一次。
多级切片
Kudu支持多层的切片方式,hash和range的切片方式可以结合起来。比如按照月把数据分成多片,每个月的数据再按照hash进行二级切片。
合理的使用多级切片,可以充分利用不同切片方式的优势。
切片调优
合理的切片可以让Kudu的Scan操作跳过部分切片,比如上文举例说明的时间序列类型存储。合理的切片还需要避免写入热点,防止大量的写入分配到同一个tablet服务之中。
切片设计案例
考虑如下表结构的设计
CREATE TABLE metrics ( host STRING NOT NULL, metric STRING NOT NULL, time INT64 NOT NULL, value DOUBLE NOT NULL, PRIMARY KEY (host, metric, time),);
该表有四个字段,host、metric、time和value。主键包含三列(host、metric和time)
Range切割方法
一个自然的方式是按照时间切片,我们可以把数据分为(2015年之前,2015年,2015年之后)。或者更直接的按照年份进行切分(2014年,2015年、2016年……)
123957488_2_20180205085854292.png
上图便是这两种切分方式的图形化表示。第一种方式的优势在于切片配置的可拓展性强,第二种方式随着时间推移,切片方式需要调整。但是如果按天进行切割,第二种方式会有较多的切片,Kudu目前的架构并不支持太多的切片。但是第一种切片方式每个partition所对应的数据量不一致,容易造成单个tablet过大。
Hash和Range切割方法
上述表结构可以使用time字段进行range的切分方式,也可以使用(host+metric)的hash切分方式。两种切分方式各有自己的优势和列式。

切分策略表增长
按时间范围切割容易造成写集中在最后一片基于时间的Scan性能高切片可以增加
按host和metric做hash写会均匀分布到不同切片基于host和metric的查询性能高切片会无限制变大
基于hash的切片方式可以极大的提高写性能,基于range的切片方式可以提高部分Scan性能,同时还可以防止单片数据的过大增长。Kudu所提供的多级切片方式可以较好的结合两种不同切片方式的优点。下图为结合hash和range的两级切片方式示意图:
123957488_3_20180205085854932.png
实际案例测试为了实际测试本文中的Kudu表结构设计理论,作者创建了三张数据表。该数据是微店后台piwik收集的实际pv数据。通过Impala后台,可以看到每个SQL操作所扫描的切片数量。
表1:该表采用了基本的hash切片,并且采用了line_id作为hash id(不指定hash id,主键即为hash id默认值)
该表共有50个切片。
create table speed1 ( line_id string, request_time timestamp, idvisitor string, primary key(line_id))partition by hash partitions 50stored as kudu;
表2该表采用了range的切片方式,并且每天一个片。需要注意的是,因为把request_time加入了切片规则,所以主键之中必须包含request_time。
该表共有13个切片,分别对应13天的数据。
create table speed_test_2 ( line_id string, request_time timestamp, idvisitor string, primary key(line_id,request_time) )partition by range(request_time)(PARTITION cast(1505130896 as timestamp) <= values="">< cast(1505217296="" as="" timestamp),partition="" cast(1505217296="" as="" timestamp)=""><= values="">< cast(1505303696="" as="" timestamp),partition="" cast(1505303696="" as="" timestamp)=""><= values="">< cast(1505390096="" as="" timestamp),partition="" cast(1505390096="" as="" timestamp)=""><= values="">< cast(1505476496="" as="" timestamp),partition="" cast(1505476496="" as="" timestamp)=""><= values="">< cast(1505562896="" as="" timestamp),partition="" cast(1505562896="" as="" timestamp)=""><= values="">< cast(1505649296="" as="" timestamp),partition="" cast(1505649296="" as="" timestamp)=""><= values="">< cast(1505735696="" as="" timestamp),partition="" cast(1505735696="" as="" timestamp)=""><= values="">< cast(1505822096="" as="" timestamp),partition="" cast(1505822096="" as="" timestamp)=""><= values="">< cast(1505908496="" as="" timestamp),partition="" cast(1505908496="" as="" timestamp)=""><= values="">< cast(1505994896="" as="" timestamp),partition="" cast(1505994896="" as="" timestamp)=""><= values="">< cast(1506081296="" as="" timestamp),partition="" cast(1506081296="" as="" timestamp)=""><= values="">< cast(1506167696="" as="" timestamp),partition="" cast(1506167696="" as="" timestamp)=""><= values="">< cast(1506254096="" as="" timestamp))stored="" as="" kudu="">
表3表3融合了表1和表2两种建表方式,切片方法既包含了hash,也包含了range。
该表共有13*3=39个切片,代表了13天的数据,每天3个hash切片。
create table speed4 ( line_id string, request_time timestamp, idvisitor string, primary key(line_id,request_time) )partition by hash (line_id) partitions 3,range(request_time)(PARTITION cast(1505130896 as timestamp) <= values="">< cast(1505217296="" as="" timestamp),partition="" cast(1505217296="" as="" timestamp)=""><= values="">< cast(1505303696="" as="" timestamp),partition="" cast(1505303696="" as="" timestamp)=""><= values="">< cast(1505390096="" as="" timestamp),partition="" cast(1505390096="" as="" timestamp)=""><= values="">< cast(1505476496="" as="" timestamp),partition="" cast(1505476496="" as="" timestamp)=""><= values="">< cast(1505562896="" as="" timestamp),partition="" cast(1505562896="" as="" timestamp)=""><= values="">< cast(1505649296="" as="" timestamp),partition="" cast(1505649296="" as="" timestamp)=""><= values="">< cast(1505735696="" as="" timestamp),partition="" cast(1505735696="" as="" timestamp)=""><= values="">< cast(1505822096="" as="" timestamp),partition="" cast(1505822096="" as="" timestamp)=""><= values="">< cast(1505908496="" as="" timestamp),partition="" cast(1505908496="" as="" timestamp)=""><= values="">< cast(1505994896="" as="" timestamp),partition="" cast(1505994896="" as="" timestamp)=""><= values="">< cast(1506081296="" as="" timestamp),partition="" cast(1506081296="" as="" timestamp)=""><= values="">< cast(1506167696="" as="" timestamp),partition="" cast(1506167696="" as="" timestamp)=""><= values="">< cast(1506254096="" as="" timestamp))stored="" as="" kudu="">
测试结论对于不同类型的查询sql,本次测试结果如下:
单次查询
请求类型表1查询对应的
切片数量
表2查询对应的
切片数量
表3查询对应的
切片数量
解释
select count(*) from table_name501339表1、2、3的全部切片都有扫描
select * from table_name
where line_id='xxx'
11313表一line_id是唯一主键,只需要查询一个切片,表2表3则不行。
同时三张表line_id均是第一索引,所以查询操作都很快。
select * from table_name
where idvisitor='xxx'
501339idvisitor不在主键之中,所以需要查询所有切片
select count(*) from table_name
where request_time>adddate(now(), -5)
50515表1扫描了全部切片,表2和表3因为有range配置,所以只扫描了部分切片
可以看到Kudu会根据where条件跳过部分分区。对于带有时间where条件的大范围Scan查询而言,可以看出,表2和表3是比较合适的。表3虽然扫描的切片比表2多,但是扫描的总数据量是和表2一样的,同时表3能更好的利用多机资源,可以把并发度从表2的5提高到15.
通常情况下,应当尽量使用类似表3的结构来降低Scan操作所扫描的数据总量。
已知的限制
  • 列的数量最多300,越少越好
  • 总切片数量不适宜太多,单个物理机最多承受1000个切片
  • 每张表每片数据在1000w条左右较合适,根据列的数量,该建议需灵活配置
  • 单个cell最大64KB
  • 单行数据不能太大
  • 描述用UTF-8编码之后不能超过256字节
  • 主键对应的cell内容不可改变
  • 主键包含那几列建表时需要设置好,后续不可改变
  • 切片规则不可改变,但是Range切片可以增加切片或者删除切片
  • 列的类型不能改变

本文大部分内容翻译整理自Kudu和Impala官网
作者: 高云翔
写于:2017年09月



楼主热帖
168大数据(www.bi168.cn)是国内首家系统性关注大数据科学与人工智能的社区媒体!
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

关闭

站长推荐上一条 /1 下一条

关于我们|小黑屋|Archiver|168大数据 ( 京ICP备14035423号|申请友情链接

GMT+8, 2019-3-23 09:28 , Processed in 0.086285 second(s), 19 queries , Xcache On.

Powered by BI168社区

© 2012-2014 海鸥科技

快速回复 返回顶部 返回列表