《HBase权威指南》一1.4 结构

  1. 云栖社区>
  2. 博客>
  3. 正文

《HBase权威指南》一1.4 结构

异步社区 2017-05-02 15:17:00 浏览1839
展开阅读全文

本节书摘来异步社区《HBase权威指南》一书中的第1章,第1.4节,作者: 【美】Lars George 译者: 代志远 , 刘佳 , 蒋杰 责编: 杨海玲,更多章节内容可以访问云栖社区“异步社区”公众号查看。

1.4 结构

本节首先介绍HBase的架构,然后介绍一些关于HBase起源的背景资料,之后将介绍其数据模型的一般概念和可用的存储API,最后在一个更高的层次上对其实现细节进行分析。

1.4.1 背景

2003年,Google发表了一篇论文,叫“The Google File System”(http://labs.google.com/papers/gfs.html)。这个分布式文件系统简称GFS,它使用商用硬件集群存储海量数据。文件系统将数据在节点之间冗余复制,这样的话,即使一台存储服务器发生故障,也不会影响数据的可用性。它对数据的流式读取也做了优化,可以边处理边读取。

不久,Google又发表了另外一篇论文,叫“MapReduce: Simplified Data Processing on Large Clusters”,参见http://labs.google.com/paper/mapreduce.html。MapReduce是GFS架构的一个补充,因为它能够充分利用GFS集群中的每个商用服务器提供的大量CPU。MapReduce加上GFS形成了处理海量数据的核心力量,包括构建Google的搜索索引。

不过以上描述的两个系统都缺乏实时随机存取数据的能力(意味着尚不足以处理Web服务)。GFS的另一个缺陷就是,它适合存储少许非常非常大的文件,而不适合存储成千上万的小文件,因为文件的元数据信息最终要存储在主节点的内存中,文件越多主节点的压力越大。

因此,Google尝试去找到一个能够驱动交互应用的解决方案,例如,Google邮件或者Google分析,能够同时利用这种基础结构、依靠GFS存储的数据冗余和数据可用性较强的特点。存储的数据应该拆分成特别小的条目,然后由系统将这些小记录聚合到非常大的存储文件中,并提供一些索引排序,让用户可以查找最少的磁盘就能够获取数据。最终,它应该能够及时存储爬虫的结果,并跟MapReduce协作构建搜索索引。

意识到RDBMS在大规模处理中的缺点(8.1节会针对这一点进行深入讨论),工程师们开始考虑问题的其他切入点:摒弃关系型的特点,采用简单的API来进行增、查、改、删(Create, Read, Update, and Delete,简称CRUD)操作,再加一个扫描函数,在较大的键范围或全表范围上迭代扫描。这些努力的成果最终在2006年的论文“BigTable: A Distributed Storage System for Structured Data”中发表了。

》“BigTable是一个管理结构化数据的分布式存储系统,它可以扩展到非常大:如在成千上万的商用服务器上存储PB级的数据。……一个稀疏的、分布式的、持久的多维排序映射。”

强烈建议对HBase感兴趣的人去阅读这篇论文,它介绍了很多BigTable的设计原理,用户最终都能在HBase中找到BigTable的影子。我们会借用这篇论文的基本概念来贯穿我们的这本书。

HBase实现了BigTable存储架构,因此我们也可以用HBase来解释每样东西。附录F介绍了两者之间的不同。

1.4.2 表、行、列和单元格

首先,做一个简要总结:最基本的单位是列(column)。一列或多列形成一行(row),并由唯一的行键(row key)来确定存储。反过来,一个表(table)中有若干行,其中每列可能有多个版本,在每一个单元格(cell)中存储了不同的值。

除了每个单元格可以保留若干个版本的数据这一点,整个结构看起来像典型的数据库的描述,但很明显有比这更重要的因素。

所有的行按照行键字典序进行排序存储。例1.1展现了如何通过不同的行键增加多行数据。

例1.1 行序是按照行键的字典序进行排序的

hbase(main):001:0> scan 'table1'
ROW                COLUMN+CELL
row-1                 column=cf1:,timestamp=1297073325971 ...
row-10               column=cf1:,timestamp=1297073337383 ...
row-11               column=cf1:,timestamp=1297073340493 ...
row-2                 column=cf1:,timestamp=1297073329851 ...
row-22               column=cf1:,timestamp=1297073344482 ...
row-3                 column=cf1:,timestamp=1297073333504 ...
row-abc               column=cf1:,timestamp=1297073349875 ...
7 row(s) in 0.1100 seconds

注意,排列的顺序可能和你预期的不一样,可能需要通过补键来获得正确排序。在字典序中,是按照二进制逐字节从左到右依次对比每一个行键,例如,row-1…小于row-2…,因此,无论后面是什么,将始终按照这个顺序排列。

按照行键排序可以获得像RDBMS的主键索引一样的特性,也就是说,行键总是唯一的,并且只出现一次,否则你就是在更新同一行。虽然BigTable的论文里只考虑了行键单一索引,但是HBase增加了对辅助索引(见9.3节)的支持。行键可以是任意的字节数组,但它并不一定是人直接可读的。

一行由若干列组成,若干列又构成一个列族(column family),这不仅有助于构建数据的语义边界或者局部边界,还有助于给它们设置某些特性(如压缩),或者指示它们存储在内存中。一个列族的所有列存储在同一个底层的存储文件里,这个存储文件叫做HFile。

列族需要在表创建时就定义好,并且不能修改得太频繁,数量也不能太多。在当前的实现中有少量已知的缺陷,这些缺陷使得列族数量只限于几十,实际情况可能还小得多(详情见第9章)。列族名必须由可打印字符组成,这与其他名字或值的命名规范有显著不同。

常见的引用列的格式为family:qualifier,qualifier是任意的字节数组。⑱与列族的数量有限制相反,列的数量没有限制:一个列族里可以有数百万个列。列值也没有类型和长度的限定。

图1-4用可视化的方式展现了普通数据库与列式HBase在行设计上的不同,行和列没有像经典的电子表格模型那样排列,而是采用了标签描述(tag metaphor),也就是说,信息保存在一个特定的标签下。


4

文字图1-4中的“NULL?”表明了固定模式的数据库在没有值的地方必须存储NULL值,但是在HBase的存储架构中,可以干脆省略整个列,换句话说,空值是没有任何消耗的:它们不占用任何存储空间。

所有列和行的信息都会通过列族在表中定义,关于这一点我们在下文进行讨论。

每一列的值或单元格的值都具有时间戳,默认由系统指定,也可以由用户显式设置。时间戳可以被使用,例如通过不同的时间戳来区分不同版本的值。一个单元格的不同版本的值按照降序排列在一起,访问的时候优先读取最新的值。这种优化的目的在于让新值比老值更容易被读取。

用户可以指定每个值所能保存的最大版本数。此外,还支持谓词删除(predicate deletion,见8.1.2节关于LSM树的内容),例如,允许用户只保存过去一周内写入的值。这些值(或单元格)也只是未解释的字节数组,客户端需要知道怎样去处理这些值。

前面提到过,HBase是按照BigTable模型实现的,是一个稀疏的、分布式的、持久化的、多维的映射,由行键、列键和时间戳索引。将以上特点联系在一起,我们就有了如下的数据存取模式:

(Table,RowKey,Family,Column,Timestamp)→ Value

可以用一种更像编程语言的风格表示如下:

SortedMap<  
   RowKey,List<  
     SortedMap<  
        Column,List<  
          Value,Timestamp  
        >  
     >  
   >
>

或者用一行来表示:

SortedMap< RowKey,List< SortedMap< Column,List< Value,Timestamp>>>>

第一个SortedMap代表那个表,包含一个列族的List。列族中包含了另一个SortedMap存储列和相应的值。这些值在最后的List中,存储了值和该值被设置的时间戳。

这个数据存取模型的一个有趣的特性是单元格可以存在多个版本,不同的列被写入的次数不同。API默认提供了一个所有列的统一视图,API会自动选择单元格的当前值。图1-5展示了示例表中的某一行。

图1-5用表示单元格被写入的时间戳tn可视化了时间组件,升序显示了这些值被插入的不同时间。图1-6是另一种查看数据的方式,在这种更类似电子表格的形式中,将时间戳添加到了它自己的那一列中。


5_6

尽管这些值插入的次数不同,并且存在多个版本,但是仍然能将行看作是所有列以及这些列的最新版本(即每一列的最大tn)的组合。这里提供了查询一个特定时间戳或者是特定时间戳之前的值的方式,也可以一次查询多个版本的值,关于这一点请参见第3章。

webtable

BigTable和HBase的典型使用场景是webtable,存储从互联网中抓取的网页。

行键是反转的网页URL,如org.hbase.www。有一个用于存储网页HTML代码的列族contents,还有其他的列族,如anchor,用于存储外向链接和入站链接,还有一个用于存储元数据的列族language。

contents列族使用多版本,允许用户存储一些旧的HTML副本,使用多版本是有益的,例如帮助分析一个页面的变化频率。使用的时间戳是抓取该页面的实际次数。

行数据的存取操作是原子的(atomic),可以读写任意数目的列。目前还不支持跨行事务和跨表事务。原子存取也是促成系统架构具有强一致性(strictly consistent)的一个因素,因为并发的读写者可以对行的状态作出安全的假设。

使用多版本和时间戳同样能够帮助应用层解决一致性问题。

1.4.3 自动分区

HBase中扩展和负载均衡的基本单元称为region,region本质上是以行键排序的连续存储的区间。如果region太大,系统就会把它们动态拆分,相反地,就把多个region合并,以减少存储文件数量。⑲

文字HBase中的region等同于数据库分区中用的范围划分(range partition)。它们可以被分配到若干台物理服务器上以均摊负载,因此提供了较强的扩展性。

一张表初始的时候只有一个region,用户开始向表中插入数据时,系统会检查这个region的大小,确保其不超过配置的最大值。如果超过了限制,系统会在中间键(middle key,region中间的那个行键)处将这个region拆分成两个大致相等的子region(详情见第8章)。

每一个region只能由一台region服务器(region server)加载,每一台region服务器可以同时加载多个region。图1-7展示了一个表,这个表实际上是一个由很多region服务器加载的region集合的逻辑视图。


7

文字BigTable的论文中指出,每台服务器中region的最佳加载数量是10~1000,每个region的最佳大小是100 MB~200 MB。这个标准是以2006年(以及更早以前)的硬件配置为基准参数建议的。按照HBase和现在的硬件能力,每台服务器的最佳加载数量差不多还是10~1000,但每个region的最佳大小是1 GB~2 GB了。

虽然数量增加了,但是基本原理还是一样的:每台服务器能加载的region数量和每个region的最佳存储大小取决于单台服务器的有效处理能力。

region拆分和服务相当于其他系统提供的自动分区(autosharding)。当一个服务器出现故障后,该服务器上的region可以快速恢复,并获得细粒度的负载均衡,因为当服务于某个region的服务器当前负载过大、发生错误或者被停止使用导致不可用时,系统会将该region移到其他服务器上。

region拆分的操作也非常快——接近瞬间,因为拆分之后的region读取的仍然是原存储文件,直到合并把存储文件异步地写成独立的文件,详情见第8章。

1.4.4 存储API

“BigTable并不支持完整的关系数据模型;相反,它提供了具有简单数据模型的客户端,这个简单的数据模型支持动态控制数据的布局格式……”
API提供了建表、删表、增加列族和删除列族操作,同时还提供了修改表和列族元数据的功能,如压缩和设置块大小。此外,它还提供了客户端对给定的行键值进行增加、删除和查找操作的功能。

scan API提供了高效遍历某个范围的行的功能,同时可以限定返回哪些列或者返回的版本数。通过设置过滤器可以匹配返回的列,通过设置起始和终止的时间范围可以选择查询的版本。

在这些基本功能的基础上,还有一些更高级的特性。系统支持单行事务,基于这个特性,系统实现了对单个行键下存储的数据的原子读-修改-写(read-modify-write)序列。虽然还不支持跨行和跨表的事务,但客户端已经能够支持批量操作以获得更好的性能。

单元格的值可以当作计数器使用,并且能够支持原子更新。这个计数器能够在一个操作中完成读和修改,因此尽管是分布式的系统架构,客户端依然可以利用此特性实现全局的、强一致的、连续的计数器。

还可以在服务器的地址空间中执行来自客户端的代码,支持这种功能的服务端框架叫做协处理器(coprocessor)。这个代码能直接访问服务器本地的数据,可以用于实现轻量级批处理作业,或者使用表达式并基于各种操作来分析或汇总数据。

文字HBase在0.91.0版本中加入了协处理器。

最后,系统通过提供包装器集成了MapReduce框架,该包装器能够将表转换成MapReduce作业的输入源和输出目标。

与RDBMS不同,HBase系统没有提供查询数据的特定域语言,例如SQL。数据存取不是以声明的方式完成的,而是通过客户端API以纯粹的命令完成的。HBase的API主要是Java代码,但是也可以用其他编程语言来存取数据。

1.4.5 实现

“BigTable……允许客户端推断在底层存储中表示的数据的位置属性。”
数据存储在存储文件(store file)中,称为HFile,HFile中存储的是经过排序的键值映射结构。文件内部由连续的块组成,块的索引信息存储在文件的尾部。当把HFile打开并加载到内存中时,索引信息会优先加载到内存中,每个块的默认大小是64KB,可以根据需要配置不同的块大小。存储文件提供了一个设定起始和终止行键范围的API用于扫描特定的值。

文字关于实现的细节会在第8章中讨论,接下来只是对系统的实现进行简单介绍。

每一个HFile都有一个块索引,通过一个磁盘查找就可以实现查询。首先,在内存的块索引中进行二分查找,确定可能包含给定键的块,然后读取磁盘块找到实际要找的键。

存储文件通常保存在Hadoop分布式文件系统(Hadoop Distributed File System,HDFS)中,HDFS提供了一个可扩展的、持久的、冗余的HBase存储层。存储文件通过将更改写入到可配置数目的物理服务器中,以保证不丢失数据。

每次更新数据时,都会先将数据记录在提交日志(commit log)中,在HBase中这叫做预写日志(write-ahead log,WAL),然后才会将这些数据写入内存中的memstore中。一旦内存保存的写入数据的累计大小超过了一个给定的最大值,系统就会将这些数据移出内存作为HFile文件刷写到磁盘中。数据移出内存之后,系统会丢弃对应的提交日志,只保留未持久化到磁盘中的提交日志。在系统将数据移出memstore写入磁盘的过程中,可以不必阻塞系统的读写,通过滚动内存中的memstore就能达到这个目的,即用空的新memstore获取更新数据,将满的旧memstore转换成一个文件。请注意,memstore中的数据已经按照行键排序,持久化到磁盘中的HFile也是按照这个顺序排列的,所以不必执行排序或其他特殊处理。

文字现在我们开始讨论本节开头提到的BigTable引文,并搞清位置属性(locality property)是什么。所有文件包含的键/值对都是按行键顺序归类的,并且对块级别的操作做了优化,比如顺序地读取这些键/值对的操作,因此,行键需要特殊指定。在前面介绍webtable的例子中,你可能已经注意到了,使用的行键是反转的FQDN(网址的域名部分),如org.hbase.www。主要原因是通过网址反转,把网址中最重要的部分——顶级域名(TLD)放在最前面,可以让来自hbase.org的网页在HBase中顺序地排列在一起。例如,blog.hbase.org下面的页面和那些来自www.hbase.org的页面会归为一类,或者说org.hbase.www会排列在org.hbase.blog的后面。

因为存储文件是不可被改变的,所以无法通过移除某个键/值对来简单地删除值。可行的解决办法是,做个删除标记(delete marker,又称墓碑标记),表明给定行已被删除的事实。在检索过程中,这些删除标记掩盖了实际值,客户端读不到实际值。

读回的数据是两部分数据合并的结果,一部分是memstore中还没有写入磁盘的数据,另一部分是磁盘上的存储文件。值得注意的是,数据检索时用不着WAL,只有服务器内存中的数据在服务器崩溃前没有写入到磁盘,而后进行恢复数据时才会用到WAL。

随着memstore中的数据不断刷写到磁盘中,会产生越来越多的HFile文件,HBase内部有一个解决这个问题的管家机制,即用合并将多个文件合并成一个较大的文件。合并有两种类型:minor合并(minor compaction)和major压缩合并(majar compaction)。minor合并将多个小文件重写为数量较少的大文件,减少存储文件的数量,这个过程实际上是个多路归并的过程。因为HFile的每个文件都是经过归类的,所以合并速度很快,只受到磁盘I/O性能的影响。

major合并将一个region中一个列族的若干个HFile重写为一个新HFile,与minor合并相比,还有更独特的功能:major合并能扫描所有的键/值对,顺序重写全部的数据,重写数据的过程中会略过做了删除标记的数据。断言删除此时生效,例如,对于那些超过版本号限制的数据以及生存时间到期的数据,在重写数据时就不再写入磁盘了。

文字这种架构来源于LSM树(见8.1.2节)。唯一的区别是,LSM树将多页块(multipage block)中的数据存储在磁盘中,其存储结构布局类似于B树。在HBase中,数据的更新与合并是轮流进行的,而在BigTable中,更新是更粗粒度的操作,整个memstore会存储为一个新的存储文件,不会马上合并。可以把HBase的这种架构称为“LSM映射”(Log-Structured Sort-and-Merge-Map)。后台合并过程与LSM树的结构合并过程相对应,只不过HBase合并重写整个文件,而不会像LSM树一样只操作树结构的部分数据,LSM树结构也正是因为这种操作而得名。

HBase中有3个主要组件:客户端库、一台主服务器、多台region服务器。可以动态地增加和移除region服务器,以适应不断变化的负载。主服务器主要负责利用Apache ZooKeeper为region服务器分配region,Apache ZooKeeper是一个可靠的、高可用的、持久化的分布式协调系统。

Apache ZooKeeper

ZooKeeper⑳是Apache软件基金会旗下的一个独立开源系统,它是Google公司为解决BigTable中问题而提出的Chubby算法的一种开源实现。它提供了类似文件系统一样的访问目录和文件(称为znode)的功能,通常分布式系统利用它协调所有权、注册服务、监听更新。

每台region服务器在ZooKeeper中注册一个自己的临时节点,主服务器会利用这些临时节点来发现可用服务器,还可以利用临时节点来跟踪机器故障和网络分区。

在ZooKeeper服务器中,每个临时节点都属于某一个会话,这个会话是客户端连接上ZooKeeper服务器之后自动生成的。每个会话在服务器中有一个唯一的id,并且客户端会以此id不断地向ZooKeeper服务器发送“心跳”,一旦发生故障ZooKeeper客户端进程死掉,ZooKeeper服务器会判定该会话超时,并自动删除属于它的临时节点。

HBase还可以利用ZooKeeper确保只有一个主服务器在运行,存储用于发现region的引导位置,作为一个region服务器的注册表,以及实现其他目的。ZooKeeper是一个关键组成部分,没有它HBase就无法运作。ZooKeeper使用分布式的一系列服务器和Zab协议(确保其状态保持一致)减轻了应用上的负担。

图1-8展示了HBase的各个组件是如何利用像HDFS和ZooKeeper这样的现有系统协调地组织起来的,而且还增加了自己的层以形成一个完整的平台。


8

master服务器负责跨region服务器的全局region的负载均衡,将繁忙的服务器中的region移到负载较轻的服务器中。主服务器不是实际数据存储或者检索路径的组成部分,它仅提供了负载均衡和集群管理,不为region服务器或者客户端提供任何的数据服务,因此是轻量级服务器。此外,主服务器还提供了元数据的管理操作,例如,建表和创建列族。

region服务器负责为它们服务的region提供读写请求,也提供了拆分超过配置大小的region的接口。客户端则直接与region服务器通信,处理所有数据相关的操作。

8.5节详细介绍了客户端如何执行region查找。

1.4.6 小结

“数十亿行×数百万列×数千个版本 = TB级或PB级的存储”

我们已经见识到,BigTable的存储架构是怎样使用多台服务器将按键归类的行拆分成多个范围来负载均衡的,还看到了它是怎样使用上千台机器存储PB级数据的。使用的存储格式对于顺序读相邻的键/值对来说是很理想的,这种格式针对块I/O操作做了优化,能最大限度地利用磁盘传输通道。

表的扫描与时间呈线性关系,行键的查找以及修改操作与时间呈对数关系——极端情况下是常数关系(使用了布隆过滤器)。HBase在设计上完全避免了显式的锁,提供了行原子性操作,这使得系统不会因为读写操作性能而影响系统扩展能力。

当前的列式存储结构允许表在实际存储时不存储NULL值,因此表可以看作是个无限的、稀疏的表。表中每行数据只由一台服务器所服务,因此HBase具有强一致性,使用多版本可以避免因并发解耦过程引起的编辑冲突,而且可以保留这一行的历史变化。

事实上,至少从2005年开始,BigTable就已经应用于Google生产,拥有非常多的应用场景,从批处理到实时数据服务都有它的身影。BigTable存储的数据既可以非常小(例如URL),也可以特别大(如网页和卫星图片),还成功地为许多知名的Google产品提供了灵活的、高性能的解决方案,这些产品包括Google Earth、Google Reader、Google Finance和Google Analytics。

网友评论

登录后评论
0/500
评论
异步社区
+ 关注