一、 HBase中的基本概念
1. HBase就是一个数据库。我们主要可以通过这5个方面描述hbase:
2. 首先它是Bigtable的开源实现,论文中描述Bigtable主要运用于网页索引倒排表数据的存储。HBase和HDFS的模型最初参考的就是BigTable和GFS。搜索关键词google 三篇论文可以搜到论文。
3. 它是分布式数据库——较传统数据库更易扩展、更高可用。这里的高可用通常来描述一个系统经过专门的设计,从而减少停工时间,而保持其服务的高度可用性。
4. HBase使用列式存储,大家都知道传统数据库中至少从逻辑上来说一行数据的所有列都连续保存在同一个文件中,而hbase中,一行数据的不同列族是分开存储的。这里列族是多个列的集合,详细定义稍后给出。课后大家可以思考下列式存储的优势。
5. HBase是一个Nosql数据库,这也意味着HBase不擅长关系处理, 特别是类似join之类的关联操作,如果数据之间的关联操作比较多的话不建议使用HBase;
6. HBase的持久化数据保存于HDFS,可重建的状态数据保存于zookeeper。可以说HBase的最重要的功能是加入了一层逻辑处理,使随机数据操作转化为顺序读写的hdfs操作。
二、 HBase应用场景
(一) 适用场景:
1. 存储大量的数据,上百TBs的数据;
2. 需要很高的吞吐量
3. 在大规模的数据量中进行很好性能的随机访问
4. 需要进行优雅的数据扩展
5. 结构化数据和半结构化的数据存储;
6. 列结构动态变化;
7. 数据需要定时删除;
8. 业务场景简单,没有复杂的交叉表、事务或者关联操作;
(二) 不适用场景:
1. 需要事务的场景,HBase各行之间没有原子性,因此对于事务性的业务场景不适用,但是各行内部是有一定的原子性的
2. 复杂的关系查询与计算,类似join、groupby等关系
3. 多条件查询,根据列内容多个条件查询的场景;
以上场景在HBase中也可以实现,但是效率低下,或者实现逻辑复杂,不建议在HBase中实现;
三、 HBase的基本使用
(一) 数据结构
首先做一个简要的总结:HBase最基本的单位是列。一列或者多列形成行数据,并由唯一的rowkey确定;HBase的主要数据结构包括:表、行、列和单元格;其中列是可动态增加的;
这是一个典型的hbase的应用,在搜索系统中保存网页页面的content、anchor等相关属性信息,下面根据这个表应用来详细介绍这个数据结构中的相关概念;
1. RowKey
RowKey是用来检索HBase数据记录的主要字段,类似于其他数据库中的PrimaryKey一样,是访问HBase数据库的关键;目前访问HBase数据库中的行记录只有三种方式:
a. 通过单个RowKey进行访问;
b. 通过RowKey的范围进行scan;
c. 全表扫描(不推荐);
HBase中的数据存储是按照行键的字典序来进行,实例如下:
图1-2 HBase表的行键
可以看到,HBase表的行键并不是按照长度来进行排序的,而是按照字典序进行排列的,在字典序中,是按照二进制逐字节从左到右依次进行比对,如上图所示,row-1小于row-2,row-20、row-21、row-2111、row-23333,不管row-2xxx后面跟着什么,始终都排在row-1的后面;
行键总是唯一指定的,只出现一次的,一般来说HBase表只支持对rowkey的单一索引,只能过rowkey来定位相关的记录,但是有扩展版的HBase支持了辅助索引的功能;
行键可以是任意的字节数组,不一定是人直接可读的;
2. 列族
一行是由若干列组成的,若干列又构成一个列族,一个列族的所有列都存储在同一个底层的存储文件里,这个存储文件叫做HFile。
列族需要在表创建的时候就定义好,目前HBase对于多列族的支持不是特别好,一般列族的数量只限于几十,实际情况可能还小的多。列族名必须由可打印字符组成。
hbase表中的每个列,都归属与某个列族。列族是表的schema的一部分(而列不是),必须在使用表之前定义。列名都以列族作为前缀。例如courses:history,courses:math都属于courses 这个列族。
访问控制、磁盘和内存的使用统计都是在列族层面进行的。实际应用中,列族上的控制权限能帮助我们管理不同类型的应用:我们允许一些应用可以添加新的基本数据、一些应用可以读取基本数据并创建继承的列族、一些应用则只允许浏览数据(甚至可能因为隐私的原因不能浏览所有数据)。
3. 时间戳
HBase表中通过rowkey和列名确定的一个存储单元成为Cell单元格,每个Cell都可以保存同一字段数据的多个版本,版本通过时间戳来索引。时间戳可以在HBase写入时由客户端显式赋值,也可以由服务端默认赋值。每个Cell中,不同版本的数据按照时间倒序排序,保证最新的数据排在前面。
为了避免数据存在过多的版本造成的管理(包括存储和索引)负担,HBase提供了两种数据版本回收方式:一种是保存数据的最后N个版本,第二种是保存最近一段时间内的版本(比如最近七天)。回收方式可以针对每个列族进行设置。
用户可以指定每个值所能保存的最大版本数
4. Cell单元格
Cell单元格是由{rowkey,column(=family+label),timestamp}唯一确定的单元,单元格中的内容全部是字节码形式存储的。
HBase中数据存储都是以key-value格式存储的,但是是多层的key-value关系。
如下所示:
{
UID1: {
bf:{
102:{
1329088321289:108viafly
}
111:{
1329088321289: 46000234573814
}
118:{
1329088321289: weather
1329088320110: telephone
}
}
af:{
404:{
1329088321289:安徽
}
303:{
1329088321289: weather
}
319:{
1329088321289: 查询天气
}
}
}
这样的结构可以理解成是一个多层的map的结构,每一层都是上面一层KV的value;
(二) 基本命令
1.shell交互
HBase可以通过HBaseshell对HBase集群进行命令行交互,HBaseshell同时提供了客户端和管理功能的操作。下面介绍基本的HBase shell命令:
命令 | 描述 | 示例 |
help | 帮助命令 | 1. 直接输入help,显示所有可用的命令 2. 输入help “命令”,显示特定命令的用法 |
create_namespace |
| create_namespace ‘ns1’ |
disable | 禁用表 | disable ‘t1’ |
drop |
| drop ‘t1’ |
exists |
| exists ‘t1’ |
create |
| 1. create ‘t1’, {NAME =>’f1’, VERSION => 5} 2. create ‘t1’, ‘cf’ |
count | 查看表的行数 | |
delete | 删除单元格数据 | delete ‘t1’, ‘r1’, c1’ |
deleteall | 删除行对应的数据 | Delete all cells in a given row; pass a table name, row, and optionally a column and timestamp. Examples: delete ‘t1’, ‘r1’ |
get | 获得行和单元格内容 | Get row or cell contents; pass table name, row, and optionally a dictionary of column(s), timestamp, timerange and versions. Examples: hbase> get 't1', 'r1'
|
list | 列出表信息 | List all tables in hbase. Optional regular expression parameter could hbase> list
|
Scan | 查看表中所有内容 | hbase> scan ‘UserTable’ |
Alter命令支持对于列族进行属性修改,但是不支持对于表的一些操作,下面介绍一些对于表的操作:
1. 复制表到其他命名空间
disable ‘ns1:t1’
snapshot ‘ns1:t1’,’t1_shot’
clone_snapshot ‘t1_shot’, ‘ns2:t1’
delete_snapshot ‘t1_shot’
enable ‘ns:t1’
2. 重命名表
disable ‘ns1:t1’
snapshot ‘ns1:t1’,’t1_shot’
clone_snapshot ‘t1_shot’, ‘ns1:t2’
delete_snapshot ‘t1_shot’
drop ‘ns1:t1’
2.pig 操作HBase
Pig可以使用HBaseStorage对HBase数据进行读写操作,当读写HBase时,需要告诉Pig具体的表名,列族,列名,可以读某一个列,也可以读整个列族,列族读进来是以MAP的格式,举个例子:
一个表users,有两个列族:user_info和links,user_info有name,email等字段,links包含ID和关系,读这样一个表可以使用:
user_links = load 'hbase://users' using org.apache.pig.backend.hadoop.hbase.HBaseStorage('user_info:name, links:*', '-loadKey true -gt 10000') as (id, name:chararray,links:map[]);
load的是hbase中的表地址,HBaseStorage包含两个参数,一个是要读取的列族或列,第二个参数是一系列的配置;
从HBase中取列操作:
1. 在HBase中列是以{列族:列名}来确定的,上面的例子中user_info:name就是取列族user_info中的name列
2. 还可以取一整个列族,通过{列族:*}来确定,如link:*
3. 还可以匹配某些列,如links:100*匹配100开头的所有列
第二个参数允许用户配置HBaseStorage,可以用来控制key的加载,所有的配置项都在一行,用空格分开,相关选项如下:
pig利用HBaseStorage也可以存储数据到HBase中,存储的时候指定存储的表名,构造参数与加载时候相似
3. Filter的使用
HBase中两种主要的读取方式是scan和get,他们都是通过rowkey进行数据访问,在此基础上可以添加更多的限制条件来减少查询中传输的数据量。get和scan函数都是支持过滤器的,过滤器基本的接口叫Filter,HBase已经有一些现成的Filter类来满足相关的过滤需求,用户还可以通过集成Filter类来定制自己的限制条件;
Filter的使用流程:用户定义一个所需要的过滤器实例,然后将定义好的过滤器实例传递给Get或者Scan实例:
Get.setFilter(filter)
实例化过滤器的时候用户可以提供一些参数来设定过滤器的过滤条件;一个过滤器实现的例子如下
RowFilter是一个过滤器类,在初始化的时候就已经设置了相关的过滤条件:小于等于row-22。
注意:过滤器只能减少在查询中服务端返回客户端的数据量,并不能减少查询中的索引次数,对于全表进行scan,然后依赖rowkey过滤器进行过滤相应的记录,这样并不能提高查询效率,正确的查询应当依赖rowkey来确定对应的记录!
(三) 行键rowkey设计
本节将介绍如何在HBase中存储自己的数据,以及如何设计表结构,HBase中表分为高表和宽表两类,前者指列少而行多,后者正好相反。因为HBase的查询主要依赖rowkey,因此应该尽量将需要查询的维度或者信息放在rowkey中,因为通过rowkey去筛选记录的效率最高。
用户可能需要扫描一定范围的记录,此时可以通过设置起始键和结束键,如果只需要起始键,只需要将终止键设置成相同的键并在末尾加上一些数据,如用户可以把aaaa设为起始键,终止键设为aaaab,此时选取的就是aaaa;
时间序列的表设计:当处理流式数据的时候,最常见的数据就是按照时间序列存储数据,这样的数据有一个特点就是这些数据会有序的存储在一个特定的范围内,由于一个region只能由一个服务器管理,所有的更新插入都会集中在一台服务器上,这样会产生读写热点,造成数据过分集中导致整个系统性能下降,解决这种问题通常的方法是:
1. salting方式
用户可以使用salting前缀来保证数据分散到所有region,如下所示:
prefix =(byte)(Long.hashCode(timestamp)%<num of region>);
rowkey=Bytes.add(Bytes.toByte(prefix),Bytes.toBytes(timestamp);
这种方法是先用一个时间相关的数与region数进行取余计算,然后获得结果与rowkey进行拼接;这样数据就平均的分散到了所有的region上;
这种情况存在一个弊端就是当用户要连续查询的时候,需要对每个region服务器都发起请求,不利于用户进行scan选取连续的键值。
2. 字段交换
用户的rowkey包含多个属性字段,可以将随机性较大的字段放在首位,如果用户的rowkey只包含时间戳,则应当将其他字段从列或者value中提取出来,放在rowkey前段。
3. 随机化
随机化是将rowkey完全随机化,比如MD5计算值,这种方法只适合用户不需要连续的扫描数据只需要随机查询的的情况。
通过上面几种方法可以发现,用户在优化读写性能的同时找到正确的平衡点并不是一件简单的事情,不同的解决方案对读写性能的影响如下图所示:
我们在设计rowkey的时候需要根据自己的在读写性能之间寻找一个平衡点。
HBase的几个设计原则:
1. rowkey
rowkey是HBase的key-value存储中的key,通常使用用户要查询的字段组合起来作为rowkey。可以通过rowkey的设计来满足不同的查询场景:
A. 数字rowkey的从大到小的排序:原生HBase只支持从小到大的排序,这样对于排行榜一类的查询需求很尴尬,可以采用rowkey=integer.MAX_VALUE-rowkey的当时进行转换,在应用层再转回来即可完成查询需求;
B. rowkey的散列原则:如果rowkey是类似时间戳的方式,可以用reverse的方式反转rowkey,使得rowkey大致均衡分布,这样可以使得region的负载均衡。
2. columnfamily
columnfamily尽量少,原因是过多的columnfamily之间会相互影响。
3. column
对于column需要扩展的应用,column可以按照普通的方式设计,但对于列相对固定的应用,最好采用将一行记录封装到一个column中的方式,这样能够节省存储空间。
(四) 索引的使用
HBase没有原生的辅助索引进行支持,但是有些场景仍然需要用到辅助索引,通常的需求是用户需要根据主坐标(rowkey、列族、列名)来查询一个单元格,也可以通过一个其他类型的坐标来完成索引,下面是几种解决方案:
1. 带索引的事务型HBase
2. 带索引的IHbase
3. 协处理器
4. 客户端维护索引
前面两种都是对于HBase的开源扩展形态,都需要额外的JAR文件和配置来支持,而且对于最新版的HBase支持性较差,协处理器是HBase的一个重要特性,可在协处理器介绍中查阅。下面主要介绍客户端维护索引:
在客户端管理索引的一个典型的做法是维护一个数据表和一个(多个)查找/映射表,每当数据更新时,同时也更新查找/映射表,查找/映射表中存储的是数据表记录的rowkey,查找数据时可以直接在数据表中查找,也可以根据查找的条件从查找/映射表中查找数据表的rowkey,然后根据rowkey查找数据。
这种做法的优点是整个逻辑都有客户端维护处理,用户可以定制自己的映射关系,缺点是原子性较差,用户不能保证数据表和查找/映射表的一致性。
(五) 多条件查询
索引的一个基本应用时多条件查询,例如每个用户每天产生的消息用如下rowkey表示:uid-date-MsgId
我们查询一个用户所有天的消息记录可以匹配uid-*,这样设置起始行键查询效率很快,但是,当我们需要查询一天所有用户的消息记录的话就需要类似这样的匹配*-date-*,这样查找的话就是全表扫描,是很不效率的。因此可以维护一个查找/映射表,表中rowkey存为:date-uid-MsgId,单元格为对应rowkey在数据表中的rowkey,这样查询一天所有的用户则可以先从查找/映射表中找一天所有用户的rowkey,然后根据这些rowkey去数据表中查找真实的记录。