HBase 作为 Google BigTable 的开源实现,广泛应用于互联网行业中。本文作为个人的学习笔记,主要从数据模型、集群架构和单机存储引擎的实现以及与另一个开源列族数据库 Cassandra 的对比四个维度进行学习记录。
HBase 的数据模型
逻辑上来说,和传统关系型数据库类似,HBase也是以表的方式组织数据,每一张表也有数据行和列,但是在 HBase 数据库中的行和列又和关系型数据库稍有不同。下面统一介绍 HBase 数据模型中的一些名词概念:
行:每一行代表着一个数据对象,由行键来唯一标识,行键以字符串的形式存在,而且所有的行都是按照行键的字典序进行排序存储的,按照行键排序可以获得像关系型数据库主键索引一样的特性。
列:由列族和列限定符共同组成。列族需要在表创建时就定义好,表中所有的列都需要组织在列族里面。列族一旦确定后就不能轻易修改,因为它会影响到 HBase 真实的物理存储结构,但是列标识符是可以动态增删的。表中每行都有相同的列族,但是不是每一行的列族都有相同的的列标识,也就是说每行只存储内容不为空的数据,这是一种稀疏的表结构,可以一定程度上避免数据的冗余。
单元格:每一个行键,列键(列族 + 列标识)共同组成一个单元,存储在单元里的数据称为单元数据。每一个单元数据都具有时间戳,默认由系统指定,也可以由用户显示设置。读取单元数据时,如果时间戳没有被指定,则默认返回最新的数据。每个单元数据默认的版本数量为3。
HBase 使用了 BigTable 的数据模型,本质上是一个三维的映射表,最基础的存储单元是由(行主键、列主键、时间)三维主键定位的,可以看成是 rowkey - columnkey - value 的二级嵌套字典,如果用编程语言来表示其实就是一个 sortedmap。
物理上,列式存储数据库以列为单位聚合数据,然后将列值顺序地存入磁盘,而行式存储数据库则是连续地存储整行。这其中带来的好处主要有两点:
对于某些特定的查询,如果只关心少数几个列的值,列式数据库只需要读取对应数据列的数据,而行式数据库则需要读取所有的数据列,这样可以减少不必要的 IO 操作。而且列式数据库通常还支持列族,即将多个经常一起访问的数据列的各个值放在一起,可以从相同的地方一次性读取多个数据列的值,避免列多个数据列的合并。
同一个数据列的数据重复度高,在数据压缩时有很大的优势,提高压缩比有利于在返回结果时降低带宽的消耗。而且可以针对列式存储做专门的索引优化,比如位图索引
HBase 集群架构
HBase 构建在 HDFS 上,在文件系统上增加一层分布式索引层,依赖 Zookeeper 进行服务器选举及全局信息维护。
HBase 中每张表由一个或多个 region (在 BigTable 中称为子表)组成,region 本质上是以行键排序的连续存储区间,等同于数据库分区中用的范围划分,可以被分配到若干台物理服务器上均摊负载,是 HBase 中扩展和负载均衡的基本单元。
HBase 中每一个 region 只能由一台 region 服务器加载,每一台 region 服务器可以同时加载多个 region
主要由三个部分组成:客户端程序库、主控服务器和多个 region 服务器。
客户端程序库:提供 HBase 的访问接口,应用程序通过客户端程序库对表格进行 CRUD 等操作。客户端程序库通过 Zookeeper 获取一些控制信息,但所有表格数据内容都在客户端与 region 服务器之间直接传送
主控服务器:负责整个系统的管理工作,包括 region 的分配、region 服务器的负载均衡、region 服务器的故障转移等
Region 服务器:主要负载 region 的数据存储和管理,同时需要响应客户端程序的读写请求
HBase 单机存储引擎的实现
基于 LSM 树实现的单机存储引擎,LSM 树的思想非常朴素,就是将对数据的修改增量保存在内存中,达到指定的大小限制后将这些修改操作批量写入磁盘,读取时需要合并磁盘中的历史数据和内存中最近的修改操作。LSM 树的优势在于有效地规避了磁盘随机写入的问题,但是读取时可能需要访问较多的磁盘文件。
HBase 单机存储引擎主要包括:内存中的 MemTable 和不可变的 MemTable 以及磁盘上的几种主要文件:当前( Current ) 文件、清单(Manifest ) 文件、操作日志(Commit Log)文件以及 HFile 文件。
HFile 文件
HFile 是 HBase 内部用来存储数据的文件,主要存储的是经过排序的键值映射结构。每个 HFile 文件划分为两块:数据存储区和索引区。数据存储区用来存储具体的数据,本身又被划分成小的数据块,每个块的默认大小时 64 KB,每次读取的单位就是一个数据块。索引区记载了每个数据块存储的“行主键”范围及其在 SSTable 中的位置信息。当把 HFile 打开并加载到内存中时,系统将索引区优先加载到内存中,当要读取一个数据块时,首先在内存的块索引中进行二分查找,快速定位某条“行记录”在 HFile 中的位置信息,之后就可以根据位置信息一次性读取某个数据块,找到实际要找的键。
CommitLog
CommitLog 的主要作用是为了实现在 region 服务器宕机时,对 MemTable 中的数据进行恢复,保证 MemTable 中的数据不会丢失。
读写流程
写入数据时,首先将更新命令记入 CommitLog 文件中,之后将更新数据写入内存的 MemTable 结构中,这样基本就算完成了写入操作。写入操作只涉及一次磁盘顺序写入和一次内存写入,而且MemTable 采用了维护有序记录快速插入查找的 SkipList 数据结构,从而实现高速写入的目的。当 MemTable 里容纳的数据超过设定大小时,将内容刷写到磁盘中,形成一个新的 HFile 文件。这种刷新有两个功能:首先,可以减少内存消耗量;其次,由于 MemTable 内数据量不会无限制地增长,即使这个 region 服务器宕机后重启,那么系统更加 CommitLog 恢复 MemTable 的速度也会较快。
删除数据时,因为存储文件时不可被改变的,所以无法通过移除某个键值对来简单地删除数据。实际上,HBase 是通过做个删除标志,表明给定行已被删除。当检索的时候,略过这些逻辑上删除的数据。
读取数据则是两部分数据合并的结果,一部分是 memstore 中还没有写入磁盘的数据,另一部分是磁盘上的存储文件,每次读取都需要在内存以及各个 HFile 文件中按照从新到老依次查找,代价较高。
合并( compaction )
随着 memstore 中的数据不断刷写到磁盘中,会产生越来越多的 SSTable 文件,为了加快读取速度,HBase 内部会执行合并 ( compaction ) 操作来将多个文件合并成一个较大的文件。合并有两种类型:minor compaction 和 major compaction。
Minor compaction 将多个小文件重写为数量较少的大文件,减少存储文件的数量,从而增加读操作的效率。这个过程实际上是个多路归并的过程。因为 HFile 的每个文件都是经过排序的,所以合并速度很快,只受到磁盘 I/O 性能的影响。
Major compaction 将一个 region 中一个列族的若干个 HFile 重写为一个新的 HFile。合并过程中,会略过做了删除标记的数据,而且对于那些超过版本号限制的数据以及生存时间到期的数据,在重写数据时就不再写入磁盘。
与 Cassandra 的比较
复制与一致性
在 CAP 理论中,HBase 是一个 CP 系统。HBase 系统保证强一致性,同一个时刻同一个子表只能被一台 region server 服务,也就是说,Master 将 region 分配给某个 region server 服务时需要确保没有其他的 region server 正在服务这个 region。这是通过 ZooKeeper 的互斥锁机制保证的,region server 启动时需要获取 ZooKeeper 互斥锁,当 region server 出现故障,Master 需要等到 region server 的互斥锁失效,才能把它上面的 region 迁移到其他的 region server。这也直接导致,在 region server 节点出现故障时部分数据短时间内无法提供读写服务。
Cassandra 则是一个 AP 系统。Cassandra 采用无中心节点的 P2P 设计,增加了系统可扩展性,但同时带来了一致性问题,本身只保证基本的最终一致性。