Region
当HBase的表中数据量非常庞大时, 数据是无法存储在一台机器上的, 需要根据行键的值对表中的行进行分区, 每个行区间构成一个分区, 称之为: Region
, 包含了某个值域区间内的所有数据.
它是负载均衡和数据分发的基本单位, 会被分发到不同的Region服务器上, 但是同一个Region不会出现在两个服务器上.
在接触过大量的传统关系型数据库后你可能会有一些新的问题: 无法整理成表格的海量数据该如何储存? 在数据非常稀疏的情况下也必须将数据存储成关系型数据库吗? 除了关系型数据库我们是否还有别的选择以应对Web2.0时代的海量数据?
如果你也曾经想到过这些问题, 那么HBase将是其中的一个答案, 它是非常经典的列式存储数据库. 本文首先介绍HBase的由来以及其与关系数据库的区别, 其次介绍其访问接口、数据模型、实现原理和运行机制. 即便之前没有接触过HBase的相关知识也不影响阅读该文章.
如果想了解其他的非关系型数据库也可以查看文章:[NoSQL数据库]/(/article/43adsyfm/)
HBase是谷歌公司BigTable的开源实现. 而BigTable是一个分布式存储系统, 使用谷歌分布式文件系统GFS作为底层存储, 主要用来存储非结构化和半结构化的松散数据. HBase的目标是处理非常庞大的表, 可以通过水平扩展的方式利用廉价计算机集群处理超过10亿行数据和百万列元素组成的数据表.
GFS、HDFS、BigTable、HBase的关系
HDFS是GFS的开源实现. HBase是BigTable的开源实现.
GFS是BigTable的底层文件系统, BigTable的数据存储在GFS上.
HDFS是HBase的底层存储方式. 虽然HBase可以使用本地文件系统, 但是为了提高数据可靠性一般还是会选择HDFS作为底层存储.
项目 | BigTable | HBase |
---|---|---|
文件存储系统 | GFS | HDFS |
海量数据处理系统 | MapReduce | Hadoop MapReduce |
协同服务系统 | Chubby | Zookeeper |
HBase和BigTable底层技术对应关系
与传统的数据库相比主要区别在于:
HBase提供了多种访问方式, 不同的方式适用于不同的场景.
类型 | 特点 | 场合 |
---|---|---|
Native Java API | 最常规高效的访问方式 | 适合Hadoop MapReduce作业并行批处理HBase表数据 |
HBase Shell | HBase的命令行工具, 最简单的接口 | 适合HBase管理 |
Thrift Gateway | 利用Thrift序列化技术, 支持C++, PHP, Python等多种语言 | 适合其他异构系统访问HBase |
REST Gateway | 解除语言限制 | 支持REST风格的HTTP API访问HBase |
Pig | 使用Pig Latin流式编程语言来处理HBase的数据 | 适合做数据统计 |
Hive | 简单 | 可以用类似SQL语言的方式来访问 |
数据模型是一个数据库产品的核心, 接下来将介绍HBase列族数据模型并阐述HBase数据库的概念视图和物理视图的差异.
HBase实际上是一个稀疏、多维、持久化存储的映射表, 采用行键、列族、列限定符和时间戳进行索引, 每个值都是未经解释的字节数组byte[].
表由行和列组成, 列被分为若干个列族
每个HBase表都由若干行组成, 每个行由行键(Row Key)进行标识.
访问表中的行有3种方式:
行键可以是任意字符串(最大长度64KB, 实际应用中一般为10-100字节). 在HBase内部将行键保存为 字节数组, 按照行键的 字典序 排序. 所以在设计行键时可以充分考虑该特性, 将需要一起读的行存储在一起.
HBase中一个表被分为多个列族, 列族是最基本的访问控制单元. 表中的每个列都必须属于一个列族, 我们可以将其理解为 把列按照需求分到不同的组中, 就如同整理文件到不同的文件夹中去.
为什么要这么做?
缺点
列族中的数据是通过列限定符来定位的. 列限定符无需事先定义, 也没有数据类型, 总被视为字节数组byte[].
在HBase的表中, 通过行、列和列限定符可以确定一个"单元格(Cell)". 单元格中存储的数据没有数据类型, 总被视为字节数组byte[].
每个单元格中可以保留一个数据的多个版本, 每个版本对应一个不同的时间戳.
每个单元格都保留了同一个数据的多个版本, 这些版本采用时间戳进行索引. 事实上每一次对于一个单元格执行的操作(增删改)时, HBase都会自动生成并存储一个时间戳, 通常这个时间戳是64位整型. 当然, 这个时间戳也可以由用户自己赋值, 用以避免应用程序中出现数据版本冲突.
一个单元格中的不同版本的数据是以时间戳降序排序的, 以便于读到最新的数据版本.
我认为下面的一张图可以很好地表述上面的5个概念. 类比于关系数据库, 行键就是主键行号, 列限定符就是列名, 列族就是列名组成小组的组名, 单元格就是具体存储数据的格子, 时间戳则标识了一个单元格中不同时间的数据版本.
相较于我们所熟悉的关系数据库, HBase无法仅使用行号和列号确定一个数据. 在HBase中, 我们需要: 行键、列族、列限定符和时间戳 这4个东西来确定一个数据.
[行键, 列族, 列限定符, 时间戳]被称为是HBase的坐标, 可以通过这个坐标来直接访问数据. 在这种层面上讲, HBase也可以被视为一个键值数据库.
在HBase的概念视图中, 一个表是一个稀疏、多维的映射关系.
时间戳 | 列族 contents | 列族 anchor | |
---|---|---|---|
com.cnn.www | t5 | anchor:cnnsi.com="CNN" | |
t4 | anchor:my.look.ca="CNN.com" | ||
com.cnn.www | t3 | contents:html="xxxx" | |
t2 | contents:html="xxxx" | ||
t1 | contents:html="xxxx" |
上表存储了一个网页的页面内容(html代码)和一些反向连接. contents中存储的是网页内容, anchor中存储的是反向连接. 不过有几个地方需要额外注意:
列族 contents
时间戳 | 列族 contents | |
---|---|---|
com.cnn.www | t3 | contents:html="xxxx" |
t2 | contents:html="xxxx" | |
t1 | contents:html="xxxx" |
列族 anchor
时间戳 | 列族 anchor | |
---|---|---|
com.cnn.www | t5 | anchor:cnnsi.com="CNN" |
t4 | anchor:my.look.ca="CNN.com" |
我们可以轻易发现, 在物理的存储层面上来看HBase采用了基于列的存储方式, 而不是传统关系数据库那样基于行来存储. 这也是HBase与传统关系数据库间的重要区别.
与概念视图的不同
行式数据库使用 NSM(N-ary Storage Model) 存储模型, 将一个元组(或行)连续地存储在磁盘页中. 数据被一行一行地储存, 写完第一行再写第二行. 在读取数据时需要从磁盘中顺序扫描每个元组的完整内容. 显然, 如果每个元组只有少量属性的值对查询有用时, NSM模型会浪费许多磁盘空间.
列式数据库采用 DSM(Decomposition Storage Model) 存储模型, 将关系进行垂直分解, 以列为单位存储, 每个列单独存储. 该方法最小化了无用的I/O.
行式存储主要适合于小批量的数据处理, 比如联机事务处理. 列式数据库主要适用于批量数据处理和即席查询(Ad-Hoc Query). 列式数据库的优点是: 降低I/O开销, 支持大量用户并发查询, 数据处理速度比传统方法快100倍, 并且具有更高的数据压缩比.
如果严格从关系数据库的角度来看, HBase并不是一个列式存储的数据库, 毕竟它是以列族为单位进行分解的, 而不是每个列都单独存储. 但是HBase借鉴和利用了磁盘上这种列存的格式, 所以某种角度上来说它可以被视为列式数据库. 常用的商业化列式数据库有: Sybase IQ, Verticad等.
HBase的实现包括3个主要的组件:
客户端并不直接从Master上读取数据, 而是获得Region的存储信息之后直接从Region服务器上读取. 值得提到的是, HBase客户端也并不从Master上获取Region信息, 而是借助于Zookeeper来获取Region的位置信息. 所以Master节点的压力非常小.
显然我们可以看出, 这个策略和HDFS非常相似. 具体的HDFS原理可以查看: HDFS数据存取策略
Region
当HBase的表中数据量非常庞大时, 数据是无法存储在一台机器上的, 需要根据行键的值对表中的行进行分区, 每个行区间构成一个分区, 称之为: Region
, 包含了某个值域区间内的所有数据.
它是负载均衡和数据分发的基本单位, 会被分发到不同的Region服务器上, 但是同一个Region不会出现在两个服务器上.
初始时, 每个表仅有一个Region, 但是随着数据的不断插入, Region的行数量达到一个阈值时, 会被自动分成两个新的Region. 所以随着数据的增加, Region的数量也会不断增加.
每个Region的默认大小是100MB到200MB, 每个Region服务器会管理10-1000个Region.
一个HBase的表可能非常庞大, 会被分裂成很多个Region, 那么客户端是如何找到它们的呢?
每个Region都有一个RegionID, 这个ID具有唯一性. 一个Region可以通过 (表名, 开始主键, RegionID) 来进行唯一的标识. 有了这个唯一标识, 就可以构建一个映射表, 映射表的每个条目包含两项内容:
元数据表
, 又名: .META.表
当.META.表的条目特别多, 一个服务器存储不下的时候, .META.表又会被分成多个Region, 此时又需要构建一个映射表来记录所有元数据的位置, 这个表被称为: 根数据表
, 又名: -ROOT-表
.
-ROOT-表是不会被分割的, 永远只有一个Region存放在一个Redion服务器上, 它的名字在程序中被写死, Master永远知道它的位置.
层次 | 名称 | 作用 |
---|---|---|
第一层 | Zookeeper文件 | 记录了-ROOT-表的位置信息 |
第二层 | -ROOT-表 | 记录了.META.表的Region位置信息, 仅有一个Region, 通过该表可以访问.META.表中的数据 |
第三层 | .META.表 | 记录了用户数据的Region信息, 可以有多个Region, 保存了HBase中所有用户数据的Region位置信息 |
HBase的三层结构
提示
为了加快访问速度, .META.表的全部Region都会保存在内存中.
客户端访问用户数据的步骤为:
访问Zookeeper
获取-ROOT-表的位置信息
访问-ROOT-表
获取.META.表信息
访问.META.表
找到所需Region的具体服务器
前往指定Region服务器读取数据
为了加速这个访问过程, 客户端会做缓存, 所以不需要每次都经历一遍三级寻址的过程. 但是如果Region被分裂, 位置信息发生变换, 则会导致缓存命中失败, 此时客户端会再次执行三级寻址以更新缓存.
接下来我们介绍HBase的运行机制, 包括HBase的架构以及Region服务器、Store和HLog这三者的工作原理.
客户端包含访问HBase接口和本地的一些Region信息缓存. 对于Master和Region, 客户端与它们都是使用rpc通信.
根据前面我们已经知道: Master需要管理若干个Region服务器, 知道它们的状态. 这是一个很繁琐的过程, 但是Zookeeper可以轻松做到. 所有的Region服务器向Zookeeper注册, 然后Zookeeper将会实时监控每一个Region服务器并将它们的状态通知给Master.
不仅如此, Zookeeper还可以帮助 避免Master单点失效的问题. 如果Master只有一个节点, 当这个节点失效的时候整个系统则无法正常运行. 但是可以同时启动多个Master节点, 由Zookeeper来选出一个Leader, 并保证任何时刻总有一个唯一的Master在运行.
Zookeeper还保存了-ROOT-表的地址和Master的信息, 并且存储了HBase的模式, 包括那些表, 每个表中有哪些列族.
Master主要负责表和Region的管理工作
Region服务器是整个HBase最核心的模块, 负责维护分配给自己的Region并相应用户的读写请求.
HBase一般采用HDFS作为底层存储, 其本身并不具备复制和维护副本的功能, 而由HDFS来提供. 当然, 也可以不采用HDFS, 而是使用其他任何支持Hadoop接口的文件系统作为底层存储(如 Amazon S3).
Region服务器是整个HBase中最核心的模块, 我们来看一下一个Region服务器中都包含了哪些部分:
写数据:
commit()
调用返回客户端读数据:
MemStore缓存的容量有限, 系统会周期性调用Region.flushcache()
把MemStore里面的内容写到磁盘StoreFile中去, 然后清空缓存, 并在HLog中写入标记来标识缓存已经全部记录到磁盘中. 每次缓存的刷新操作都会在磁盘上生成一个新的StoreFile文件, 因此每个Store会包含多个StoreFile.
Region服务器在启动时都会查看HLog来确定最近一次缓存刷新后是否又新的写入操作. 如果发现更新则将更新写入MemStore然后刷新缓存, 写到StoreFile中. 最后删除旧的HLog, 并开始为用户提供数据访问服务.
由于每次的MemStore都会生成新的StoreFile, 所以会有许多StoreFile. 而当需要访问Store中的某个值时必须查找所有的这些StoreFile, 效率极低. 一般为了减少时间, 系统会调用Store.compact()
把多个StoreFile进行合并成一个大文件, 这个动作只会在StoreFile文件数量达到一定阈值时才会触发.
Store是Region服务器的核心. 每个Store对应了表中的一个列族的存储, 每个Store包含一个MemStore缓存和若干个StoreFile文件.
MemStore是排序的内存缓存区. 用户写入新数据时系统会首先将数据放入MemStore. 当MemStore满了的时候就会刷新到磁盘中的一个StoreFile中去. 随着StoreFile的数量不断增加, 达到阈值时会触发合并操作, 多个StoreFile会合并成一个大文件. 当单个StoreFile的大小达到一定阈值时, 会触发文件分裂操作, 会被分裂成两个Region然后被Master分配到对应的Region服务器上.
疑惑
其实这个部分我也有点疑惑, 相当于是一个Region服务器的落盘数据也有可能被分配到其他的Region服务器上去, 这听上去有点奇怪. 如果你知道这样做的原因或解释, 欢迎留言.
HLog可以保证在MemStore缓存还未写入文件时Region服务器发生故障的情况下(即MemStore缓存丢失时)可以恢复数据到正确的状态.
HBase系统为每个Region服务器配置了一个HLog文件, 但是一台Region服务器下的所有Region对象都共用这个HLog. 这是一个预写式日志(Write Ahead Log), 即用户数据更新时必须先写到HLog中才能写入MemStore缓存.
当Zookeeper监控到某个Region服务器发生故障时, 会通知Master. Master将会处理该服务器上的HLog文件, 将HLog中不同Region对象的日志拆分出来并存到对应的Region对象的目录下. 然后将失效的Region重新分配到可用的Region服务器上, 并把与该Region相关的HLog记录也发送给相关的Region服务器. 新Region服务器会重新做一遍HLog中的操作, 把日志记录中的数据写入MemStore缓存, 然后刷新到StoreFile磁盘中, 完成数据恢复.
共用一个HLog的好处
值得注意的是, 在同一个Region服务器上的Region对象是公用同一个HLog的. 这样做的目的是多个Region对象的更新操作发生时, 不需要同时打开和写入多个日志文件, 可用减少磁盘寻址次数并提高对表的写操作性能.
本文参考资料