Skip to content

分布式数据库HBase

5573字约19分钟

大数据关系型数据库

2024-07-01

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

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

HBase logo
HBase logo

如果想了解其他的非关系型数据库也可以查看文章:[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作为底层存储.

项目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. 全表扫描

行键可以是任意字符串(最大长度64KB, 实际应用中一般为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

Region

当HBase的表中数据量非常庞大时, 数据是无法存储在一台机器上的, 需要根据行键的值对表中的行进行分区, 每个行区间构成一个分区, 称之为: Region, 包含了某个值域区间内的所有数据.

它是负载均衡和数据分发的基本单位, 会被分发到不同的Region服务器上, 但是同一个Region不会出现在两个服务器上.

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

每个Region的默认大小是100MB到200MB, 每个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存放在一个Redion服务器上, 它的名字在程序中被写死, 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对象的更新操作发生时, 不需要同时打开和写入多个日志文件, 可用减少磁盘寻址次数并提高对表的写操作性能.