本文共 5611 字,大约阅读时间需要 18 分钟。
在HBase中有两类基本的键结构:行健和列键
这两者都存储着有意义的信息,不仅仅是该键对应的值:
- 键本身存储的内容
- 键的排列顺序
在HBase中,键的排序顺序是十分重要的
如之前所说,HBase的键分为两种:
- 列键:包括了列族名和限定符,定位到列的索引
- 行健:相当于关系型数据库中的主键,通过行健得到逻辑布局中一行的所有列
如上图所示,逻辑上用户设置的每一行都没放在一起,但是实际存储的时候每个列族的以单独的文件存储的,不同列族的单元格绝对不会出现在同一个StoreFile中
同时,HBase也不会存储值为空的单元格,所以磁盘上的文件也只有这些有值的单元格每个单元格在实际存储的时候也保存了行健和列键,所以每个单元格都单独存储了它在表中所处位置的关键信息
同一个单元格的不同版本被单独存储为连续的单元格,单元格按照时间戳降序排列,所以默认读取的是最新的单元格数据
同一列族的单元格在存储的时候先按行健排序,当一行中有多个单元格的时候再按列键排序,同一单元格有多个版本的时候按时间戳排序
根据以上的存储特性,建议在查询的时候指定列族信息可以有效减少查询的存储文件,提高效率
下图表示了查询数据的时候指定各种不同的筛选条件时会读取到存储文件的范围信息:
值得注意的一点是:在图1中左下角的位置,展示的是“数据位移”,对于一个KeyValue来说,筛选的效率从左至右明显下降
所以在设计的时候用户可以考虑将一些重要的筛选信息左移到合适的位置,从而在不改变数据量的情况下改变数据的排序病提供查询性能HBase中的表可以设计为两类:高表和宽表
顾名思义,高表就是列少行多,宽表则反之
通过之前我们所描述的信息来看,除了行健和列键之外,指定其余的筛选粒度信息效率都不是很高,由于列键是由列族名和限定符组成的,属于定位具体StoreFile的,所以我们应该将需要查询的维度或者信息存储在行健中,因为用它来筛选数据效率最高!
那么,在设计表结构的时候,我们如何知道设计成高表还是宽表呢?
举一个简单的例子来说明:
在一个电子邮箱系统中,将行健设置为用户的唯一ID,其余属性作为列族或者列族中的列 如此一来,同一个用户的所有邮件信息都会被存储在同一行中(因为他们的行健相同),这在大部分情况下是通用的,但是有些用户的邮件数量非常非常大,大到一行数据就超出了HFile的最大限度 这个时候你可能就会疑问,HFile和Region达到一定大小的时候不是会自动切分的吗? 别忘了,HBase只能按行切分,当行数量达到阈值的时候会根据行健进行切分这样一来,上面的那种宽表设计方式就出现了致命的缺陷:表数据无法拆分,更无法进行HBase的负载均衡等特性
解决这个问题更好的办法是代替宽表,设计一个高表:
我们可以将行健设置为用户Id+单独邮件Id,这样一来,每封邮件都是单独的一行,将宽表中的行数据释放出来了这样做的好处是:
1.行数量可以更容易的被拆分
2.行健中包含关键的筛选信息(邮件Id被移到了左边),用户可以直接根据行健定位到唯一的一封邮件
综上所述,HBase中高表的性能要优于宽表
但是要使用高表还是宽表还要根据具体的业务场景来确定,因为在以上高表的设计中,同一用户的数据分布在多行中,用户不可能在一个简单的操作中修改一个收件箱的全局属性,因为这涉及到了原子性的操作,如果用户没有一次修改整个收件箱的需求时,这种设计是非常合适的 但是如果用户真的有这种需求,而且需求很大,那么宽表反而更加合适,因为HBase能够保证行级别的原子性在之前的例子中,我们通过用户Id+邮件Id的形式形成了组合键,使得一个用户的所有数据分布到不同的行中,这同时也决定了我们无法通过操作宽表一样来直接获得一个用户整个收件箱的数据
在高表的设计中,如果没有提供足够的维度信息的行健的话,用户是无法精确定位到一个值的
为了解决这个问题,用户可以使用包含部分键的扫描:将扫描操作中键的开始和结束设置为一个用户的Id(终止键应该为该用户Id+1)扫描的范围包括起始键,但不包含结束键,设置用户Id之后,HBase会按字典顺序找到第一个行健的位置(例如:-),读取所有数据直至行健不等于提供的用户Id
如此一来,通过部分键扫描,就可以得到一个用户的所有邮件信息了
用户通过部分键扫描可以设计出非常有效的左对齐索引,根据行健的维度,从左至右依次提供长度相同的索引维度
需要注意的是,要保证行健中每个字段的值都被补齐到这个字段所设置的长度,这样字典序才会按预期排列(按二进制内容比较并升序排列) 也就是说用户需要为每个字段设定一个固定长度来保证每个字段比较时只会与同字段内容从左向右比较根据邮件系统这个例子,下表给出了可能的起始键及其意义:
起始键 | 描述 |
---|---|
userId | 给定用户Id下的所有信息 |
userId-data | 给定用户Id下的特定日期的所有信息 |
userId-data-messageId | 给定用户Id下的特定日期下的指定信息 |
这种组合键提供的其实和关系型数据库中的where条件功能类似,用户可以控制每个字段的内容以达到控制段内排序的目的
例如,用户也可以将时间转化为Unix格式之后添加入组合键中,使得数据按照日期进行排序HBase中分页的效果可以由两种方式来实现:
1.利用行健
2.使用分页过滤器
如果只是简单的分页,推荐使用分页过滤器,因为其可以避免传输多余的数据
使用行健进行分页操作的步骤如下:
1.设置起始键(例如某个用户Id)
2.客户端设置offset的行数 3.客户端设置limit数目的行 4.返回数据并关闭扫描器
例如某个用户的邮件很多的时候,需要分页浏览,limit设置为50,初始的时候offset设置为0
通过用户Id扫描得到该用户下的所有邮件的前50封,当用户点击next的时候offset设置为50,代表跳过前50行返回第51-100行当时间序列被当做行健使用时,在存储的时候出现一个问题:
由于时间序列的连续性,这些数据都会被集中存储在一台或者几台服务器上,势必会造成读写热点的问题下面提供几种解决时间序列特性的数据产生数据热点的解决思路:
例如下面的公式:
byte prefix = (byte)(Long.hashCode(timestamp) %)byte[] rowKey = Bytes.add(Bytes.toBytes(prefix),Bytes.toBytes(timestamp))
使用hash函数产生的前缀添加到行健中,将使得整个数据被分散到不同的服务器上
但是这么做也有一个缺点:当用户要扫描一定范围内的数据时可能需要对很对region服务器发起请求 但从另外一个角度来看,用户也可以多线程的并行读取数据其实刚刚所讨论的行健添加前缀的方式也属于字段变换的一种:将连续增长的时间戳在行健中的位置从第一位变成第二位
- 如果行健中已经包含很多字段了则可以尝试调整他们的位置
- 如果行健中只有时间戳,那么可以尝试提取其他的字段到行健中并调整一个合适的顺序
另外一种比较极端的方式是将整个行健随机化
比如使用一个MD5值作为整个行健的组成这么做的好处很明显:所有的数据都会被随机的、均匀的分散到各个服务器中
缺陷也很明显:对于顺序读将造成很大的麻烦如果用户的数据不需要连续扫描而只需要随机读取,用户就可以使用这种策略
下图总结了各个方案的性能对比:
以上只针对顺序读的情况,用户需要根据自己的业务需求来调整行健的设计
以上的数据默认情况下都会以产生的时间顺序以独立行插入HBase中
但是用户也可以在插入的时候使用时间戳作为列插入到HBase中,进而可以根据该时间列族对数据单独进行排序这么做有什么用呢?
其实上面的话要表达的意思是:我们可以使用列族来建立辅助索引来对数据进行另类的排序继续之前邮件系统的例子,如果使用的是将用户的所有邮件存储在一行中,行健是用户Id
那么在默认情况下,一个用户所看到的邮件列表视图是根据数据产生的时间前后来排序的如果用户需要按照邮件地址或者邮件题目排序查看,我们可以这么做:
单独设置一个列族,里面包含两列,名列为不同的前缀(区分开不同的排序)+邮件地址/邮件题目当用户需要以邮件地址来排序显示的时候,我们可以查询这个列族中的邮件地址列
因为该用户的所有数据都被存放在同一行中,所以在这行的辅助索引列族的邮件地址列中,数据是按照邮件地址这个列名的字典顺序来排序的 所以最终用户得到的结果就是根据邮件地址来排序的数据 同理,对于邮件题目的排序也是一样需要注意的一点是,辅助索引列族中存放的数据主体是什么可以有两种解决方式:
1.存放的并不是真实的数据,而是该数据的行健
2.直接存放真实数据
第一种方式类似关系型数据库中的外键形式
第二种则是反范式化得设计了,虽然会存在数据冗余,但是响应速度快首先确定业务需求比较符合高表还是宽表的设计
1.尽量提取最有查询价值的字段到行健中组成多个维度(注意:行健也不宜太长,最大长度64kb,最好16字节以内)
2.权重越高的字段在行健中应该排在越前面 3.每个字段都保持相同的长度以支持左对齐的部分键扫描
如果因为时间戳作为行健导致部分数据热点的问题可以尝试:
1.时间戳行健添加随机前缀
2.提取其他字段到行健中并调整字段的顺序,使时间戳的影响降低
经常和不经常使用的两类数据放入不同列族中,列族名字尽可能短,注意列族的数量级,避免数据倾斜
另外别忘了查询的时候指定列族信息可以有效减少查询的存储文件 同时宽表中也可以使用列族来完成辅助索引的工作(高表参考下一节的辅助索引中的客户端管理索引)更加具体的行健设计可以参考:
HBase并没有直接提供可以使用的索引方案,但是在某些应用场景中又确实需要辅助索引的支持
辅助索引可以提供除了主坐标(行健、列族和限定符等)之外的查询方式和关系型数据库中的辅助索引意义类似,辅助索引存储了一个新坐标到现有坐标的映射关系
下面列出了一些可行的解决方案
典型的做法就是把一个数据表和一个多个索引表关联起来
当程序写数据表的时候,同时更新索引表 读数据的时候可以直接从数据表读,也可以先从辅助索引表中查找原表的行健,再在原表中读取实际的数据这种做法和上一节时间顺序关系中讨论的辅助索引列族类似
不一样的是,上一节中我们假设了一个用户的所有数据都被存放在一行中,所以我们可以直接建立辅助索引列族 对应到不同的情况,我们也可以通过辅助索引表来完成同样的效果这种情况的优点是整个逻辑都是由客户端代码处理的,用户可以根据需求来设计映射关系
但是缺点很明显:用户不能保证主表和依赖表的一致性,因为HBase只能保证行之间的原子性操作解决这个问题的方案也有很多,例如通过定期的修复程序来删除过时的条目或者增加缺失的条目
但是不管怎么说,都不能保证实时同步的状态开源的带索引的事务型HBase提供了一个不同的解决方案,它扩展了Hbase并增加了特殊的客户端和服务端类的实现
最核心的扩展是增加了用来保证所有辅助索引更新操作一致性的事务功能
并提供了一个查询辅助索引的客户端类这种方案会自动化管理主键和辅助键之间的映射关系,但是可能不支持最新可用的HBase,同事也增加了同步的开销,将导致性能一定量的下降
有兴趣的朋友可以了解ITHBase这个项目
和ITHBase一样,IHBase也提供了自动化管理主键和辅助键映射关系,而且是完全在内存中维护索引
当一个region第一次打开,或者一个memstore被刷写到磁盘时
用户可以通过扫描整个region来建立索引基于这样的特性,IHBase的速度很快,但是同时也需要大量额外的内存来维护索引
类似关系型数据库中的触发器,可以使用其将维每个region载入索引层并维护索引
代码可以利用其透明的便利一个正常的数据表,也可以遍历这张表上的索引视图,并由一个协处理器基类读取关于协处理器的详细介绍请关注后续文章~
使用之前介绍的索引机制用户可以按照行健以外的顺序来遍历数据表
但用户仍然受限于使用键或者过滤器来筛选数据,或者直接遍历数据来查找所需要的内容 一个非常普遍的需求是使用任意关键字来搜索数据如何完成这类需求呢?
这时候我们可能需要集成一个完整的搜索引擎常见的选择是Lucene或者Solr的解决方案
下面列出几个可行的方法需要使用HBase存储数据,同时使用MapReduce任务来建立索引
类似Facebook的一个搜索表的解决方案如下:
- 每个用户在搜索表中都有单独的一行
- 列是可能会被索引的关键字
- 版本是消息ID
- 值包括附加消息,例如词组的位置
这个模式使得用户很容易在收件箱中搜索包含特写关键词的消息
独立于HBase使用Lucene提供了建立索引的类
该类会扫描整个表并建立Lucene索引,最终以HDFS上的目录形式存储 一般情况下,这种方式只是用HBase来存储数据 如果通过Lucene执行一个搜索,通常只返回匹配的行健该方式直接在HBase内部建立搜索索引,同时为用户提供Lucene的API
作者: