dcddc

西米大人的博客

0%

HBase

介绍

当数据积累越来越多,他们需要一个简单的方式来扩展整个系统而不是重新构建。

从19世纪70年代开始,关系型数据库 (RDBMS)几乎统治了数据管理场景。

但是当业务不断扩大,存储和处理的数据量也不断增长,关系型数据库越来越难以扩展。
假如你明白或者认为你要面对海量数据存储需求,有数十亿行及数百万列的情况,你应该考虑HBase。

这类新数据库设计初衷就是在商业服务器集群中能够完成从基础建设到水平扩展阶段,而不需要垂直扩展设法去买更高级的机器(而且最终还是可能没法买到更好的机器)。

初入HBase

HBase是一个能够提供实时、随机读写,能够存储数十亿行和数百万列的数据库。它设计是要运行于一个商业服务器的集群之上,当新服务器添加之后能够自动扩展,还能保证同样的性能。
有很高的容错性,因为数据是分割至服务器集群中,保存在一个冗余的文件系统比如HDFS。当某些服务器异常时,你的数据仍然是安全的。这些数据会在当前活动的服务器中自动均衡直到替换服务器上线。
HBase是高一致性的数据存储。你修改的内容能够马上在其他所有的客户前展示。

HBase实质是一个带有自动数据版本控制的键值(key-value)存储。

当在HBase中进行数据扫描时,数据行总是按照行主键(row key)顺序返回。

每行数据都由一个唯一排序后的行主键(可以认为是关系型数据库的主键)和任意数量的列,每列都属于一个列簇(column family)并且包含一个或多个版本的值。值都是简单的二进制数组,根据应用需要可以转换成需要展示或存储的形式。

总结

HBase是非关系型、强一致性、自动数据版本控制的分布式键值数据库。

上手

如何通过命令行使用HBase。
HBase自带基于JRuby开发的shell工具,能够定义和管理表、对数据执行增删改查操作、扫描表以及执行一些相关的维护。
HBase在产品环境中应该部署到服务器集群中,但也可以下载下来然后启动运行一个单机模式,只需要花几分钟时间。第一件要做的事情就是使用HBase的shell。下面的示例中演示了通过shell新建一个博客表、展示HBase中的有效表、添加一个博客实体、查询该实体以及扫描博客表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
$ bin/hbase shell
HBase Shell; enter 'help<RETURN>' for list of supported commands.
Type "exit<RETURN>" to leave the HBase Shell
Version 0.96.0-hadoop2, r1531434, Fri Oct 11 15:28:08 PDT 2013

hbase(main):001:0> create 'blog', 'info', 'content'
0 row(s) in 6.0670 seconds

=> Hbase::Table - blog

hbase(main):002:0> list
TABLE
blog
fakenames
my-table
3 row(s) in 0.0300 seconds

=> ["blog", "fakenames", "my-table"]

hbase(main):003:0> put 'blog', '20130320162535', 'info:title', 'Why use HBase?'
0 row(s) in 0.0650 seconds

hbase(main):004:0> put 'blog', '20130320162535', 'info:author', 'Jane Doe'
0 row(s) in 0.0230 seconds

hbase(main):005:0> put 'blog', '20130320162535', 'info:category', 'Persistence'
0 row(s) in 0.0230 seconds

hbase(main):006:0> put 'blog', '20130320162535', 'content:', 'HBase is a column-oriented...'
0 row(s) in 0.0220 seconds

hbase(main):007:0> get 'blog', '20130320162535'
COLUMN CELL
content: timestamp=1386556660599, value=HBase is a column-oriented...
info:author timestamp=1386556649116, value=Jane Doe
info:category timestamp=1386556655032, value=Persistence
info:title timestamp=1386556643256, value=Why use HBase?
4 row(s) in 0.0380 seconds

hbase(main):008:0> scan 'blog', { STARTROW => '20130300', STOPROW => '20130400' }
ROW COLUMN+CELL
20130320162535 column=content:, timestamp=1386556660599, value=HBase is a column-oriented...
20130320162535 column=info:author, timestamp=1386556649116, value=Jane Doe
20130320162535 column=info:category, timestamp=1386556655032, value=Persistence
20130320162535 column=info:title, timestamp=1386556643256, value=Why use HBase?
1 row(s) in 0.0390 seconds

上面的命令中,我们首先新建了一个包含列簇info 和 content的博客表。列出所有的表并且看到我们新建的博客表以后,我们向表中添加了一些数据。put命令指定了表名,唯一行主键,列簇的主键由列簇名和限定名(qualifier)组成,例如info是列簇名,而title和author就是限定名。所以,info:title就指向在列簇info中值为“Why use HBase?”的列title,info:title同样也被作为列主键。接下来,我们使用命令查询一行单独数据,并且最终在一个限定的行主键范围内扫描了博客表数据。指定了开始行20130300(包含)和结束行20130400 (不包含),和你预想的一样,我们能够查询到在此范围内的所有数据。上面博客的例子中,因为行主键就是发布的时间,所以实际上包含了所有2013三月份的数据。

HBase的一个重要特性就是,你定义了列簇,然后根据列限制名,可以再列簇中添加任意数量的列。HBase优化了磁盘的列存储方式,不存在的列不会占用空间,这样使得存储更有效率。而关系型数据库缺必须保存一个空值(null)数据。数据行是由包含的列组成的,所以如果行中没有任何列理论上它是不存在的。接着上面的列子,下面会从一个数据行中删除一些指定的列。

1
2
3
4
5
6
7
8
9
hbase(main):009:0>  delete 'blog', '20130320162535', 'info:category'
0 row(s) in 0.0490 seconds

hbase(main):010:0> get 'blog', '20130320162535'
COLUMN CELL
content: timestamp=1386556660599, value=HBase is a column-oriented...
info:author timestamp=1386556649116, value=Jane Doe
info:title timestamp=1386556643256, value=Why use HBase?
3 row(s) in 0.0260 seconds

如上所示,你能够从表中删除一个指定列如info:category。你也可以使用deleteall命令删除一行中的所有列,从而删除这行数据。更新数据的话,只需要再次使用put命令即可。HBase默认会保持单列三个版本的数据,所以假如你向 info:title put了一个新值,HBase会同时保留新旧两个值

上面例子中的命令展示了如何在HBase中增、删、改、查数据。数据查询只有两种方式:使用get命令查询单行数据;通过scan查询多行数据。在HBase中查询数据时,你应当注意只查询你需要的信息。由于HBase是从每个列簇中分别获取数据,如果你只需要一个列簇的数据,就能够指定只获取该部分。下面的例子中,我们只查询博客 title 列,指定行主键范围为2013年3月到4月。

1
2
3
4
hbase(main):011:0> scan 'blog', { STARTROW => '20130300', STOPROW => '20130500', COLUMNS => 'info:title' }
ROW COLUMN+CELL
20130320162535 column=info:title, timestamp=1386556643256, value=Why use HBase?
1 row(s) in 0.0290 seconds

通过设置行主键范围、限制需要的列名称、需要查询的数据版本,你能够优化HBase的数据访问。当然上面的例子中,全都是通过shell完成的,你也能够使用HBase的API完成相同甚至更多的事情。

架构

HBase采用Master/Slave架构搭建集群,它隶属于Hadoop生态系统,由一下类型节点组成:HMaster节点、HRegionServer节点、ZooKeeper集群,而在底层,它将数据存储于HDFS中,因而涉及到HDFS的NameNode、DataNode等

HRegion

HBase使用RowKey将表水平切割成多个HRegion,从HMaster的角度,每个HRegion都纪录了它的StartKey和EndKey(第一个HRegion的StartKey为空,最后一个HRegion的EndKey为空),由于RowKey是排序的,因而Client可以通过HMaster快速的定位每个RowKey在哪个HRegion中。

HMaster对表中的数据按照RowKey分割成几部分,每一部分就是一个HRegion

HRegionServer

HRegion由HMaster分配到相应的HRegionServer中,然后由HRegionServer负责HRegion的启动和管理,和Client的通信,负责数据的读(使用HDFS)。每个HRegionServer可以同时管理1000个左右的HRegion

HMaster

HMaster没有单点故障问题,可以启动多个HMaster,通过ZooKeeper的Master Election机制保证同时只有一个HMaster出于Active状态,其他的HMaster则处于热备份状态。

一般情况下会启动两个HMaster,非Active的HMaster会定期的和Active HMaster通信以获取其最新状态,从而保证它是实时更新的,如果启动了多个HMaster反而增加了Active HMaster的负担。

HMaster的主要用于HRegion的分配和管理,DDL的实现等,既它主要有两方面的职责:

  • 协调HRegionServer
  • 启动时HRegion的分配,以及负载均衡和修复时HRegion的重新分配。
  • 监控集群中所有HRegionServer的状态(通过Heartbeat和监听ZooKeeper中的状态)。

总结

HMaster相当于主控节点,负责分配HRegion以及管理HRegionMaster
HRegionMaster相当于存储节点,与Client打交道读取数据
HRegion相当于表中的一块数据区

ZooKeeper

ZooKeeper为HBase集群提供协调服务,它管理着HMaster和HRegionServer的状态(available/alive等),并且会在它们宕机时通知给HMaster,从而HMaster可以实现HMaster之间的failover,或对宕机的HRegionServer中的HRegion集合的修复(将它们分配给其他的HRegionServer)。ZooKeeper集群本身使用一致性协议(PAXOS协议)保证每个节点状态的一致性。

ZooKeeper协调集群所有节点的共享信息,在HMaster和HRegionServer连接到ZooKeeper后创建Ephemeral节点,并使用Heartbeat机制维持这个节点的存活状态

HMaster通过监听ZooKeeper中的Ephemeral节点(默认:/hbase/rs/*)来监控HRegionServer的加入和宕机,如果某个Ephemeral节点实效,则HMaster会收到通知,并做相应的处理。

第一个HMaster连接到ZooKeeper时会创建Ephemeral节点(默认:/hbasae/master)来表示Active的HMaster,其后加进来的HMaster则监听该Ephemeral节点,如果当前Active的HMaster宕机,则该节点消失,因而其他HMaster得到通知,而将自身转换成Active的HMaster,在变为Active的HMaster之前,它会创建在/hbase/back-masters/下创建自己的Ephemeral节点。

HMaster的备份节点会监听ACTIVE HMaster的Ephemeral节点,而ACTIVE HMaster会监听HRegionMaster的Ephemeral节点

HBase读写

META TABLE

在HBase 0.96以后去掉了-ROOT- Table,只剩下这个特殊的目录表叫做Meta Table(hbase:meta),它存储了集群中所有用户HRegion的位置信息,而ZooKeeper的节点中(/hbase/meta-region-server)存储的则直接是这个Meta Table的位置,并且这个Meta Table如以前的-ROOT- Table一样是不可split的。这样,客户端在第一次访问用户Table的流程就变成了:

1、从ZooKeeper(/hbase/meta-region-server)中获取hbase:meta的位置(HRegionServer的位置),缓存该位置信息。
2、从HRegionServer中查询用户Table对应请求的RowKey所在的HRegionServer,缓存该位置信息。
3、从查询到HRegionServer中读取Row。

通过ZooKeeper找到META TABLE,META TABLE存储了用户TABLE和该TABLE所存储的节点HRegionServer的对应关系
通过META TABLE找到存储用户TABLE数据的HRegionServer,通过HRegionServer读写数据

我感觉META TABLE就存在HMaster节点上,因为META TABLE存储的内容其实就是HMaster为用户TABLE记录分配的HRegionInfo

从这个过程中,我们发现客户会缓存这些位置信息,然而第二步它只是缓存当前RowKey对应的HRegion的位置,因而如果下一个要查的RowKey不在同一个HRegion中,则需要继续查询hbase:meta所在的HRegion,然而随着时间的推移,客户端缓存的位置信息越来越多,以至于不需要再次查找hbase:meta Table的信息,除非某个HRegion因为宕机或Split被移动,此时需要重新查询并且更新缓存。

HRegionServer详解

HRegionServer一般和DataNode在同一台机器上运行,实现数据的本地性。HRegionServer包含多个HRegion,由WAL(HLog)、BlockCache、MemStore、HFile组成

WAL:
WAL即Write Ahead Log,在早期版本中称为HLog,它是HDFS上的一个文件,如其名字所表示的,所有写操作都会先保证将数据写入这个Log文件后,才会真正更新MemStore,最后写入HFile中。

采用这种模式,可以保证HRegionServer宕机后,我们依然可以从该Log文件中读取数据,Replay所有的操作,而不至于数据丢失。这个Log文件会定期Roll出新的文件而删除旧的文件(那些已持久化到HFile中的Log可以删除)。

WAL文件存储在/hbase/WALs/${HRegionServer_Name}的目录中(在0.94之前,存储在/hbase/.logs/目录中),一般一个HRegionServer只有一个WAL实例,也就是说一个HRegionServer的所有WAL写都是串行的(就像log4j的日志写也是串行的),这当然会引起性能问题,因而在HBase 1.0之后,通过HBASE-5699实现了多个WAL并行写(MultiWAL),该实现采用HDFS的多个管道写,以单个HRegion为单位。

WAL的用处就是为了容灾,具体来说就是HRegionServer宕机后的数据恢复

BlockCache:
BlockCache是一个读缓存,即“引用局部性”原理(也应用于CPU,分空间局部性和时间局部性,空间局部性是指CPU在某一时刻需要某个数据,那么有很大的概率在下一时刻它需要的数据在其附近;时间局部性是指某个数据在被访问过一次后,它有很大的概率在不久的将来会被再次的访问),将数据预读取到内存中,以提升读的性能。

HBase中提供两种BlockCache的实现:默认on-heap LruBlockCache和BucketCache(通常是off-heap)。通常BucketCache的性能要差于LruBlockCache,然而由于GC的影响,LruBlockCache的延迟会变的不稳定,而BucketCache由于是自己管理BlockCache,而不需要GC,因而它的延迟通常比较稳定,这也是有些时候需要选用BucketCache的原因。

BlockCache作用是提升读数据的性能。注意,WAL和BlockCache都是服务于HRegionServer的,即服务于该Server上存储的所有HRegion数据

HRegion:
HRegion是一个Table中的一个Region在一个HRegionServer中的表达。一个Table可以有一个或多个Region,他们可以在一个相同的HRegionServer上,也可以分布在不同的HRegionServer上,一个HRegionServer可以有多个HRegion,他们分别属于不同的Table。

HStore:
HRegion由多个Store(HStore)构成,每个HStore对应了一个Table在这个HRegion中的一个Column Family,即每个Column Family就是一个集中的存储单元,因而最好将具有相近IO特性的Column存储在一个Column Family,以实现高效读取(数据局部性原理,可以提高缓存的命中率)。HStore是HBase中存储的核心,它实现了读写HDFS功能,一个HStore由一个MemStore 和0个或多个StoreFile组成。

MemStore:
MemStore是一个写缓存(In Memory Sorted Buffer),所有数据的写在完成WAL日志写后,会写入MemStore中,由MemStore根据一定的算法将数据Flush到底层HDFS文件中(HFile),通常每个HRegion中的每个Column Family有一个自己的MemStore。

HFile:
HFile(StoreFile) 用于存储HBase的数据(Cell/KeyValue)。在HFile中的数据是按RowKey、Column Family、Column排序,对相同的Cell(即这三个值都一样),则按timestamp倒序排列。

HStore是个逻辑概念,就如同HRegion一样,底层真正存储数据的是HFile,而MemStore负责将内存的数据flush到HFile
MemStore与HStore对应,表示col family列簇的数据,所以HRegion包含多个HStore,这样划分是为了提升IO性能

HRegionServer写数据

当客户端发起一个Put请求时,首先它从hbase:meta表中查出该Put数据最终需要去的HRegionServer。然后客户端将Put请求发送给相应的HRegionServer,在HRegionServer中它首先会将该Put操作写入WAL日志文件中(Flush到磁盘中)。

写完WAL日志文件后,HRegionServer根据Put中的TableName和RowKey找到对应的HRegion,并根据Column Family找到对应的HStore,并将Put写入到该HStore的MemStore中。此时写成功,并返回通知客户端。

MemStore是一个In Memory Sorted Buffer,在每个HStore中都有一个MemStore,即它是一个HRegion的一个Column Family对应一个实例。它的排列顺序以RowKey、Column Family、Column的顺序以及Timestamp的倒序,如下所示:

每一次Put/Delete请求都是先写入到MemStore中,再Flush成一个新的StoreFile(底层实现是HFile),即一个HStore(Column Family)可以有0个或多个StoreFile(HFile)。有以下三种情况可以触发MemStore的Flush动作,需要注意的是MemStore的最小Flush单元是HRegion而不是单个MemStore。

1、当一个HRegion中的所有MemStore的大小总和超过了hbase.hregion.memstore.flush.size的大小,默认128MB。此时当前的HRegion中所有的MemStore会Flush到HDFS中。

2、当全局MemStore的大小超过了hbase.regionserver.global.memstore.upperLimit的大小,默认40%的内存使用量。此时当前HRegionServer中所有HRegion中的MemStore都会Flush到HDFS中,Flush顺序是MemStore大小的倒序,直到总体的MemStore使用量低于hbase.regionserver.global.memstore.lowerLimit,默认38%的内存使用量。

触发MemStore flush到HFile的条件,不是MemStore负载,而是HRegion或者HRegionServer设置的负载条件

Java API

我们使用HBase的Java API来建表,插入数据以及按照行主键查询数据。我们也会建立一个限制列范围的基本表查询,以及使用过滤器进行分页查询。

之前学习了HBase的整体架构,现在了解一下我们的应用如何通过Java API与HBase进行交互。如同之前提到的,你也同样可以通过其他的RPC(Remote Procedure Call)技术手段与HBase交互,比如Apache Thrift通过REST网关的方式,但我们主要使用Java API的方式。API提供了DDL(数据定义语言)和DML(数据操作语言)你会发现和关系型数据库SQL的语言很相似。假设我们要存储用户信息,我们先开始建立一张新表。下列代码展示如何使用HBaseAdmin类。

1
2
3
4
5
6
7
Configuration conf = HBaseConfiguration.create();
HBaseAdmin admin = new HBaseAdmin(conf);
HTableDescriptor tableDescriptor = new HTableDescriptor(TableName.valueOf("people"));
tableDescriptor.addFamily(new HColumnDescriptor("personal"));
tableDescriptor.addFamily(new HColumnDescriptor("contactinfo"));
tableDescriptor.addFamily(new HColumnDescriptor("creditcard"));
admin.createTable(tableDescriptor);

下面的代码展示了如何使用Put类插入John Doe的数据,指定名称和电子邮件指定(为了简单期间,这里忽略了通常应该有的错误处理)

1
2
3
4
5
6
7
8
9
10
Configuration conf = HBaseConfiguration.create();
HTable table = new HTable(conf, "people");
Put put = new Put(Bytes.toBytes("doe-john-m-12345"));
put.add(Bytes.toBytes("personal"), Bytes.toBytes("givenName"), Bytes.toBytes("John"));
put.add(Bytes.toBytes("personal"), Bytes.toBytes("mi"), Bytes.toBytes("M"));
put.add(Bytes.toBytes("personal"), Bytes.toBytes("surame"), Bytes.toBytes("Doe"));
put.add(Bytes.toBytes("contactinfo"), Bytes.toBytes("email"), Bytes.toBytes("john.m.doe@gmail.com"));
table.put(put);
table.flushCommits();
table.close();

上面的代码中示例是Put类提供了唯一行主键作为构造方法参数。接下来我们会添加值,必须包括列簇、列标识符、二进制数组形式的值。
或许你会注意到,HBase API的常用工具类中的Bytes类是经常用到的,它提供了一些方法能够在原始类型、字符串与二进制数组间转换。(添加一个toBytes()静态引用方法能够节省大堆代码)接下来我们将数据存入表中,刷新提交确认本地缓存的改变能够生效,最终关闭表。更新数据也和之前展示的代码方式相同。
与关系型数据库不同,HBase即使只有一列改变也必须更新整行数据。假如你只需要更新一列,只需要在Put类和HBase中指定需要更新的列。也会有确认并更新的动作,本质上就是一系列并发操作,只是在用户确认待替换的值之后才进行更新动作。

如下面的代码所示,使用Get类来查询我们刚刚创建完成的数据。(从这里开始,会忽略一些代码如构建配置,实例化HTable、提交及关闭)

1
2
3
4
Get get = new Get(Bytes.toBytes("doe-john-m-12345"));
get.addFamily(Bytes.toBytes("personal"));
get.setMaxVersions(3);
Result result = table.get(get);

上面的代码中实例化了Get类,并且提供了待查询的行主键。接下来我们通过 addFamily 方法告知HBase:我们只需要从个人信息列簇中获取数据。这样能够减少HBase在读取数据时与磁盘的交互。我们还指定了结果中每列最多保存三个版本,这样就能列出每列的历史数据。最终会返回一个结果实例,包含所有可以查看的返回值列。

很多情况下你需要查询多行数据,HBase使用扫描行来实现。正如在第二篇在HBase的shell工具中执行scan,下面主要讨论Scan类。Scan类支持多种条件选项,比如待查询的行主键范围、需要包含的列和列簇、以及需要展示的最大数据版本。你也可以添加一个过滤器,通过自定义过滤逻辑限制需要返回哪些行和列。过滤器的常用场景就是分页,例如我们可能想要获取所有的姓Smith的人,每次一页25人。下面的代码展示了如何使用基本的scan方法。

1
2
3
4
5
6
7
8
Scan scan = new Scan(Bytes.toBytes("smith-"));
scan.addColumn(Bytes.toBytes("personal"), Bytes.toBytes("givenName"));
scan.addColumn(Bytes.toBytes("contactinfo"), Bytes.toBytes("email"));
scan.setFilter(new PageFilter(25));
ResultScanner scanner = table.getScanner(scan);
for (Result result : scanner) {
// ...
}