Skip to content

HBase 分布式数据库

5599字约19分钟

大数据分布式NoSQL

2024-07-01

在接触过大量的传统关系型数据库后你可能会有一些新的问题: 无法整理成表格的海量数据该如何储存? 在数据非常稀疏的情况下也必须将数据存储成关系型数据库吗? 除了关系型数据库我们是否还有别的选择以应对 Web 2.0 时代的海量数据?

如果你也曾经想到过这些问题, 那么 HBase 将是其中的一个答案, 它是非常经典的列式存储数据库. 本文首先介绍 HBase 的由来以及其与关系数据库的区别, 其次介绍其访问接口、数据模型、实现原理和运行机制. 即便之前没有接触过 HBase 的相关知识也不影响阅读该文章.

HBase logo
HBase logo

如果想了解其他的非关系型数据库也可以查看文章: NoSQL 数据库.

概述

HBase 是谷歌公司 BigTable 的开源实现. 而 BigTable 是一个分布式存储系统, 使用谷歌分布式文件系统 GFS 作为底层存储, 主要用来存储非结构化和半结构化的松散数据. HBase 的目标是处理非常庞大的表, 可以通过水平扩展的方式利用廉价计算机集群处理超过 10 亿行数据和百万列元素组成的数据表.

GFS、HDFS、BigTable 以及 HBase 的关系

HDFS 是 GFS 的开源实现. HBase 是 BigTable 的开源实现.

GFS 是 BigTable 的底层文件系统, BigTable 的数据存储在 GFS 上.

HDFS 是 HBase 的底层存储方式. 虽然 HBase 可以使用本地文件系统, 但是为了提高数据可靠性一般还是会选择 HDFS 作为底层存储.

项目BigTableHBase
文件存储系统GFSHDFS
海量数据处理系统MapReduceHadoop MapReduce
协同服务系统ChubbyZookeeper

HBase 和 BigTable 底层技术对应关系

与传统的数据库相比主要区别在于:

  1. 数据类型: 关系数据库采用关系模型, HBase 则采用更加简单的数据模型, 将数据存储为未经解释的字符串.
  2. 数据操作: 关系数据库通常包括丰富的操作, 涉及复杂的多表连接. HBase 则不存在复杂的多表关系, 只有简单的增删查改.
  3. 存储模式: 关系数据库是基于行模式存储的, 元组或行被连续地存储在磁盘中. HBase 是基于列存储的.
  4. 数据索引: 关系数据库可以针对不同列构建复杂的多个索引以提高访问效率. HBase 则只有一个索引--行键.
  5. 数据维护: 关系数据库中更新操作会用新值替换旧值. HBase 则会保留旧数据, 仅仅生成一个新的版本.
  6. 可伸缩性: 关系数据库很难进行横向扩展, 纵向扩展的空间也比较有限. HBase 作为分布式数据库可以轻易地通过增加集群中的机器数量来达到性能的伸缩.

访问接口

HBase 提供了多种访问方式, 不同的方式适用于不同的场景.

类型特点场合
Native Java API最常规高效的访问方式适合 Hadoop MapReduce 作业并行批处理 HBase 表数据
HBase ShellHBase 的命令行工具, 最简单的接口适合 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 种方式:

  1. 通过单个行键访问
  2. 通过行键区间访问
  3. 全表扫描

行键可以是任意字符串(最大长度 64 KB, 实际应用中一般为 10 - 100 字节). 在 HBase 内部将行键保存为 字节数组, 按照行键的 字典序 排序. 所以在设计行键时可以充分考虑该特性, 将需要一起读的行存储在一起.

列族

HBase 中一个表被分为多个列族, 列族是最基本的访问控制单元. 表中的每个列都必须属于一个列族, 我们可以将其理解为 把列按照需求分到不同的组中, 就如同整理文件到不同的文件夹中去.

为什么要这么做?

  1. 控制权限. 我们通过列族可以实现权限的控制, 例如某些应用只可以修改某些数据.
  2. 获得更高的压缩率. 同一个列族中的所有数据都属于同一种数据类型, 着通常意味着更高的压缩率.

缺点

  1. 列族数量不可太多. HBase 的一些缺陷导致列族只能有几十个.
  2. 不能频繁修改.

列限定符

列族中的数据是通过列限定符来定位的. 列限定符无需事先定义, 也没有数据类型, 总被视为字节数组 byte[].

单元格

在 HBase 的表中, 通过行、列和列限定符可以确定一个"单元格(Cell)". 单元格中存储的数据没有数据类型, 总被视为字节数组 byte[].

每个单元格中可以保留一个数据的多个版本, 每个版本对应一个不同的时间戳.

时间戳

每个单元格都保留了同一个数据的多个版本, 这些版本采用时间戳进行索引. 事实上每一次对于一个单元格执行的操作(增删改)时, HBase 都会自动生成并存储一个时间戳, 通常这个时间戳是 64 位整型. 当然, 这个时间戳也可以由用户自己赋值, 用以避免应用程序中出现数据版本冲突.

一个单元格中的不同版本的数据是以时间戳降序排序的, 以便于读到最新的数据版本.

我认为下面的一张图可以很好地表述上面的 5 个概念. 类比于关系数据库, 行键就是主键行号, 列限定符就是列名, 列族就是列名组成小组的组名, 单元格就是具体存储数据的格子, 时间戳则标识了一个单元格中不同时间的数据版本.

一个 HBase 数据模型的实例
一个 HBase 数据模型的实例

数据坐标

相较于我们所熟悉的关系数据库, HBase 无法仅使用行号和列号确定一个数据. 在 HBase 中, 我们需要: 行键、列族、列限定符和时间戳 这 4 个东西来确定一个数据.

[行键, 列族, 列限定符, 时间戳] 被称为是 HBase 的坐标, 可以通过这个坐标来直接访问数据. 在这种层面上讲, HBase 也可以被视为一个键值数据库.

概念视图

在 HBase 的概念视图中, 一个表是一个稀疏、多维的映射关系.

时间戳列族 contents列族 anchor
com.cnn.wwwt5anchor:cnnsi.com="CNN"
t4anchor:my.look.ca="CNN.com"
com.cnn.wwwt3contents:html="xxxx"
t2contents:html="xxxx"
t1contents:html="xxxx"
::: center HBase 概念视图 :::

上表存储了一个网页的页面内容(html 代码)和一些反向连接. contents 中存储的是网页内容, anchor 中存储的是反向连接. 不过有几个地方需要额外注意:

  1. 行键. 行键采用的是 url 的倒序, 因为 HBase 的行键采用字典倒序排列, 这样可以使得相同的网页都保存在相邻的位置.
  2. 每个行都包含了相同的列族, 即便有些列族不需要存储数据(为空).

物理视图

列族 contents

时间戳列族 contents
com.cnn.wwwt3contents:html="xxxx"
t2contents:html="xxxx"
t1contents:html="xxxx"

列族 anchor

时间戳列族 anchor
com.cnn.wwwt5anchor:cnnsi.com="CNN"
t4anchor:my.look.ca="CNN.com"

我们可以轻易发现, 在物理的存储层面上来看 HBase 采用了基于列的存储方式, 而不是传统关系数据库那样基于行来存储. 这也是 HBase 与传统关系数据库间的重要区别.

与概念视图的不同

  1. 列族的分开存放. 可以看到 contents 和 anchor 两个列族被分开存放.
  2. 不存在空值. 在概念视图中有些列是空的, 但是在物理视图中这些值根本不会被存储.

总结

行式数据库使用 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个主要的组件:

  1. 库函数: 链接到每个客户端.
  2. 一个 Master 主服务器: 负责管理和维护 HBase 表的分区信息. 把特定的 Region 分配到 Region 服务器, 确保负载均衡. 监控集群中的 Region 服务器, 将故障的 Region 重新分配到新的 Region 服务器上.
  3. 若干个 Region 服务器: 负责存储和维护分配给自己的 Region.

客户端并不直接从 Master 上读取数据, 而是获得 Region 的存储信息之后直接从 Region 服务器上读取. 值得提到的是, HBase 客户端也并不从 Master 上获取 Region 信息, 而是借助于 Zookeeper 来获取 Region 的位置信息. 所以 Master 节点的压力非常小.

显然我们可以看出, 这个策略和 HDFS 非常相似. 具体的 HDFS 原理可以查看: HDFS 数据存取策略.

表和 Region

当 HBase 的表中数据量非常庞大时, 数据是无法存储在一台机器上的, 需要根据行键的值对表中的行进行分区, 每个行区间构成一个分区, 称之为: Region, 包含了某个值域区间内的所有数据. 它是负载均衡和数据分发的基本单位, 会被分发到不同的 Region 服务器上, 但是同一个 Region 不会出现在两个服务器上.

初始时, 每个表仅有一个 Region, 但是随着数据的不断插入, Region 的行数量达到一个阈值时, 会被自动分成两个新的 Region. 所以随着数据的增加, Region 的数量也会不断增加.

每个 Region 的默认大小是 100 MB 到 200 MB, 每个 Region 服务器会管理 10 - 1000 个 Region.

Region 的定位

一个 HBase 的表可能非常庞大, 会被分裂成很多个 Region, 那么客户端是如何找到它们的呢?

每个 Region 都有一个 RegionID, 这个 ID 具有唯一性. 一个 Region 可以通过 (表名, 开始主键, RegionID) 来进行唯一的标识. 有了这个唯一标识, 就可以构建一个映射表, 映射表的每个条目包含两项内容:

  1. Region 标识: (表名, 开始主键, RegionID).
  2. Region 服务器标识. 这样我们就知道某个 Region 被保存在哪个 Region 服务器上. 这个映射表被称为元数据表, 又名: .META.表.

.META.表 的条目特别多, 一个服务器存储不下的时候, .META.表 又会被分成多个 Region, 此时又需要构建一个映射表来记录所有元数据的位置, 这个表被称为: 根数据表, 又名: -ROOT-表.

-ROOT-表是不会被分割的, 永远只有一个 Region 存放在一个 Region 服务器上, 它的名字在程序中被写死, Master 永远知道它的位置.

层次名称作用
第一层Zookeeper 文件记录了 -ROOT-表 的位置信息
第二层-ROOT-表记录了 .META.表 的 Region 位置信息, 仅有一个 Region, 通过该表可以访问 .META.表 中的数据
第三层.META.表记录了用户数据的 Region 信息, 可以有多个 Region, 保存了 HBase 中所有用户数据的 Region 位置信息

HBase 的三层结构

提示

为了加快访问速度, .META.表 的全部 Region 都会保存在内存中.

客户端访问用户数据的步骤为:

  1. 访问 Zookeeper.

    获取 -ROOT-表 的位置信息.

  2. 访问 -ROOT-表 .

    获取 .META.表 信息.

  3. 访问 .META.表.

    找到所需 Region 的具体服务器.

  4. 前往指定 Region 服务器读取数据.

为了加速这个访问过程, 客户端会做缓存, 所以不需要每次都经历一遍三级寻址的过程. 但是如果 Region 被分裂, 位置信息发生变换, 则会导致缓存命中失败, 此时客户端会再次执行三级寻址以更新缓存.

运行机制

接下来我们介绍 HBase 的运行机制, 包括HBase的架构以及 Region 服务器、Store 和 HLog 这三者的工作原理.

HBase 架构系统

HBase 架构系统
HBase 架构系统

客户端

客户端包含访问 HBase 接口和本地的一些 Region 信息缓存. 对于 Master 和 Region, 客户端与它们都是使用 rpc 通信.

Zookeeper服务器

根据前面我们已经知道: Master 需要管理若干个 Region 服务器, 知道它们的状态. 这是一个很繁琐的过程, 但是 Zookeeper 可以轻松做到. 所有的 Region 服务器向 Zookeeper 注册, 然后 Zookeeper 将会实时监控每一个 Region 服务器并将它们的状态通知给 Master.

不仅如此, Zookeeper 还可以帮助 避免 Master 单点失效的问题. 如果 Master 只有一个节点, 当这个节点失效的时候整个系统则无法正常运行. 但是可以同时启动多个 Master 节点, 由 Zookeeper 来选出一个 Leader, 并保证任何时刻总有一个唯一的 Master 在运行.

Zookeeper 还保存了 -ROOT-表 的地址和 Master 的信息, 并且存储了 HBase 的模式, 包括那些表, 每个表中有哪些列族.

Master

Master 主要负责表和 Region 的管理工作

  1. 管理用户对表的增删查改.
  2. 实现不同 Region 服务器之间的负载平衡.
  3. 在 Region 分裂或合并后, 负责重新调整 Region 的分布.
  4. 对发生故障的 Region 服务器上的 Region 进行迁移.

Region服务器

Region 服务器是整个 HBase 最核心的模块, 负责维护分配给自己的 Region 并相应用户的读写请求.

HBase 一般采用 HDFS 作为底层存储, 其本身并不具备复制和维护副本的功能, 而由 HDFS 来提供. 当然, 也可以不采用 HDFS, 而是使用其他任何支持 Hadoop 接口的文件系统作为底层存储(如 Amazon S3).

Region 服务器工作原理

Region 服务器是整个 HBase 中最核心的模块, 我们来看一下一个 Region 服务器中都包含了哪些部分:

  1. 一系列 Region 对象: 每个 Region 对象又有多个 Store 组成, 每个 Store 对应了表中的一个列族存储. 每个 Store 又包括一个 MemStore(内存中的缓存)和若干个 StoreFile(磁盘B树文件).
  2. HLog: 磁盘上的记录文件, 记录所有的更新操作.

1. 用户读写数据过程

写数据:

  1. 用户数据被写入 MemStore 和 HLog 中
  2. 写入 HLog 后, commit() 调用返回客户端

读数据:

  1. Region 服务器访问 MemStore.
  2. 未命中时前往 StoreFile 寻找.

2. 缓存的刷新

MemStore 缓存的容量有限, 系统会周期性调用 Region.flushcache() 把 MemStore 里面的内容写到磁盘 StoreFile 中去, 然后清空缓存, 并在 HLog 中写入标记来标识缓存已经全部记录到磁盘中. 每次缓存的刷新操作都会在磁盘上生成一个新的 StoreFile 文件, 因此每个 Store 会包含多个 StoreFile.

Region 服务器在启动时都会查看 HLog 来确定最近一次缓存刷新后是否又新的写入操作. 如果发现更新则将更新写入 MemStore 然后刷新缓存, 写到 StoreFile 中. 最后删除旧的 HLog, 并开始为用户提供数据访问服务.

3. StoreFile 的合并

由于每次的 MemStore 都会生成新的 StoreFile, 所以会有许多 StoreFile. 而当需要访问 Store 中的某个值时必须查找所有的这些 StoreFile, 效率极低. 一般为了减少时间, 系统会调用 Store.compact() 把多个StoreFile进行合并成一个大文件, 这个动作只会在StoreFile文件数量达到一定阈值时才会触发.

Store工作原理

Store是Region服务器的核心. 每个Store对应了表中的一个列族的存储, 每个Store包含一个MemStore缓存和若干个StoreFile文件.

MemStore是排序的内存缓存区. 用户写入新数据时系统会首先将数据放入MemStore. 当MemStore满了的时候就会刷新到磁盘中的一个StoreFile中去. 随着StoreFile的数量不断增加, 达到阈值时会触发合并操作, 多个 StoreFile 会合并成一个大文件. 当单个 StoreFile 的大小达到一定阈值时, 会触发文件分裂操作, 会被分裂成两个 Region 然后被 Master 分配到对应的 Region 服务器上.

疑惑

其实这个部分我也有点疑惑, 相当于是一个 Region 服务器的落盘数据也有可能被分配到其他的 Region 服务器上去, 这听上去有点奇怪. 如果你知道这样做的原因或解释, 欢迎留言.

HLog 工作原理

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 对象的更新操作发生时, 不需要同时打开和写入多个日志文件, 可用减少磁盘寻址次数并提高对表的写操作性能.




变更历史

最后更新于: 查看全部变更历史
  • docs: udpate docs

    于 2024/12/1
  • docs: update docs

    于 2024/11/29
  • docs: update docs

    于 2024/11/19
  • 整理图片和文章

    于 2024/11/7
  • 整理文章格式

    于 2024/10/29
  • update

    于 2024/10/17

Keep It Simple