概述
简介
HBase 利用 Hadoop MapReduce 来处理 HBase 中的海量数据,实现高性能计算;利用 Zookeeper 作为协同服务,实现稳定服务和失败恢复;使用 HDFS 作为高可靠的底层存储,利用廉价集群提供海量数据存储能力。当然,HBase 也可以直接使用本地文件系统而不用 HDFS 作为底层数据存储方式,不过,为了提高数据可靠性和系统的健壮性,发挥 HBase 处理大数据量等功能,一般都使用 HDFS 作为 HBase 的底层数据存储方式。此外,为了方便在 HBase 上进行数据处理,Sqoop 为 HBase 提供了高效、便捷的 RDBMS 数据导人功能,Pig 和 Hive 为 HBase 提供了高层语言支持。HBase 是 BigTable 的开源实现。
HBase 数据模型
数据模型概述
HBase 是一个稀疏、多维度、排序的映射表,这张表的索引是行键、列族、列限定符和时间戳。每个值是一个未经解释的字符串,没有数据类型。用户在表中存储数据,每一行都有一个可排序的行键和任意多的列。表在水平方向由一个或者多个列族组成,同一个列族中可以包含任意多个列,同一个列族里面的数据存储在一起。列族支持动态扩展,可以很轻松地添加-个列族或列,无需预先定义列的数量以及类型,所有列均以字符串形式存储,用户需要自行进行数据类型转换。由于同-张表里面的每一行数据都可以有截然不同的列,因此对于整个映射表的每行数据而言,有些列的值就是空的,所以说 HBase 是稀疏的。
在 HBase 中执行更新操作时,并不会删除数据旧的版本,而是生成一个新的版本,旧有的版本仍然保留,HBase 可以对允许保留的版本的数量进行设置。客户端可以选择获取距离某个时间最近的版本,或者一次获取所有版本。如果在查询的时候不提供时间戳,那么会返回距离现在最近的那一个版本的数据,因为在存储的时候,数据会按照时间戳排序。
HBase 提供了两种数据版本回收方式:
- 一是保存数据的最后 n 个版本;
- 二是保存最近一段时间内的版本(如最近7天)。
数据模型的相关概念
表
HBase 采用表来组织数据,表由行和列组成,列划分为若千个列族。
行
每个 HBase 表都由若干行组成,每个行由行键(Row Key)来标识。访问表中的行只有 3 种方式:
- 通过单个行键访问;
- 通过一个行键的区间来访问;
- 全表扫描。
行键可以是任意字符串(最大长度是 64 KB,实际应用中长度一般为 10~100 字节),在 HBase 内部,行键保存为字节数组。存储时,数据按照行键的字典序排序存储。在设计行键时,要充分考虑这个特性,将经常一起读取的行存储在一起。
列族
一个 HBase 表被分组成许多“列族”的集合,它是基本的访问控制单元。列族需要在表创建时就定义好,数量不能太多(HBase 的一些缺陷使得列族数量只限于几十个),而且不要频繁修改。
存储在一个列族当中的所有数据,通常都属于同一种数据类型,这通常意味着具有更高的压缩率。表中的每个列都归属于某个列族,数据可以被存放到列族的某个列下面,但是在把数据存放到这个列族的某个列下面之前,必须首先创建这个列族。在创建完成一个列族以后,就可以使用同一个列族当中的列。列名都以列族作为前缀。例如,courses:history 和 courses:math 这两个列都属于 courses 这个列族。
在 HBase 中,访问控制、磁盘和内存的使用统计都是在列族层面进行的。实际应用中,我们可以借助列族上的控制权限帮助实现特定的目的。比如,我们可以允许一些应用能够向表中添加新的数据,而另一些应用则只允许浏览数据。HBase 列族还可以被配置成支持不同类型的访问模式。比如,一个列族也可以被设置成放入内存当中,以消耗内存为代价,从而换取更好的响应性能。
列限定符
列族里的数据通过列限定符(或列)来定位。列限定符不用事先定义,也不需要在不同行之间保持一致。列限定符没有数据类型,总被视为字节数组 byte[]。
单元格
在 HBase 表中,通过行、列族和列限定符确定一个“单元格”(Cell)。单元格中存储的数据没有数据类型,总被视为字节数组 bytel]。每个单元格中可以保存一个数据的多个版本,每个版本对应一个不同的时间戳。
时间戳
每个单元格都保存着同一份数据的多个版本,这些版本采用时间戳进行索引。每次对一个单元格执行操作(新建、修改、删除)时,HBase 都会隐式地自动生成并存储一个时间戳。时间戳一般是 64 位整型,可以由用户自己赋值(自己生成唯一时间戳可以避免应用程序中出现数据版本冲突),也可以由 HBase 在数据写入时自动赋值。一个单元格的不同版本是根据时间戳降序的顺序进行存储的,这样,最新的版本可以被最先读取。
数据坐标
HBase 使用坐标来定位表中的数据,也就是说,每个值都是通过坐标来访问的。对于我们熟悉的关系数据库而言,数据定位可以理解为采用“二维坐标”,即根据行和列就可以确定表中一个具体的值。但是,HBase中需要根据行键、列族、列限定符和时间戳来确定一个单元格,因此可以视为一个“四维坐标”,即 [行键,列族,列限定符,时间戳]。
概念视图
在 HBase 的概念视图中,一个表可以视为一个稀疏、多维的映射关系。
上图行键是一个反向 URL(即 com.cnn.www),之所以这样存放,是因为 HBase 是按照行键的字典序来排序存储数据的,采用反向 URL 的方式,可以让来自同一个网站的数据内容都保存在相邻的位置,在按照行键的值进行水平分区时,就可以尽量把来自同一个网站的数据划分到同一个分区(Region)中。
物理视图
从概念视图层面,HBase 中的每个表是由许多行组成的,但是在物理存储层面,它是采用了基于列的存储方式,而不是像传统关系数据库那样采用基于行的存储方式,这也是 HBase 和传统关系数据库的重要区别。
面向列的存储
行式数据库使用 NSM(N-ary Storage Model)存储模型,一个元组(或行)会被连续地存储在磁盘页中,也就是说,数据是一行一行被存储的,第一行写人磁盘页后,再继续写人第二行,依此类推。在从磁盘中读取数据时,需要从磁盘中顺序扫描每个元组的完整内容,然后从每个元组中筛选出查询所需要的属性。如果每个元组只有少量属性的值对于查询是有用的,那么 NSM 就会浪费许多磁盘空间和内存带宽。
列式数据库采用 DSM(Decomposition Storage Model)存储模型,它是在 1985 年提出来的,目的是最小化无用的 I/O。DSM 采用了不同于 NSM 的思路,对于采用 DSM 存储模型的关系数据库而言,DSM 会对关系进行垂直分解,并为每个属性分配一个子关系。因此,一个具有 n 个属性的关系会被分解成 n 个子关系,每个子关系单独存储,每个子关系只有当其相应的属性被请求时才会被访问。也就是说,DSM 是以关系数据库中的属性或列为单位进行存储,关系中多个元组的同一属性值(或同一列值)会被存储在一起,而一个元组中不同属性值则通常会被分别存放于不同的磁盘页中。
- 行式数据库主要适合于小批量的数据处理,如联机事务型数据处理,我们平时熟悉的 Oracle 和 MySQL 等关系数据库都属于行式数据库。
- 列式数据库主要适合于批量数据处理和即席查询(Ad-Hoc Query)。
列式存储的优点是:
- 可以降低 IO 开销,支持大量并发用户查询,其数据处理速度比传统方法快 100 倍,因为仅需要处理可以回答这些查询的列,而不是分类整理与特定查询无关的数据行;
- 具有较高的数据压缩比,较传统的行式数据库更加有效,甚至能达到 5 倍的效果。
列式数据库主要用于数据挖掘、决策支持和地理信息系统等查询密集型系统中,因为一次查询就可以得出结果,而不必每次都要遍历所有的数据库。所以,列式数据库大多都是应用在人口统计调查、医疗分析等行业中,这种行业需要处理大量的数据统计分析,假如采用行式数据库,势必导致消耗的时间会无限放大。
DSM 存储模型的缺陷是:
- 执行连接操作时需要昂贵的元组重构代价,因为一个元组的不同属性被分散到不同磁盘页中存储,当需要一个完整的元组时,就要从多个磁盘页中读取相应字段的值来重新组合得到原来的一个元组。
- 对于联机事务型数据处理而言,需要频繁对一些元组进行修改(如百货商场售出一件衣服后要立即修改库存数据),如果采用DSM存储模型,就会带来高昂的开销。
HBase 的实现原理
HBase 的功能组件
HBase 的实现包括3个主要的功能组件:
- 库函数,链接到每个客户端;
- 一个 Master 主服务器;
- 许多个 Region 服务器。
- Region 服务器负责存储和维护分配给自己的 Region,处理来自客户端的读写请求。
- 主服务器 Master 负责管理和维护 HBase 表的分区信息,比如,一个表被分成了哪些 Region,每个 Region 被存放在哪台 Region 服务器上,同时也负责维护 Region 服务器列表。
因此,如果 Master 主服务器死机,那么整个系统都会无效。Master 会实时监测集群中的 Region 服务器,把特定的 Region 分配到可用的 Region 服务器上,并确保整个集群内部不同 Region 服务器之间的负载均衡,当某个 Region 服务器因出现故障而失效时,Master 会把该故障服务器上存储的 Region 重新分配给其他可用的 Region 服务器。除此以外,Master 还处理模式变化,如表和列族的创建。同客户端并不是直接从 Master 主服务器上读取数据,而是在获得 Region 的存储位置信息后,直接从 Region 服务器上读取数据。尤其需要指出的是,HBase 客户端并不依赖于 Master 而是借助于 Zookeeper 来获得 Region 的位置信息的,所以大多数客户端从来不和主服务器 Master 通信,这种设计方式使 Master 的负载很小。
表和 Region
在一个 HBase 中,存储了许多表。对于每个 HBase 表而言,表中的行是根据行键的值的字典序进行维护的,表中包含的行的数量可能非常庞大,无法存储在一台机器上,需要分布存储到多台机器上。因此,需要根据行键的值对表中的行进行分区,每个行区间构成一个分区,被称为”Region”,包含了位于某个值域区间内的所有数据,它是负载均衡和数据分发的基本单位,这些 Region 会被分发到不同的 Region 服务器上。
初始时,每个表只包含一个Region,随着数据的不断插入,Region 会持续增大,当一个 Region 中包含的行数量达到一个阈值时,就会被自动等分成两个新的 Region。随着表中行的数量继续增加,就会分裂出越来越多的 Region。
每个 Region 的默认大小是 100MB 到 200MB,是 HBase 中负载均衡和数据分发的基本单位。Master 主服务器会把不同的 Region 分配到不同的 Region 服务器上,但是同一个 Region 是不会被拆分到多个 Region 服务器上的。每个 Region 服务器负责管理-个 Region 集合,通常在每个 Region 服务器上会放置 10~1000 个 Region。
Region 的定位
每个 Region 都有一个 RegionID 来标识它的唯一性,这样,一个 Region 标识符就可以表示成“表名 + 开始主键 + RegionID”。
有了 Region 标识符,就可以唯一标识每个 Region。为了定位每个 Region 所在的位置,就可以构建一张映射表,映射表的每个条目(或每行)包含两项内容,一个是 Region 标识符,另一个是 Region 服务器标识,这个条目就表示 Region 和 Region 服务器之间的对应关系,从而就可以知道某个 Region 被保存在哪个 Region 服务器中。这个映射表包含了关于 Region 的元数据(即 Region 和 Region 服务器之间的对应关系),因此也被称为“元数据表”,又名“.META.表”。
当一个 HBase 表中的 Region 数量非常庞大的时候,.META.表的条目就会非常多,一个服务器保存不下,也需要分区存储到不同的服务器上,因此.META.表也会被分裂成多个 Region,这时,为了定位这些 Region,就需要再构建一个新的映射表,记录所有元数据的具体位置,这个新的映射表就是“根数据表”,又名“-ROOT-表”。-ROOT-表是不能被分割的,永远只存在一个 Region 用于存放-ROOT-表,因此这个用来存放-ROOT-表的唯一一个 Region, 它的名字是在程序中被写死的,Master 主服务器永远知道它的位置。
综上所述,HBase 使用类似 B+树的三层结构来保存 Region 位置信息。
为了加快访问速度,.META.表的全部 Region 都会被保存在内存中。假设.META.表的每行(一个映射条目)在内存中大约占用 1KB,并且每个 Region 限制为 128MB,那么,上面的三层结构可以保存的用户数据表的 Region 数目的计算方法是:(-ROOT-表能够寻址的.META.表的 Region 个数) x (每个.META.表的 Region 可以寻址的用户数据表的 Region 个数)。一个-ROOT-表最多只能有一个 Region,也就是最多只能有 128MB,按照每行(一个映射条目)占用 1KB 内存计算,128MB 空间可以容纳 128MB/1KB=2^17 行,也就是说,一个-ROOT表可以寻址 2^17 个.META.表的 Region。同理,每个.META.表的 Region 可以寻址的用户数据表的 Region 个数是 128MB/1KB=2^17。最终,三层结构可以保存的 Region 数目是 (128MB/1KB) * (128MB/1KB) = 234 个 Region。可以看出,这种数量已经足够可以满足实际应用中的用户数据存储需求。
客户端访问用户数据之前,需要首先访问 Zookeeper,获取-ROOT-表的位置信息,然后访问-ROOT-表,获得.META.表的信息,接着访问.META.表,找到所需的 Region 具体位于哪个 Region 服务器,最后才会到该 Region 服务器读取数据。该过程需要多次网络操作,为了加速寻址过程,一般会在客户端做缓存,把查询过的位置信息缓存起来,这样以后访问相同的数据时,就可以直接从客户端缓存中获取 Region 的位置信息,而不需要每次都经历一个“三级寻址”过程。需要注意的是,随着 HBase 中表的不断更新,Region 的位置信息可能会发生变化,但是客户端缓存并不会自己检测 Region 位置信息是否失效,而是在需要访问数据时,从缓存中获取 Region 位置信息却发现不存在的时候,才会判断出缓存失效,这时,就需要再次经历上述的“三级寻址”过程,重新获取最新的 Region 位置信息去访问数据,并用最新的 Region 位置信息替换缓存中失效的信息。
当一个客户端从 Zookeeper 服务器上拿到-ROOT-表的地址以后,就可以通过“三级寻址”找到用户数据表所在的 Region 服务器,并直接访问该 Region 服务器获得数据,没有必要再连接主服务器 Master。因此,主服务器的负载相对就小了很多。
HBase 运行机制
HBase 系统架构
HBase 的系统架构包括客户端、Zookeeper 服务器、Master 主服务器、Region 服务器。需要说明的是,HBase 一般采用 HDFS 作为底层数据存储。
客户端
客户端包含访问 HBase 的接口,同时在缓存中维护着已经访问过的 Region 位置信息,用来加快后续数据访问过程。HBase 客户端使用 HBase 的 RPC 机制与 Master 和 Region 服务器进行通信。其中,对于管理类操作,客户端与 Master 进行 RPC;而对于数据读写类操作,客户端则会与 Region 服务器进行 RPC。
Zookeeper 服务器
Zookeeper 服务器并非一台单一的机器,可能是由多台机器构成的集群来提供稳定可靠的协同服务。Zookeeper 能够很容易地实现集群管理的功能,如果有多台服务器组成一个服务器集群,那么必须有一个“总管”知道当前集群中每台机器的服务状态,一旦某台机器不能提供服务,集群中其他机器必须知道,从而做出调整重新分配服务策略。同样,当增加集群的服务能力时,就会增加一台或多台服务器,同样也必须让“总管”知道。
在 HBase 服务器集群中,包含了一个 Master 和多个 Region 服务器,Master 就是这个 HBase 集群的“总管”,它必须知道 Region 服务器的状态。Zookeeper 就可以轻松做到这一点,每个 Region 服务器都需要到 Zookeeper 中进行注册,Zookeeper 会实时监控每个 Region 服务器的状态并通知给 Master,这样,Master 就可以通过 Zookeeper 随时感知到各个 Region 服务器的工作状态。
Zookeeper 不仅能够帮助维护当前的集群中机器的服务状态,而且能够帮助选出一个“总管”,让这个总管来管理集群。HBase 中可以启动多个 Master,但是 Zookeeper 可以帮助选举出一个 Master 作为集群的总管,并保证在任何时刻总有唯一一个 Master 在运行,这就避免了 Master 的“单点失效”问题。
Zookeeper 中保存了-ROOT-表的地址和 Master 的地址,客户端可以通过访问 Zookeeper 获得-ROOT-表的地址,并最终通过“三级寻址”找到所需的数据。Zookeeper 中还存储了 HBase 的模式,包括有哪些表,每个表有哪些列族。
Master
主服务器 Master 主要负责表和 Region 的管理工作。
- 管理用户对表的增加、删除、修改、查询等操作。
- 实现不同 Region 服务器之间的负载均衡。
- 在 Region 分裂或合并后,负责重新调整 Region 的分布。
- 对发生故障失效的 Region 服务器上的 Region 进行迁移。
客户端访问 HBase 上数据的过程并不需要 Master 的参与,客户端可以访问 Zookeeper 获取-ROOT-表的地址,并最终到达相应的 Region 服务器进行数据读写,Master 仅仅维护着表和 Region 的元数据信息,因此负载很低。
任何时刻,一个 Region 只能分配给一个 Region 服务器。Master 维护了当前可用的 Region 服务器列表,以及当前哪些 Region 分配给了哪些 Region 服务器,哪些 Region 还未被分配。当存在未被分配的 Region,并且有一个 Region 服务器上有可用空间时,Master 就给这个 Region 服务器发送一个请求,把该 Region 分配给它。Region 服务器接受请求并完成数据加载后,就开始负责管理该 Region 对象,并对外提供服务。
Region 服务器
Region 服务器是 HBase 中最核心的模块,负责维护分配给自己的 Region,并响应用户的读写请求。HBase 一般采用 HDFS 作为底层存储文件系统,因此 Region 服务器需要向 HDFS 文件系统中读写数据。采用 HDFS 作为底层存储,可以为 HBase 提供可靠稳定的数据存储,HBase 自身并不具备数据复制和维护数据副本的功能,而HDFS可以为 HBase 提供这些支持。当然,HBase 也可以不采用 HDFS,而是使用其他任何支持 Hadoop 接口的文件系统作为底层存储,比如本地文件系统或云计算环境中的 AmazonS3(Simple Storage Service)。
Region 服务器的工作原理
Region 服务器是 HBase 中最核心的模块,Region 服务器内部管理了一系列 Region 对象和一个 HLog 文件,其中 HLog 是磁盘上面的记录文件,它记录着所有的更新操作。每个 Region 对象又是由多个 Store 组成的,每个 Store 对应了表中的一个列族的存储。每个 Store 又包含了一个 MemStore 和若千个 StoreFile。
- MemStore 是在内存中的缓存,保存最近更新的数据;
- StoreFile 是磁盘中的文件,这些文件都是 B 树结构的,方便快速读取。StoreFile 在底层的实现方式是 HDFS 文件系统的 HFile, HFile 的数据块通常采用压缩方式存储,压缩之后可以大大减少网络 I/0 和磁盘 I/O。
用户读写数据的过程
当用户写入数据时,会被分配到相应的 Region 服务器去执行操作。用户数据首先被写人到 MemStore 和 HLog 中,当操作写入 HLog 之后,commit() 调用才会将其返回给客户端。
当用户读取数据时,Region 服务器会首先访问 MemStore 缓存,如果数据不在缓存中,才会到磁盘上面的 StoreFile 中去寻找。
缓存的刷新
MemStore 缓存的容量有限,系统会周期性地调用 Region.flushcache() 把 MemStore 缓存里面的内容写到磁盘的 StoreFile 文件中,清空缓存,并在 HLog 文件中写入一个标记,用来表示缓存中
的内容已经被写人 StoreFile 文件中。每次缓存刷新操作都会在磁盘上生成一个新的 StoreFile 文件,因此每个 Store 会包含多个 StoreFile 文件。
每个 Region 服务器都有一个自己的 HLog 文件,在启动的时候,每个 Region 服务器都会检查自己的 HLog 文件,确认最近一次执行缓存刷新操作之后是否发生新的写人操作。如果没有更新,说明所有数据已经被永久保存到磁盘的 StoreFile 文件中;如果发现更新,就先把这些更新写人 MemStore,然后再刷新缓存,写人到磁盘的 StoreFile 文件中。最后,删除旧的 HLog 文件,并开始为用户提供数据访问服务。
StoreFile 的合并
每次 MemStore 缓存的刷新操作都会在磁盘上生成一个新的 StoreFile 文件,这样,系统中的每个 Store 就会存在多个 StoreFile 文件。当需要访问某个 Store 中的某个值时,就必须查找所有这些 StoreFile 文件,非常耗费时间。因此,为了减少查找时间,系统一般会调用 Store.compact() 把多个 StoreFile 文件合并成一个大文件。由于合并操作比较耗费资源,因此只会在 StoreFile 文件的数量达到一个阈值的时候才会触发合并操作。
Store 的工作原理
Region 服务器是 HBase 的核心模块,而 Store 则是 Region 服务器的核心。每个 Store 对应了表中的一个列族的存储。每个 Store 包含一个 MemStore 缓存和若于个 StoreFile 文件。MemStore 是排序的内存缓冲区,当用户写入数据时,系统首先把数据放人 MemStore 缓存,当 MemStore 缓存满时,就会刷新到磁盘中的一个 StoreFile 文件中。随着 StoreFile 文件数量的不断增加,当达到事先设定的数量时,就会触发文件合并操作,多个 StoreFile 文件会被合并成一个大的 StoreFile 文件。当多个 StoreFile 文件合并后,会逐步形成越来越大的 StoreFile 文件,当单个 StoreFile 文件大小超过一定阈值时,就会触发文件分裂操作。同时,当前的 1 个父 Region 会被分裂成 2 个子 Region, 父 Region 会下线,新分裂出的 2 个子 Region 会被 Master 分配到相应的 Region 服务器上。
HLog 的工作原理
在分布式环境下,必须要考虑到系统出错的情形,比如当 Region 服务器发生故障时,MemStore 缓存中的数据(还没有被写人文件)会全部丢失。因此,HBase 采用 HLog 来保证系统发生故障时能够恢复到正确的状态。
HBase 系统为每个 Region 服务器配置了一个 HLog 文件,它是一种预写式日志(Write Ahead Log),也就是说,用户更新数据必须首先被记人日志后才能写人 MemStore 缓存,并且直到 MemStore 缓存内容对应的日志已经被写入磁盘之后,该缓存内容才会被刷新写入磁盘。
Zookeeper 会实时监测每个 Region 服务器的状态,当某个 Region 服务器发生故障时,Zookeeper 会通知 Master。Master 首先会处理该故障 Region 服务器上面遗留的 HLog 文件,由于一个 Region 服务器上面可能会维护着多个 Region 对象,这些 Region 对象共用一个HLog文件,因此这个遗留的 HLog 文件中包含了来自多个 Region 对象的日志记录。系统会根据每条日志记录所属的 Region 对象对 HLog 数据进行拆分,分别放到相应 Region 对象的目录下,然后再将失效的 Region 重新分配到可用的 Region 服务器中,并把与该 Region 对象相关的 HLog 日志记录也发送给相应的 Region 服务器。Region 服务器领取到分配给自己的 Region 对象以及与之相关的 HLog 日志记录以后,会重新做一遍日志记录中的各种操作,把日志记录中的数据写人 MemStore 缓存,然后刷新到磁盘的 StoreFile 文件中,完成数据恢复。
需要特别指出的是,HBase 系统中,每个 Region 服务器只需要维护一个 HLog 文件,所有 Region 对象共用一个 HLog,而不是每个 Region 使用一个 HLog。在这种 Region 对象共用一个 HLog 的方式中,多个 Region 对象的更新操作所发生的日志修改,只需要不断把日志记录追加到单个日志文件中,而不需要同时打开、写入到多个日志文件中,因此可以减少磁盘寻址次数,提高对表的写操作性能。这种方式的缺点是,如果个 Region 服务器发生故障,为了恢复其上的 Region 对象,需要将 Region 服务器上的 HLog 按照其所属的 Region 对象进行拆分,然后分发到其他 Region 服务器上执行恢复操作。