Hbase

Published: by Creative Commons Licence

  • Tags:

前言

我们生活在互联网时代,大量的公司开始致力于提供针对性的信息,例如推荐和广告。以及随着机器学习的不断发展,收集更多数据的需求也在与日俱增。

过去,由于缺乏划算的方式来存储所有信息,唯一的选择是将收集到的数据删减后保存起来,例如仅保留最近N天的数据。然而,这种方法会导致数据丢失。Google和Amazon是认识到数据价值的典范,他们已经开始开发满足自己业务需求的解决方案。

目前Hadoop几乎是现在所有数据库系统的一种补充,它给用户提供了数据存储的无限空间,支持用户在恰当的时候存储和获取数据,并且针对大文件的存储,批量访问和流式访问做了优化。

关系数据库(RDBMS)

通常用户在随机访问结构化数据的时候都会查询数据库。RDBMS在这方面最为突出,但是也有一些少量的有差异的实现方式,比如面向对象的数据库。大多数RDBMS一直遵守科德十二定律(Codd's 12 rules),这个定律对于RDBMS来说是刚性标准,并且由于RDBMS的底层架构是经过仔细研究的,所以在相当长额一段时间里这种架构都不会有明显的改变。

RDBMS在设计和实现商业应用方面扮演了一个不可或缺的角色。只要用户需要保留用户、产品、会话、订单等信息,就会采用一些存储后端为前端应用服务器提供持久化数据的服务,并且要节省成本,换句话说就是使用免费软件。这种经典案例就可以使用开源的LAMP快速搭建原型。

关系数据库模型通过外键关联多个表,并且为表中的列建立索引,允许你使用该列的值查找数据。

事务提供了原子性跨表更新数据的特性,可以让修改同时生效。RDBMS提供了所谓的ACID。

通常这种模式的设计能在较长的一段时间内满足需求。如果足够幸运,你可能会成为下一个互联网的热点,每天有越来越多的用户加入你的网站。但随着你网站用户量的增加,共享数据库服务器的压力也会越来越大,增加应用服务器的数量相对来说是比较容易的,因为应用服务器之间是共享中央数据库的,但随着共享中央数据库的CPU和IO负载的上升,你将很难预测你能承受这种增速度多久。

减少压力的第一步是增加用于并行读取的从服务器,将读写分离。这种方案保留了一个主数据库服务器,但是这个主数据库服务器仅服务于写请求,这样做主要是考虑到网站的请求主要由用户浏览产生,因此写请求远少于读请求。如果这个方案也因为用户数的持续增加而失败了,或者降低了系统的性能,又该怎么办?

下一步常见的做法是增加缓存,如Memcached。现在可以将读操作连接到告诉的在内存中缓存数据的系统中,但是这种方案无法保证数据一致性,因为用户更新数据到数据库,而数据库不会主动更新缓存系统中的数据,所以需要尽可能快地同步缓存和数据库视图,把更新缓存数据与更新数据库数据的时间差最小化。

虽然这种方案能够缓解读请求的压力,但是写请求压力的增加问题还是没有得到解决。一旦主数据库服务器写性能下降,可以把主服务器换成加强服务器,即垂直扩容,让加强服务器使用更多的内核,更多的内存,更快的硬盘,总之与初始的主服务器相比要花费更多的金钱。同时值得注意的是,用户如果已经选择了主从的配置方案,就必须要让从服务器的性能与主服务器同样强,否则从服务器的更新速度就会跟不上主服务器的更新速度,这样增加一倍到两倍的成本,甚至更多。

伴随网站受欢迎程度增加,网站会需要更多新功能,而这样新功能无疑都会转化为后台数据库的查询语句。以前顺利执行额SQL的JOIN语句执行突然变慢了,或者是干脆无法执行,这时候就不得不采用逆范式化存储结构。如果情况越来越糟,就不得不停止使用存储过程,因为存储过程最后会慢的无法执行。本质上讲,减少数据库中的存储数据才能优化访问。

所以随着用户的增加,负载会不断提高,合乎逻辑的方式就是不时地预先实现最昂贵的查询方案,从而给用户提供更快的数据服务。最终,不得不放弃辅助索引的使用,原因是数据量增大的同时,索引量也达到了足以让数据库的性能直线下降,最后所能提供的查询模式只剩下按照主键查询。

这时该怎么办?如果负载在未来的几个月内预期会增加一个数量级或更多又该怎么办?此时用户可以考虑将数据分区(sharding)到多个数据库中,但是采用此方案会使得运维操作成为噩梦,而且代价非常昂贵,因此也不是最合理的解决方案啊。但从本质来讲,采用RDBMS也是因为没有其他可以选择的方案。

非关系型数据库系统(Not-Only-SQL)

NoSQL是Eric Evans针对Johan Oskarsson提出的“为新兴的新数据存储空间命名“问题而创造的一个名词。

标识符号化(tagword)实际上是一个不错的选择:最新的存储系统不提供通过SQL查询数据的手段,只提供一些比较简单的、类似于API接口的方式来存取数据。

但是,也有一些工具为NoSQL数据存储提供了SQL语言的入口,用于执行一些关系数据库中常用的复杂条件查询。因此,从查询方式上的限制来说,关系型数据库和非关系型数据库并没有严格的区分。

实际上两者在底层上是由区分的,尤其涉及到模式或者ACID事务特性时,因为这与实际的存储架构时相关的。很多这一类的新系统首先做的事情时:抛弃一些限制因素以提升扩展性。例如,它们通常不支持事务或辅助索引。

维度

  • 数据模型:数据由多种存储方式,包括键值对、半结构化的列式存储和文档结构存储。用户的应用如何存取数据?同时数据模式随着时间而变化?

  • 存储模型:内存还是持久化?

  • 一致性模型:严格一致性还是最终一致性?

  • 物理模型:分布式模式还是单机模式

可扩展性

RDBMS非常适合事务性操作,但不见长于超大规模的数据分析处理,因为超大规模的查询要进行大范围的数据查询或全表扫描。分析型数据库可以存储数百或数千TB的数据,在一台服务器上做查询工作的响应时间会远远超过用户可接受的合理响应时间。垂直扩展服务器性能,即增加CPU核数和磁盘数目,也并不能很好地解决该问题。

更糟糕的是,RDBMS的等该和死锁出现频率与事务和并发的增加并不是线性关系,准确地说,与并发数目的平方以及事务规模的3次方甚至5次方相关。分区通常是一个不切合实际的解决方案,因为它需要客户端采用非常复杂的方式和较高的代价来维护分区信息。

一些商业RDBMS也解决过类似的问题,但它们往往只是解决了问题的某几个方面,更重要的是,它们非常的昂贵。而一些开源的RDBMS解决方案中,往往放弃了其中的一些甚至全部的关系型特性,如辅助索引,来换取更高的性能拓展能力。

问题是,为了性能而一直放弃以上关系型特性是否值得?用户可以反范式化数据模型来避免等待,并且可以通过降低锁粒度的方式来尽量避免死锁。数据增长时,无需重新分区迁移数据并内嵌水平扩展性的方式。最后,用户还是面对容错和数据可用性问题,采用提高扩展性的机制,用户最终会得到一个NoSQL的解决方案,更确切地说,HBase可以满足以上多种需求。

数据库的范式化和反范式化

不同的规模,经常需要设计不同的系统结构,对这种原则的最佳描述时:反范式化、复制和智能的主键。这就需要重新思考在类似BigTable的存储系统中如何才能高效合理地存储数据。

部分原则时采用反范式化模式,例如将数据复制到多张表中,这样在读取的时候就不需要从多张表中聚合数据了。或者预先物化所需的视图,一次优化从而避免进一步的处理来提高读取性能。

结构

表、行、列和单元格

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

行键可以时任意的字节数组,并且总是唯一的。

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

列族需要在表创建时就定义好,并且不能修改得太频繁,数量也不能太多。列族名必须由可打印字符组成,这与其他名字或值的命名规范有显著不同。

常见的引用列的格式为family:qualifier,qualifier是任意的字节数组。与列族的数量限制不同,列的数量没有限制,一个列族允许有数百万个列,列值没有类型和长度的限制。

每一列的值或单元格的值都具有时间戳,默认由系统指定,也可以由用户显示设置。一个单元格的不同版本的值按照降序排列在一起,访问的时候优先读取最新的值,这种优化的目的在于让新值比老值更容易读取。用户可以指定每个值所能保存的最大版本数。此外,还支持谓词删除(predicate deletion),例如,允许用户只保存过去一周内写入的值。这些值也只是未解释的字节数组,客户端需要自己去解释处理这些值。

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

(Table,RowKey,Family,Column,Timestamp) => Value

用编程风格来表示:

SortedMap<RowKey,List<SortedMap<Column,List<Value,TimeStamp>>>> table;

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

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

自动分区

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

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

一张表初始的时候只有一个region,用户开始向表中插入数据时,系统会检查这个region的大小,确保其不超过配置的最大值。如果超过限制,系统会在中间值处将这个region拆分成两个大致相等的子region。每一个region只能由一台region服务器加载,每一台region服务器可以同时加载多个region。

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

系统支持单行事务,基于这个特性,系统实现了对单个行键下存储的数据的原子读-修改-写序列。虽然还不支持跨行和跨表的事务,但客户端已经能够支持批量操作以获得更好的性能。单元格的值可以当作计数器使用,并且能够支持原子更新。

实现

数据存储在存储文件中,称为HFile,HFile中存储的是经过排序的键值映射结构。文件内部由连续的块组成,块的索引信息存储在文件的尾部。当把HFile打开并加载到内存中时,索引信息会优先加载到内存中, 每个块的默认大是64KB。

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

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

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

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

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

HBase中由三个主要组件:客户端库、一台主服务器、多台region服务器。可以动态地增加和移除region服务器,以适应不断变化的负载。主服务器主要负责利用ZooKeeper为region服务器分配region,ZooKeeper时一个可靠的、高可用的、持久化的分布式协调系统。master服务器负责跨region的服务器的全局region的负载均衡,将繁忙的服务器中的region移到负载较轻的服务器中。主服务器不是实际数据存储或者检索路径的组成部分,它仅提供了负载均衡和集群管理。

概念

列式存储数据库

列式存储数据库以列为单位聚合数据,然后将列值顺序地存入磁盘,这种存储方式不同于行式存储的传统数据库,行式存储数据库连续地存储整行。

列式存储的出现基于这样一种假设:对于特定的查询,不是所有的值都是必须的。尤其是在分析型数据库中,这种情形常见,因此需要选择一种更加合适的存储模式。

在这种新型的设计中,减少IO只是因素之一,还有其它的优点:列的数据类型天生是相似的,即便逻辑上每一行之间有轻微的不同,但仍旧比按行存储的结构聚集在一起的数据更加利于压缩,因为大多数的压缩算法只关注有限的压缩窗口。

分区

分区(sharding)主要描述了逻辑上水平划分数据的方案。这个方案的特点是将数据分文件或分服务器存储,而不是连续存储。

数据的分区是在固定范围内实施的:在传入数据之前,必须提前划分好数据的存储范围,如果一个水平划分的压力超过其所能提供的容量,就需要将数据重分区(reshard)并迁移数据。

重分区并迁移数据是非常消耗资源的操作,等同于数据重做。需要重新划分边界然后横向拆分(split)。大规模的复制操作会消耗大量的IO资源,同时还会临时性地增加存储需求。在对数据重分区的过程中,客户端应用仍然会有更新操作要执行,不过此时的更新操作受重分区的影响会执行得非常慢。

可以采用虚拟分区(virtual shard)的方式来减少这种资源消耗,虚拟分区按照关键词定义范围较大的数据范围,每个服务器加载同等数量的数据分区。但是在新增服务器的时候,需要重新加载数据分区到新服务器,并且这个过程仍旧需要将数据迁移到新服务器。

分区是简单的完全脱离用户操作的事后操作,如果没有数据库的支持,可能会对生产系统造成严重破坏。

一致性模型

在这本书中,我们会经常提到一致性问题,所以有必要在这里对它稍加介绍,一开始的一致性时保证数据库客户端操作的正确性,数据库必须保证每一步操作都是从一个一致的状态到下一个一致的状态。

一致性可以按照严格程度由强到弱分类,或者是按照对客户端的保证程序分类,下面是一个非正式的分类列表:

  • 严格一致性:数据的变化是原子的,一经改变即时生效
  • 顺序一致性:每个客户端看到的数据依照它们操作执行的顺序而变化
  • 因果一致性:客户端以因果关系顺序观察到数据的改变
  • 最终一致性:在没有更新数据的一段时间里,系统将通过广播保证副本之间数据一致性
  • 弱一致性:没有做出保证的情况下,所有的更新会通过广播的形式传递,展现给不同客户端的数据顺序可能不一样。