基于行和列的key-value数据库,支持单表内上百万列、数十亿行稀疏数据的分布式存储,自动分片,方便扩容,但不支持MySQL中的非聚集索引、事务、触发器、高级查询语句等特性。

存储


在MySQL中保存网页的(历史)数据,会这样存:

hbase按照行存储 hbase按什么集中存放_hbase按照行存储


如果转化成HBase的存储方式:

第一步,对列进行分类:

hbase按照行存储 hbase按什么集中存放_数据_02

RowKey 主键:
对单个Key的数据读写是原子性的,保证并发安全
按字典顺序排序,无特殊分库逻辑

Timestamp 时间戳:
时间戳,64bit整数,微秒
保存每行数据的多个版本,按时间降序排列,可以设置保存最近的多少个版本,自动回收老版本(gc)
RowKey + Timestamp 相当于mysql中唯一索引

Column Family 列簇:
把行分类,每一类叫做一个列簇。
访问控制、存储等大部分操作都是以列簇为基本单位分开进行的。
列簇最多几百个,可以增加,但实际操作中应该尽可能少变化。

Column Qualifier 列名:
ColumnFamily + ColumnQualifier 相当于mysql中的一列,列可以随意增减,没有任何约束。
ColumnQualifier理论上可以无限增加。
实际上也可以无限增加,只要资源足够。

HBase的真实存储方式,按列簇分别存在不同的地方:

第一张表对应 Anchor,按Key的字典序、时间戳降序:

hbase按照行存储 hbase按什么集中存放_布隆过滤器_03

第二张表对应 Contents:

hbase按照行存储 hbase按什么集中存放_数据_04

第三张表对应 People:

hbase按照行存储 hbase按什么集中存放_hbase_05

这种存储方式的优点:

  • 表结构没有严格确定,非常灵活,不需要修改表结构(添加列、删除列、修改列等),就可以单独为某行数据加一列。举个例子,mysql表结构像java bean,所有字段都提前定义好,hbase表结构像map,可以随意添加key/value
  • 空缺列不占用任何空间,充分利用存储资源,容纳更大数据量。 适合数据稀疏(也适合密集),对于数据稀疏和数据密集的场景,性能没有差别。

缺点也很多:

  • 存储粒度变小,写入操作增加,列簇存储在不同机器上时,每台机器都要写入。
  • Key、列簇、列名占用空间。为了节约空间,Rowkey、Column Family和Column Qualifier字符数尽可能少。

 

架构


组成

hbase按照行存储 hbase按什么集中存放_数组_06

HMaster 用来监控和管理RegionServer实例。平衡流量,数据拆分和转移,垃圾回收,创建表,创建列簇等。
HRegionServer 用来读写数据。
简单来说,客户端从注册中心(Zookeeper)读取HRegionServer地址,然后向HRegionServer发送读写请求,并接收返回的结果。
HRegionServer 是执行数据存储和读写的节点,通过LSM的方式管理内存和磁盘。
HRegion 表示某张表的某一范围内的rowKey,Store表示某个列簇,一般情况下Store内的文件数不会很多,这取决于Compaction的参数配置。

hbase按照行存储 hbase按什么集中存放_hbase_07


LSM

设计理念:

分层。磁盘分成从上到下的多层结构,每一层保存一部分HFile文件。
有序。每个HFile内部存储的数据是严格有序的。
以及磁盘随机读写比顺序读写慢几千倍,磁盘顺序写的性能某些环境下甚至高于内存随机写,所以写入过程没有随机写,只有顺序追加(MySQL有很多随机写)。

LSM(Log-Structured Merge-Tree)的结构:

此图表示单个RegionServer上某个Region的某个Store:

hbase按照行存储 hbase按什么集中存放_布隆过滤器_08

写入数据的过程:

hbase按照行存储 hbase按什么集中存放_hbase_09

  1. 写入WAL顺序日志文件,相当于binlog。顺序写入,速度很快。用于故障恢复。
  2. 写入内存中的MemTable,跳跃表(ConcurrentSkipListMap)结构,时间复杂度log2n。
  3. Minor Compaction:MemTable达到阈值的时候,从内存写入到磁盘第0层的一个新的SSTable中。
  4. Major Compaction:定期把第0层的文件k路归并排序(最小堆排序),合并到第1层…清理无效的数据,合并重复的数据。
  5. Major Compaction:L层依次合并到L+1层,文件越来越少,单个文件越来越大。

在《HBase原理与实践》中Minor Compaction表示合并部分小文件,Major Compaction表示对整个store文件合并。

Minor Compaction,从内存到第0层:

hbase按照行存储 hbase按什么集中存放_hbase_10

Major Compaction,从第L层到第L+1层:

hbase按照行存储 hbase按什么集中存放_hbase_11

读取数据的过程:

hbase按照行存储 hbase按什么集中存放_hbase_12

  1. 查询内存中的MemTable,找到就返回。
  2. 查询磁盘的第0层文件。
  3. 从磁盘第1层开始依次往上层遍历,找到即返回,最差情况下最后一层肯定能找到。

修改数据的过程:

不支持修改历史数据。
会把新版本的数据直接追加在后面,可能会同时存在多个key相同但时间戳不同的数据,之后进行Compaction合并排序的时候再处理。

删除数据的过程:

在数据行里添加删除标记,Compaction的时候再垃圾回收。

LSM的缺点:

  • 存在一些遍历操作,没有MySQL中类似于B+树索引。
  • 频繁Compaction,占用计算资源。

LSM的优点:

  • 写和读一样快,甚至写比读快(如果不考虑Compaction)。
  • 分层设计让热数据保留在数据量相对较少的底层级上,查询时更容易找到。

 

文件


结构

HFile由多个Block组成。Block是HBase中最小的数据读取单元,数据从HFile中读取都是以Block为单位执行的。

hbase按照行存储 hbase按什么集中存放_数组_13

最重要的两类Block为数据块和布隆过滤器块。

数据块又可以分为真实kv数据、数据索引两种,可以看出在HFile中也存在树状的索引结构,并非每一次查找都要顺序遍历整个文。

布隆过滤器块可以分为位数组、布隆过滤器索引两种,布隆过滤器索引结构为单层,由root节点直接指向每一个位数组。


优化

BlockCache

在RegionServer级别的内存中缓存访问过的Block,有以下三种:

LRUCache: 默认的缓存机制,只用到JVM堆内存,使用ConrurrentHashMap+LRU的方式存储数据。采用三层设计:single-access,multi-access,in-memory,分别存储第一次读取、多次读取和访问频繁但量少(如meta,以及设置为in-memory的列簇)的数据。这种缓存机制的优点是简单有效,缺点是受限于JVM堆内存产生内存碎片时的full-gc,极端情况下可能影响正常读写。

SlabCache: 数据存储在堆外,分配固定大小如64kb、128kb的缓存区,分别保存小于64kb和小于128kb的Block,在Block数量超过阈值时同样使用LRU淘汰。缺点是固定大小的内存设置会导致浪费,内存实际使用率较低。官方不建议使用此方案。

BucketCache: 使用三种存储方式:堆内存、直接内存和磁盘;和SlabCache类似,使用14种固定大小的块保存Block,分别为4kb、8kb…,512kb;使用异步写入方式,在LRUCache中保存IndexBlock和BloomBlock,在固定大小的BucketCache中保存DataBlock。写入过程如下:
● Block写入到临时缓存RAMCache中
● 写线程从RANCache中读取Block,为其分配内存空间,写入内存
● 将写入的内存地址及偏移量存入BackingMap中
读取时首先从RAMCache查找,找到即返回,否则从BackingMap查找地址,然后根据地址和偏移量查找。

布隆过滤器

每个HFile文件中都有多个布隆过滤器,提高读操作效率。

布隆过滤器是一个位数组,通过位数组过滤。

hbase按照行存储 hbase按什么集中存放_数据_14

把元素x写入文件后,还需要写位数组:

通过5个hash映射函数(f1-f5),将元素x映射到位数组的5个位置上,相应的位置置为1,其他位置还是0。

在文件中查找x之前,先检查位数组:

通过5个hash映射函数(f1-f5),将元素x映射到位数组的5个位置上,如果有任意位置为0,表示元素不在文件中。
位数组越长,占用空间越大,过滤效率越高。

 

实践


  • 读多写少的场景可以配置更大空间的BlockCache和更小空间的MemStore。
  • HFile文件数越多,检索所需要的IO次数越多,所以Compation中和文件数相关的配置不要太大。
  • 如果Compaction占用太多资源,手动配置其在业务低峰时段执行。
  • 为了节省空间,列簇、列名等不要太长。

 

索引


局部索引

本身要保存的数据我们把它叫做真实数据行和真实列,局部在同一Region内创造RowKey为二级索引的索引数据行,这些索引数据行的真实列簇为空,添加一个新的列,值为1,表示索引数据行存在。

例如插入新的真实数据行RowKey以2001开头,值为20011314,想要设置的二级索引列的值为X1Y2,则在插入真实数据行的同时,在该表中还要添加RowKey为2001:X1Y2:20011314的索引数据行。在查找时,首先需要确认可能包含X1Y2的Region集合,然后在这些Region中查找以2001:X1Y2开头的RowKey,并从这些RowKey中解析出真实数据行的RowKey,然后点查对应的真实数据。

由于HBase在Region内保证原子性,索引局部索引写入开销低,但查询开销大。

全局索引

为数据表另外建立一个单独的索引表,RowKey为二级索引值,列为真实数据的RowKey。

全局索引的写入开销较大,因为需要额外的跨Region事务机制保证原子性,读取开销较小,只需要两次点查即可。

 

事务


HBase2.x引入Procedure v2的设计,通过一种分布式任务流框架保证任务的原子性,要么全部执行成功,要么失败回滚。简单来说,保证多个子任务原子性的基本数据结构其实是一个队列和栈,队列用于控制子任务的执行顺序和进程,栈用于失败时回滚。

正常情况下,失败后立刻逐步回滚是合理的,而在某些需要重试失败子任务的场景下,队列+栈的结构也可以派上用场,只需要重新将失败的任务(立即或者延时)加入到队列中等待被再次执行。

 

参考


  • HBase原理与实践
  • Apache HBase
  • Chang, Fay, et al. “Bigtable: A distributed storage system for structured data.” ACM Transactions on Computer Systems (TOCS) 26.2 (2008): 1-26.