我们可以将一个表想象成一个大的映射关系,通过行健、行健+时间戳或行键+列(列族:列修饰符),就可以定位特定数据,是稀疏存储数据的,因此某些列可以是空白的,
Row Key
Time Stamp
Column Family:c1
Column Family:c2
列
值
列
值
r1
t7
c1:1
value1-1/1
t6
c1:2
value1-1/2
t5
c1:3
value1-1/3
t4
c2:1
value1-2/1
t3
c2:2
value1-2/2
t2
t2
c1:1
value2-1/1
t1
c2:1
value2-1/1
从上表可以看出,test表有r1和r2两行数据,并且c1和c2两个列族,在r1中,列族c1有三条数据,列族c2有两条数据;在r2中,列族c1有一条数据, 列族c2有一条数据,每一条数据对应的时间戳都用数字来表示,编号越大表示数据越旧,反而表示数据越新。
3:物理视图
虽然从概念视图来看每个表格是由很多行组成的,但是在物理存储上面,它是按照列来保存的。
Row Key
Time Stamp
Column Family:c1
列
值
r1
t7
c1:1
value1-1/1
t6
c1:2
value1-1/2
t5
c1:3
value1-1/3
表:Hbase数据的物理视图(1)
Row Key
Time Stamp
Column Family:c2
列
值
r1
t4
c2:1
value1-2/1
t3
c2:2
value1-2/2
表:Hbase数据的物理视图(2)
需要注意的是,在概念视图上面有些列是空白的,这样的列实际上并不会被存储,当请求这些空白的单元格时,会返回null值。如果在查询的时候不
提供时间戳,那么会返回距离现在最近的那一个版本的数据,因为在存储的时候,数据会按照时间戳来排序。
通过shell操作hbase 会更清楚结构
这里我们用一个学生成绩表作为例子,对Hbase的基本操作和基本概念进行讲解:
下面是学生的成绩表:
name grad course:math course:art
Tom 1 87 97
Jerry 2 100 80
这里grad对于表来说是一个列,course对于表来说是一个列族,这个列族由两个列组成:math和art,当然我们可以根据我们的需要在course中建立更多的列族,如computer,physics等相应的列添加入course列族.
有了上面的想法和需求,我们就可以在Hbase中建立相应的数据表啦!
1, 建立一个表格 scores 具有两个列族grad 和courese
hbase(main):002:0> create 'scores', 'grade', 'course'
0 row(s) in 4.1610 seconds
2,查看当先Hbase中具有哪些表
hbase(main):003:0> list
scores
1 row(s) in 0.0210 seconds
3,查看表的构造
hbase(main):004:0> describe 'scores'
{NAME => 'scores', IS_ROOT => 'false', IS_meta => 'false', FAMILIES => [{NAME => 'course', BLOOMFILTER => 'false', IN_MEMORY => 'false', LENGTH => '2147483647', BLOCKCACHE => 'false', VERSIONS => '3', TTL => '-1', COMPRESSION => 'NONE'}, {NAME => 'grade', BLOOMFILTER => 'false', IN_MEMORY => 'false', LENGTH => '2147483647', BLOCKCACHE => 'false', VERSIONS => '3', TTL => '-1', COMPRESSION => 'NONE'}]}
1 row(s) in 0.0130 seconds
4, 加入一行数据,行名称为 Tom 列族grad的列名为”” 值位1
hbase(main):005:0> put 'scores', 'Tom', 'grade:', '1'
0 row(s) in 0.0070 seconds
5,给Tom这一行的数据的列族添加一列 <math,87>
hbase(main):006:0> put 'scores', 'Tom', 'course:math', '87'
0 row(s) in 0.0040 seconds
6,给Tom这一行的数据的列族添加一列 <art,97>
hbase(main):007:0> put 'scores', 'Tom', 'course:art', '97'
0 row(s) in 0.0030 seconds
7, 加入一行数据,行名称为 Jerry 列族grad的列名为”” 值位2
hbase(main):008:0> put 'scores', 'Jerry', 'grade:', '2'
0 row(s) in 0.0040 seconds
8,给Jerry这一行的数据的列族添加一列 <math,100>
hbase(main):009:0> put 'scores', 'Jerry', 'course:math', '100'
0 row(s) in 0.0030 seconds
9,给Jerry这一行的数据的列族添加一列 <art,80>
hbase(main):010:0> put 'scores', 'Jerry', 'course:art', '80'
0 row(s) in 0.0050 seconds
10,查看scores表中Tom的相关数据
hbase(main):011:0> get 'scores', 'Tom'
COLUMN CELL
course:art timestamp=1224726394286, value=97
course:math timestamp=1224726377027, value=87
grade: timestamp=1224726360727, value=1
3 row(s) in 0.0070 seconds
11,查看scores表中所有数据
hbase(main):012:0> scan 'scores'
ROW COLUMN+CELL
Tom column=course:art, timestamp=1224726394286, value=97
Tom column=course:math, timestamp=1224726377027, value=87
Tom column=grade:, timestamp=1224726360727, value=1
Jerry column=course:art, timestamp=1224726424967, value=80
Jerry column=course:math, timestamp=1224726416145, value=100
Jerry column=grade:, timestamp=1224726404965, value=2
6 row(s) in 0.0410 seconds
12,查看scores表中所有数据courses列族的所有数据
hbase(main):013:0> scan 'scores', ['course:']
ROW COLUMN+CELL
Tom column=course:art, timestamp=1224726394286, value=97
Tom column=course:math, timestamp=1224726377027, value=87
Jerry column=course:art, timestamp=1224726424967, value=80
Jerry column=course:math, timestamp=1224726416145, value=100
4 row(s) in 0.0200 seconds
上面就是Hbase的基本shell操作的一个例子,可以看出,hbase的shell还是比较简单易用的,从中也可以看出Hbase shell缺少很多传统sql中的一些类似于like等相关操作,当然,Hbase作为BigTable的一个开源实现,而BigTable是作为 google业务的支持模型,很多sql语句中的一些东西可能还真的不需要.
Hbase是一个分布式的、面向列的数据库,它和一般关系型数据库的最大区别是:Hbase很适合于存储非结构化的数据,还有就是它基于列的而不是基于行的模式。
既然Hbase是采用KeyValue的列存储,那Rowkey就是KeyValue的Key了,表示唯一一行。Rowkey也是一段二进制码流,最大长度为64KB,内容可以由使用的用户自定义。数据加载时,一般也是根据Rowkey的二进制序由小到大进行的。
Hbase是根据Rowkey来进行检索的,系统通过找到某个Rowkey (或者某个 Rowkey 范围)所在的Region,然后将查询数据的请求路由到该Region获取数据。Hbase的检索支持3种方式:
(1) 通过单个Rowkey访问,即按照某个Rowkey键值进行get操作,这样获取唯一一条记录;
(2) 通过Rowkey的range进行scan,即通过设置startRowKey和endRowKey,在这个范围内进行扫描。这样可以按指定的条件获取一批记录;
(3) 全表扫描,即直接扫描整张表中所有行记录。
Hbase按单个Rowkey检索的效率是很高的,耗时在1毫秒以下,每秒钟可获取1000~2000条记录,不过非key列的查询很慢。
Table中Family和Qualifier的关系与区别
就像用MySQL一样,我们要做的是表设计,MySQL中的表,行,列的在Hbase已经有所区别了,在Hbase中主要是Table和Family和Qualifier,这三个概念。Table可以直接理解为表,而Family和Qualifier其实都可以理解为列,一个Family下面可以有多个Qualifier,所以可以简单的理解为,Hbase中的列是二级列,也就是说Family是第一级列,Qualifier是第二级列。两个是父子关系。
测试发现:
在实际应用场景中,对于单column qualifier和多column qualifier两种情况,
如果value长度越长,row key长度越短,字段数(column qualifier数)越少,前者和后者在实际传输数据量上会相差小些;反之则相差较大。
如果采用多column qualifier的方式存储,且客户端采取批量写入的方式,则可以根据实际情况,适当增大客户端的write buffer大小,以便能够提高客户端的写入吞吐量。
从性能的角度谈table中family和qualifier的设置 对于传统关系型中的一张table,在业务转换到hbase上建模时,从性能的角度应该如何设置family和qualifier呢? 最极端的,①每一列都设置成一个family,②一个表仅有一个family,所有列都是其中的一个qualifier,那么有什么区别呢? 从读的方面考虑: family越多,那么获取每一个cell数据的优势越明显,因为io和网络都减少了。 如果只有一个family,那么每一次读都会读取当前rowkey的所有数据,网络和io上会有一些损失。 当然如果要获取的是固定的几列数据,那么把这几列写到一个family中比分别设置family要更好,因为只需一次请求就能拿回所有数据。 从写的角度考虑: 首先,内存方面来说,对于一个Region,会为每一个表的每一个Family分配一个Store,而每一个Store,都会分配一个MemStore,所以更多的family会消耗更多的内存。 其次,从flush和compaction方面说,目前版本的hbase,在flush和compaction都是以region为单位的,也就是说当一个family达到flush条件时,该region的所有family所属的memstore都会flush一次,即使memstore中只有很少的数据也会触发flush而生成小文件。这样就增加了compaction发生的机率,而compaction也是以region为单位的,这样就很容易发生compaction风暴从而降低系统的整体吞吐量。 第三,从split方面考虑,由于hfile是以family为单位的,因此对于多个family来说,数据被分散到了更多的hfile中,减小了split发生的机率。这是把双刃剑。更少的split会导致该region的体积比较大,由于balance是以region的数目而不是大小为单位来进行的,因此可能会导致balance失效。而从好的方面来说,更少的split会让系统提供更加稳定的在线服务。而坏处我们可以通过在请求的低谷时间进行人工的split和balance来避免掉。 因此对于写比较多的系统,如果是离线应该,我们尽量只用一个family好了,但如果是在线应用,那还是应该根据应用的情况合理地分配family。
首先,不同的family是在同一个region下面。而每一个family都会分配一个memstore,所以更多的family会消耗更多的内存。其次,目前版本的hbase,在flush和compaction都是以region为单位的,也就是说当一个family达到flush条件时,该region的所有family所属的memstore都会flush一次,即使memstore中只有很少的数据也会触发flush而生成小文件。这样就增加了compaction发生的机率,而compaction也是以region为单位的,这样就很容易发生compaction风暴从而降低系统的整体吞吐量。第三,由于hfile是以family为单位的,因此对于多个family来说,数据被分散到了更多的hfile中,减小了split发生的机率。这是把双刃剑。更少的split会导致该region的体积比较大,由于balance是以region的数目而不是大小为单位来进行的,因此可能会导致balance失效。而从好的方面来说,更少的split会让系统提供更加稳定的在线服务。上述第三点的好处对于在线应用来说是明显的,而坏处我们可以通过在请求的低谷时间进行人工的split和balance来避免掉。因此对于写比较多的系统,如果是离线应该,我们尽量只用一个family好了,但如果是在线应用,那还是应该根据应用的情况合理地分配family。
Rowkey是一个二进制码流,Rowkey的长度被很多开发者建议说设计在10~100个字节,不过建议是越短越好,不要超过16个字节。
原因如下:
(1)数据的持久化文件HFile中是按照KeyValue存储的,如果Rowkey过长比如100个字节,1000万列数据光Rowkey就要占用100*1000万=10亿个字节,将近1G数据,这会极大影响HFile的存储效率;
(2)MemStore将缓存部分数据到内存,如果Rowkey字段过长内存的有效利用率会降低,系统将无法缓存更多的数据,这会降低检索效率。因此Rowkey的字节长度越短越好。
(3)目前操作系统是都是64位系统,内存8字节对齐。控制在16个字节,8字节的整数倍利用操作系统的最佳特性。
如果Rowkey是按时间戳的方式递增,不要将时间放在二进制码的前面,建议将Rowkey的高位作为散列字段,由程序循环生成,低位放时间字段,这样将提高数据均衡分布在每个Regionserver实现负载均衡的几率。如果没有散列字段,首字段直接是时间信息将产生所有新数据都在一个 RegionServer上堆积的热点现象,这样在做数据检索的时候负载将会集中在个别RegionServer,降低查询效率。
必须在设计上保证其唯一性。
基于Rowkey的上述3个原则,应对不同应用场景有不同的Rowkey设计建议。
事务数据是带时间属性的,建议将时间信息存入到Rowkey中,这有助于提示查询检索速度。对于事务数据建议缺省就按天为数据建表,这样设计的好处是多方面的。按天分表后,时间信息就可以去掉日期部分只保留小时分钟毫秒,这样4个字节即可搞定。加上散列字段2个字节一共6个字节即可组成唯一 Rowkey。如下图所示:
这样的设计从操作系统内存管理层面无法节省开销,因为64位操作系统是必须8字节对齐。但是对于持久化存储中Rowkey部分可以节省25%的开销。也许有人要问为什么不将时间字段以主机字节序保存,这样它也可以作为散列字段了。这是因为时间范围内的数据还是尽量保证连续,相同时间范围内的数据查找的概率很大,对查询检索有好的效果,因此使用独立的散列字段效果更好,对于某些应用,我们可以考虑利用散列字段全部或者部分来存储某些数据的字段信息,只要保证相同散列值在同一时间(毫秒)唯一。
统计数据也是带时间属性的,统计数据最小单位只会到分钟(到秒预统计就没意义了)。同时对于统计数据我们也缺省采用按天数据分表,这样设计的好处无需多说。按天分表后,时间信息只需要保留小时分钟,那么0~1400只需占用两个字节即可保存时间信息。由于统计数据某些维度数量非常庞大,因此需要4个字节作为序列字段,因此将散列字段同时作为序列字段使用也是6个字节组成唯一Rowkey。如下图所示:
同样这样的设计从操作系统内存管理层面无法节省开销,因为64位操作系统是必须8字节对齐。但是对于持久化存储中Rowkey部分可以节省25%的开销。预统计数据可能涉及到多次反复的重计算要求,需确保作废的数据能有效删除,同时不能影响散列的均衡效果,因此要特殊处理。
通用数据采用自增序列作为唯一主键,用户可以选择按天建分表也可以选择单表模式。这种模式需要确保同时多个入库加载模块运行时散列字段(序列字段)的唯一性。可以考虑给不同的加载模块赋予唯一因子区别。设计结构如下图所示。
Hbase按指定的条件获取一批记录时,使用的就是scan方法。 scan方法有以下特点:
(1)scan可以通过setCaching与setBatch方法提高速度(以空间换时间);
(2)scan可以通过setStartRow与setEndRow来限定范围。范围越小,性能越高。
通过巧妙的RowKey设计使我们批量获取记录集合中的元素挨在一起(应该在同一个Region下),可以在遍历结果时获得很好的性能。
(3)scan可以通过setFilter方法添加过滤器,这也是分页、多条件查询的基础。
在满足长度、三列、唯一原则后,我们需要考虑如何通过巧妙设计RowKey以利用scan方法的范围功能,使得获取一批记录的查询速度能提高。下例就描述如何将多个列组合成一个RowKey,使用scan的range来达到较快查询速度。
例子:
我们在表中存储的是文件信息,每个文件有5个属性:文件id(long,全局唯一)、创建时间(long)、文件名(String)、分类名(String)、所有者(User)。
我们可以输入的查询条件:文件创建时间区间(比如从20120901到20120914期间创建的文件),文件名(“中国好声音”),分类(“综艺”),所有者(“浙江卫视”)。
假设当前我们一共有如下文件:
这里UserID应该对应另一张User表,暂不列出。我们只需知道UserID的含义:
1代表 浙江卫视; 2代表 好声音剧组; 3代表 XX微博; 4代表赞助商。调用查询接口的时候将上述5个条件同时输入find(20120901,20121001,”中国好声音”,”综艺”,”浙江卫视”)。此时我们应该得到记录应该有第1、2、3、4、5、7条。第6条由于不属于“浙江卫视”应该不被选中。我们在设计RowKey时可以这样做:采用 UserID + CreateTime + FileID组成RowKey,这样既能满足多条件查询,又能有很快的查询速度。
需要注意以下几点:
(1)每条记录的RowKey,每个字段都需要填充到相同长度。假如预期我们最多有10万量级的用户,则userID应该统一填充至6位,如000001,000002…
(2)结尾添加全局唯一的FileID的用意也是使每个文件对应的记录全局唯一。避免当UserID与CreateTime相同时的两个不同文件记录相互覆盖。
按照这种RowKey存储上述文件记录,在Hbase表中是下面的结构:
rowKey(userID 6 + time 8 + fileID 6) name category ….
00000120120902000001
00000120120904000002
00000120120906000003
00000120120908000004
00000120120910000005
00000120120914000007
00000220120912000006
00000220120916000008
00000320120918000009
00000420120920000010
怎样用这张表?
在建立一个scan对象后,我们setStartRow(00000120120901),setEndRow(00000120120914)。
这样,scan时只扫描userID=1的数据,且时间范围限定在这个指定的时间段内,满足了按用户以及按时间范围对结果的筛选。并且由于记录集中存储,性能很好。
然后使用 SingleColumnValueFilter(org.apache.hadoop.hbase.filter.SingleColumnValueFilter),共4个,分别约束name的上下限,与category的上下限。满足按同时按文件名以及分类名的前缀匹配。
(注意:使用SingleColumnValueFilter会影响查询性能,在真正处理海量数据时会消耗很大的资源,且需要较长的时间)
如果需要分页还可以再加一个PageFilter限制返回记录的个数。
以上,我们完成了高性能的支持多条件查询的Hbase表结构设计。
- 加盐
- 哈希
- 反转
第三种防止热点的方法时反转固定长度或者数字格式的rowkey。这样可以使得rowkey中经常改变的部分(最没有意义的部分)放在前面。这样可以有效的随机rowkey,但是牺牲了rowkey的有序性。
- 时间戳反转