过去的几年中,在多种方式的数据存储和查询上,我们都看到了真正意义上的爆炸式发展。其中被称为NoSQL的数据库站在了改革的前沿,正在形成新的持久化存储备选方案。NoSQL的流行很大程度应当归功于如Google、Amazon、Twitter和Facebook等大型公司,因为他们聚集了大量的数据需要进行保存、查询和分析。且越来越多的公司正在收集海量的数据,并且需要能够有效的全部利用来充实自己的业务。例如社交网络需要分析用户直接的社交关系并对关系用户进行推荐,同时几乎每个大型网站都有自己的推荐引擎向你推荐可能想要购买的商品。当数据积累越来越多,他们需要一个简单的方式来扩展整个系统而不是重新构建。
从19世纪70年代开始,关系型数据库 (RDBMS)几乎统治了数据管理场景。但是当业务不断扩大,存储和处理的数据量也不断增长,关系型数据库越来越难以扩展。一开始你可能从单机到主从节点,然后再数据库之上加入一道缓存层用于存储加载热点读写数据。当查询性能降低时,索引(indexes)常常是首先被丢弃的,接下来根据反范式( Denormalization )迅速避免关联(joins),因为该操作会很大程度消耗性能。之后你可能评估最耗性能的一些查询,修改使得这些查询能够变成有效的主键查询,或者将大表中的数据分布到多个数据库切片中(shards)。这个时候,你再回顾一下,会发现关系型数据库的很多关键优势已经被放弃了——引用完整性(referential integrity)、事务(ACID transactions)、索引(indexes)等等。当然,这里描述的场景是在你的业务非常成功的情况下,发展很快速需要处理越来越多的数据,持续以高比例增长的数据。换句话说,你就是下一个Twitter。
你是吗?也许你正研究一个环境监测项目,需要部署一个世界范围内的传感器网络,而所有的传感器会产生海量的数据。又或者你在研究DNA序列。假如你明白或者认为你要面对海量数据存储需求,有数十亿行及数百万列的情况,你应该考虑HBase。这类新数据库设计初衷就是在商业服务器集群中能够完成从基础建设到水平扩展阶段,而不需要垂直扩展设法去买更高级的机器(而且最终还是可能没法买到更好的机器)。
初入HBase
HBase是一个能够提供实时、随机读写,能够存储数十亿行和数百万列的数据库。它设计是要运行于一个商业服务器的集群之上,当新服务器添加之后能够自动扩展,还能保证同样的性能。另外,还要有很高的容错性,因为数据是分割至服务器集群中,保存在一个冗余的文件系统比如HDFS。当某些服务器异常时,你的数据仍然是安全的。这些数据会在当前活动的服务器中自动均衡直到替换服务器上线。HBase是高一致性的数据存储。你修改的内容能够马上在其他所有的客户前展示。
HBase是在谷歌的Bigtable之后建立的,谷歌在2006年发布了一份论文“稀疏的、分布式的、持久化多维排序图”。所以如果你习惯了关系型数据库,HBase初看来很陌生。还有表的概念,却不同于关系型数据库的表。不支持典型的关系型数据库概念如:关联(joins)、索引(indexes)、事务(ACID transactions)等。但你放弃了这些特性,却得到了扩展性和容错性。HBase实质是一个带有自动数据版本控制的键值(key-value)存储。
你能够按照你希望的那样进行增删改查的操作。也能够在HBase的表中按照顺序扫描数据行。当在HBase中进行数据扫描时,数据行总是按照行主键(row key)顺序返回。每行数据都由一个唯一排序后的行主键(可以认为是关系型数据库的主键)和任意数量的列,每列都属于一个列簇(column family)并且包含一个或多个版本的值。值都是简单的二进制数组,根据应用需要可以转换成需要展示或存储的形式。HBase并未试图对开发者隐藏面向列的数据模型,而且Java 的API显然比其他你可能使用的接口更加底层。比如JPA,甚至JDBC都比HBase的API更加抽象。HBase的操作基本在原生的层面。
我们来了解如何通过命令行使用HBase。HBase自带基于JRuby开发的shell工具,能够定义和管理表、对数据执行增删改查操作、扫描表以及执行一些相关的维护。进入shell后,输入help就能获得完整的帮助信息。也可以使用 help
代码如下 | 复制代码 |
$ bin/hbase shell HBase Shell; enter 'help Type "exit 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)数据。数据行是由包含的列组成的,所以如果行中没有任何列理论上它是不存在的。接着上面的列子,下面会从一个数据行中删除一些指定的列。
代码如下 | 复制代码 |
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月。
代码如下 | 复制代码 |
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是一个分布式数据库,原本是设计运行在上千台甚至更多的服务器集群中。因此,HBase的安装自然比在单台服务器上安装一套独立的关系型数据库更加复杂。同时,所有分布式计算中都存在的典型问题HBase也不能避免,比如远程处理的合作与管理、锁、数据分布、网络延迟以及服务器间的交互。好在HBase利用了很多成熟技术,比如Apache Hadoop和Apache ZooKeeper解决了很多类似的问题。下面的图中展示了HBase的主要架构组件。
上图中可以看到,存在单个HBase主节点以及多个域服务器。(HBase可以运行在多主节点集群中,但同时只会有单个活动主节点)。HBase的表分割并存储在多个域中,每个域保存表中一定范围数据,由主节点将多个域分配至一个域服务器。
HBase是一个面向列的存储,即数据按照列存储而不是行。这样使得数据访问方式比面向行的传统数据存储系统更加高效。例如HBase中如果列簇中不包含数据,则根本不会存储任何信息。而对于一个关系型数据库来说,则会存储null值。另外,若在HBase中查询数据,只需要指定你需要的列簇,因为在一行数据中可能存在上百万行数据,你需要确定你只查询自己需要的数据。
HBase使用ZooKeeper(分布式协作服务)来管理域分配至域服务器,以及在域服务器崩溃时,能够通过将崩溃的域服务器中的域加载至其他有效的域服务器上来恢复功能。
域包括内存数据(MemStore)以及持久化数据(HFile),域服务器中的所有域共用一个先写日志(write-ahead log [WAL]),该日志用于在还未持久化存储前保存新数据,并且当域服务器崩溃后能够恢复数据。每个域都保存了一定范围内的行主键,当域包含的数据量超过定义的范围时,HBase会将域分割至两个子域,这样就扩展了HBase。
当表增大时,集群中会构建和分割出越来越多的域。当用户查询一个指定行主键或者指定范围内的主键,HBase给出这些主键所在的域,用户可以直接与这些域存在的域服务器通信。这样的设计最小化搜索指定行时可能出现的问题的数量,并且优化了数据回传时HBase的磁盘传输。关系型数据库则可能在从磁盘回传数据之前要进行大规模的磁盘扫描,甚至在有索引建立的情况下也一样。
HDFS组件使用了Hadoop分布式文件系统,分布式、高容错性、可扩展文件系统用于防止数据丢失,能够将数据切分为块且分散在整个集群中,这是HBase实际存储数据的地方。严格来说只要实现了Hadoop文件系统的API的任何形式数据都可以被纯粹,一般来说HBase会发布在运行了HDFS的Hadoop集群中。实际上,当你第一次在单机上下载和安装HBase时,如果你没用修改配置,使用的就是本地文件系统。
用户通过有效的API与HBase交互时,包括本地Java API、基于REST的接口和一些RPC接口(Apache Thrift, Apache Avro)。也可以通过Groovy, Jython, 和Scala来访问接口(Domain Specific Language [DSL])
之前学习了HBase的整体架构,现在了解一下我们的应用如何通过Java API与HBase进行交互。如同之前提到的,你也同样可以通过其他的RPC(Remote Procedure Call)技术手段与HBase交互,比如Apache Thrift通过REST网关的方式,但我们主要使用Java API的方式。API提供了DDL(数据定义语言)和DML(数据操作语言)你会发现和关系型数据库SQL的语言很相似。假设我们要存储用户信息,我们先开始建立一张新表。下列代码展示如何使用HBaseAdmin类。
代码如下 | 复制代码 |
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); |
用户表定义包含三个列簇:个人信息、联系信息以及信用卡。你需要用HTableDescriptor构建一张表,并且使用HColumnDescriptor添加一个或多个列簇。然后调用createTable方法建表。完成之后来添加一些数据。下面的代码展示了如何使用Put类插入John Doe的数据,指定名称和电子邮件指定(为了简单期间,这里忽略了通常应该有的错误处理)
代码如下 | 复制代码 |
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("[email protected]")); table.put(put); table.flushCommits(); table.close(); |
上面的代码中示例是Put类提供了唯一行主键作为构造方法参数。接下来我们会添加值,必须包括列簇、列标识符、二进制数组形式的值。或许你会注意到,HBase API的常用工具类中的Bytes类是经常用到的,它提供了一些方法能够在原始类型、字符串与二进制数组间转换。(添加一个toBytes()静态引用方法能够节省大堆代码)接下来我们将数据存入表中,刷新提交确认本地缓存的改变能够生效,最终关闭表。更新数据也和之前展示的代码方式相同。与关系型数据库不同,HBase即使只有一列改变也必须更新整行数据。假如你只需要更新一列,只需要在Put类和HBase中指定需要更新的列。也会有确认并更新的动作,本质上就是一系列并发操作,只是在用户确认待替换的值之后才进行更新动作。
如下面的代码所示,使用Get类来查询我们刚刚创建完成的数据。(从这里开始,会忽略一些代码如构建配置,实例化HTable、提交及关闭)
代码如下 | 复制代码 |
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方法。
代码如下 | 复制代码 |
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) { // ... } |
上面的代码中,我们新建了一个Scan类,查询以 smith- 开头的行主键,使用 addColumn 来限制返回的列(这样就能够减少HBase与磁盘磁盘传输)为 personal:givenName 和 contactinfo:email 两列。scan中的PageFilter用于限制扫描的行数为25.(也可以考虑使用在Scan构造函数中指定结束行主键的分页过滤器)。我们使用结果扫描类来查看刚才的结果,循环并执行业务动作。在HBase中,唯一查询多行数据的方式就是根据排序的行主键扫描,因此如何设计行主键就显得很重要。稍候会对此再进行讨论。
你也可以使用Delete类删除HBase中的数据,和Put类相似删除了一行中的所有列(意味着完全删除了行),删除了列簇、列、等类似的组合。
处理连接
上面的示例对如何处理连接以及远程调用(RPC)没有太多关注。HBase提供了HConnection类用于提供类似于连接池的功能共用连接,例如你使用 getTable() 方法来获得一个HTable实例的引用。同样,也有一个HConnectionManager类提供HConnection实例。类似Web应用中避免网络频繁交换,可以有效地管理RPC的数量。在使用HBase时,返回的海量数据这类的问题很重要的。编写HBase应用时要考虑类似的问题。
HBase不同于关系型数据库比如SQL,有丰富的查询能力。相反,它放弃了这样的能力,以及其他的比如关系,join等。将注意力集中在提供高性能的扩展性和故障恢复。在使用HBase的时候需要根据行数据和列簇设计表结构和行主键以符合应用程序的数据访问模式。这与你在关系型数据库中完全不同,在关系型数据库中,从一个普通的数据库结构开始,独立的表,使用SQL来使用joins等来组织需要的数据。在HBase中设计表式特别要考虑应用将会如何使用,预先设计数据的访问方式。使用HBase而不是关系型数据库能够让你更加接近底层的抽象实现细节和存储机制。总之,应用能够存储海量数据,高扩展性,高性能以及服务器容错性,潜在的益处大大超过了投入。
在之前关于Java API的章节中,有提到过当在HBase中扫描数据时,行主键是很关键的,因为它是限制行扫描数据的主要方式。HBase中也没有类似关系型数据库中SQL那种富查询方式。常规做法就是构建一个设置了起始和结束行主键的扫描任务,可选的加入一些过滤器来进一步限制返回的行与列数据。为了在扫描时能够有较高的灵活性,设计主键时应该包括一些需要查询的数据内容的子集。在人员和博客这个我们一直使用的例子中,行主键被设计成扫描时最常用的访问方式。例如博客,原本行主键就是发布日期。这样就能够以时间升序的方式扫描展示博客文章,但这可能不是查看博客最常见的模式。所以比较好的行主键设计使用时间戳的逆序,使用公式(Long.MAX_VALUE – timestamp , Long长整型的最大值减去时间戳即为时间戳逆序),这样扫描时就会先返回最近发布的博客文章。这样也能够比较方便的扫描指定的时间区间,例如扫描展示过去一周或者一月的所有博客文章,在web应用中是很常见的做法。
以人员表为例,我们使用复合主键,由姓、名、中名首字母以及个人身份识别码(唯一),中间以连字符连接,来区别重名的人员。例如 Brian M. Smith 身份识别码为 12345 的人员行主键为smith-brian-m-12345。扫描人员表时指定的开始与结束行主键组合,可以查询指定姓的人员,姓的开头为指定字母组合的人员,或者有相同的姓,和名首字母的人员。例如,你希望找到姓为Smith,名的首字母为B的人员,可以使用smith-b为开始行主键,smith-c为结束行主键(开始行主键是包含的,而结束行主键不包含,所以能够保证姓为Smith,名的首字母为B的人员都包含在扫描中)。能够看到HBase支持部分主键的概念,意味着你不需要指定确切的主键,能够为创建合适的扫描范围提供较高的灵活性。你能够使用部分主键扫描和过滤器的组合来只查询你需要的特定数据,能够优化数据查询,为你的应用提供合适的数据访问模式。
至此样例中式操作了包含一类信息的单表,没有其他关联。HBase没有类似关系型数据库的外键关系,但是由于它单行数据支持数百万列,所以HBase设计表时的一种方式就是将所有的关联信息都保存在同一行中–称之为宽表设计。之所以称之为宽表,因为你将所有关联的数据都保存在单行数据中,数据列可能和数据项一样多。在我们的博客例子中,你可能需要保存每篇文章的评论。根据宽表设计原则,可以设计一个列簇名为评论,使用评论时间作为列标识。这样评论的列名形式为 comments:20130704142510 和 comments:20130707163045。HBase查询数据时,返回的列能够按照一定原则排序,就像行主键一样。所以为了展示一篇博文和它所有的评论,你能够请求content, info 列以及评论(comments)列簇从一行查出所有的数据。你也能够添加一个过滤器来展示部分评论数据,分页显示。
人员表列簇也能够重新设计,用于存储联系信息比如独立地址、电话号码以及email地址。列簇能够让一个人的所有个人信息存储在一行数据中。 这样的设计在列数据不算太多时能够很好的适应,比如博客评论和个人联系信息。假如你设计的东西如电子邮件收件箱、财务事务或者海量的自动收集传感数据,这样就需要将用户的电子邮件、事务或者传感读书分散至多行数据(“高”表设计)并且为了能够有效的扫描和分页设计合理的行主键。收件箱的行主键可能形式是 <用户id>-<邮件收取时间戳逆序>这样就能够简单的对一个用户的收件箱进行扫描和分页,同样对财务事务行主键设计为<用户id>-<事务到达时间戳逆序>。这样的设计被称之为“高”表设计,就是将相同的内容(比如同一传感器的读数、同一账户的事务)分散至多行数据,可以考虑用于收集不断扩展的海量信息,比如在一个要从一个巨大的传感器网络中进行数据收集的场景。
设计HBase的行主键和表结构是使用HBase的一个很重要的步骤,也会继续作为HBase的基础架构来考虑。在HBase中还可以增加一些其他方式作为可选的数据访问通道。比如能够使用Apache Lucene实现全文检索,可以针对HBase内部数据或者外部检索(搜索下HBASE-3529)。你也能够构建(及维护)二级索引从而允许表使用代替行主键结构。例如在人员表中行主键是姓名和唯一身份id组成的符合主键。但如果我们希望能够根据人员的生日、电话区号、电子邮件地址或者其他的方式来访问时,我们就能够添加二级索引实现这样的交互。不过要注意,添加二级索引并不是轻量的操作,每次你向主表(例如人员表)写入数据时,就会更新所有的二级索引!(是的,有些事情关系型数据库做的很好,不过记住HBase是设计用于容纳比传统关系型数据库多的多的数据的)。
我们介绍了HBase中的结构设计(不包含关系及SQL)。尽管HBase丢失了传统关系型数据库的一些特性,比如外键、参照完整性、多行数据事务、多级索引等等,需要应用需要的是HBase的固有特性,比如扫描,就像很多复杂的事物一样,需要作出取舍。在HBase中,我们放弃了丰富的表结构设计和查询的灵活度,但是获得了扩大容纳海量数据的能力,只需要简单的向集群中添加服务器即可。