HFile原理解析-HBase高性能查询之道

166 阅读8分钟

在大数据分析领域,有几种通用的文件格式,如Parquet、RCFile、ORCFile、CarbonData等等,这些文件大都是基于列示的设计结构,来加速通用的分析型查询。但是在实时数据库领域,却以各种私有的文件格式最为常见,如Bigtable的SSTable,HBase的HFile,Kudu的DiskRowSets,Cassandra的变种SSTable,MongoDB支持的每一种Storage Engine都是私有的文件格式设计等等。

作为Hbase高性能随机查询的基础本文将详细解读HBase的HFile设计,第一部分为HFile原理概述,第二部分为HFile从无到有的生成过程。

HFile原理概述

最初的HFile格式(HFile V1)参考了BigTable的SSTable以及Hadoop的TFile(HADOOP-3315)。如下图所示:

HFile在生成之前,数据在内存中已经是按序组织的,存放用户数据的KeyValue,被存储在一个个默认为64kb大小的Data Block中,在Data Index部分存储了每个Data Blick的索引信息({Offset、Size、FirstKey}),而Data Index 的索引信息({Data Index Offset,Data Block Count})被存储在HFile的Trailer部分呢。除此之外,在Meta Block部分还存储了Bloom Filter的数据,下图非常直观的表达出HFile V1的数据组织结构:

这种设计简单、直观。但这个HFile版本存在严重的问题:Region Open的时候需要加载所有的Data Block Index 数据,另外,第一次读取的时候需要加载所有的Bloom Fliter数据到内存中。一个HFile中的Bloom Filter的数据大小可达百MB级别,一个RegionServer启动的时候可能要加载数GB的Data Block Index 数据。这在一个大数据量的集群中,几乎无法忍受。

一个Data Block 在Data Block Index中的索引信息包含({Offset、Size、Firstkey}),BlockOffset使用Long型数字表示,Size使用Int表示即可。假设用户数据RowKey的长度50bytes,那么,一个64KB的Data Block在Data Block Index中的一条索引数据大小为62KB。

假设一个RegionServer中有500个Region,每个Region的数量大小为10GB(倘如这是Data Blocks的总大小),在这个RegionServer上,约有81920000个Data Blocks,此时,Data Block Index所占用的大小约为4.7GB

这是HFIle V2设计的初衷,HFIle V2期望限制降低RegionServer启动加载HFile的时延,更希望解决一次全量加载数百MB甚至GB级别的Data Block Index 和BloomFilter数据带来的时延过大问题。下图是HFile V2的数据组织结构:

与HFile V1比较,我们来看下V2的几点显著变化:

1.分层索引

V2版本引入多级索引。之所以引入多级索引,是因为随着HFIle文件越来越大,Data Block越来越多,索引数据也越来越大,已经无法全部加载到内存中了,多级索引可以只加载部分索引,从而降级内存使用空间。

无论是Data Block Index 还是Bloom Filter,都采用了分层索引的设计。

Data Block的索引,在HFile V2中最多可支持三层索引:最底层的Data Block Index称之为Leaf Index Block,可直接索引到Data Block;中间层称之为Intermediate Index Block,最上层称之为Root Data Index,Root Data index存放一个称之为"Load-on-open"区域,Region Open时会被加载到内存中。基本的索引逻辑为:由Root Data Index 索引到Intermediate Index Block,再由Intermediate Index Block 索引到leaf Index Block,最后由leaf Index Block 查找到对应的Data Block。在实际场景中,由于HFile文件大小的关系,Intermediate Index Block基本不存在,因此,索引逻辑被简化为:由Root Data Index直接索引到Leaf Index Block,再由Leaf Block查找到对应的Data Block。

Bloom Filter也被拆成了多个Bloom Block,在"Load-on-open-Section"区域中,同样存放了所有Bloom Block的索引数据。

2.交叉存放

在“Scanned Block Section”区域,Data Block(存放用户数据KeyValue)、存放Data Block索引的Leaf Index Block(存放Data Block的索引)与Bloom Block(Bloom Filter数据)交叉存在。

3.按需读取

无论是Data Block的索引数据,还是Bloom Filter数据,都被拆成了多个Block,基于这样的设计,无论是索引数据,还是Bloom Filter,都可以按需索取,避免在Region Open阶段或读取阶段一次性读入大量的数据,有效降低时延。

HFile生成流程

在本篇章,我们以Flush流程为例,介绍如何一步步生成HFile的流程,来加深对HFile原理的理解。

起初HFile中还没有任何的Block,数据还在MemStore中。

Flush发生时,创建了HFile Writer,第一个空的Data BLock出现,初始化后的Data Block中为Header部分预留了空间,Header部分用来存放一个Data Block的元数据信息。

而后,位于MemStore的KeyValues被一个个append到位于内存中的第一个Data Block中:

当Data Block增长到设置大小,一个Data Block 被停止写入,该Data Block将经历如下一系列处理流程:

  1. 如果配置了启动压缩或加密特性,对Data Block的数据按相应的算法进行压缩和加密。

  1. 在预留的Header区,写入该Data Block的元数据信息,包含({压缩前的大小、压缩后的大小、上一个Block的偏移信息、Checksum元数据信息})等信息,下图是一个Header的完整结构

  1. 生成Checksum信息

  1. Data Block以及Checksum信息通过HFile Writer中的输出流写入到HDFS中
  2. 为输出的Data Block生成一条索引记录,包含这个Data Block的(起始Key、偏移、大小)信息,这条索引记录被暂时记录到内存的Block Index Chunk中

至此,已经写入第一个Data Block,并且Block Index Chunk中记录了关于这个Data Block的一条索引记录。

随着Data Blocks数量的不断增多,Block Index Chunk中的记录数量也在不断变多,当BLock Index Chunk达到一定大小后,Block Index Chunk也经与Data Block 的类似处理流程后输出到HDFS中,形成第一个Leaf Index Block:

此时,已输出的scanned Block Section部分的构成如下:

正是因为Leaf Index Block与Data Block在scanned Block Section交叉存在,Leaf Index Block被称之为Inline Block(Bloom Block 也属于Inline Block)。在内存中还有一个Root Index Chunk 用来记录每个Leaf Index Block 的索引信息:

从Root Index 到Leaf Data Block 再到Data Block的索引关系如下:

我们假设没有Bloom Filter 数据。当MemStore中所有的Keyvalue全部写完以后,HFile Writer开始在close方法中处理最后的"收尾工作":

  1. 写入最后一个Data Block。
  2. 写入最后一个Leaf Index Block。

如上属于Scanned Block Section部分的"收尾工作"。

  1. 如果有MetaData则写入位于Non-Scanned Block Section 区域的Meta Blocks,事实上这部分为空。
  2. 写Root BLock Index Chunk 部分数据:

如果Root Block Index Chunk 超出了预期设置大小,则输出位于Non-Scanned Block Section区域的Intermediate Index Block 数据,以及生成并输出Root Index BLock(记录Intermediate Index Block 索引)到Load-On-Open-Section部分。

  1. 写入用来索引Meta Blocks的Meta Index数据(是事实上这部分知识写入一个空的Block)。
  2. 写入FileInfo信息
  3. 写入Bloom Filter索引信息
  4. 写入Trailer部分信息,Trailer 中包含:

Root Index Block的Offset、FileInfo部分Offset、Data Block Idnex层级、Data Block Index 数据总大小、第一个Data BLock的Offset、最后一个Data Block的Offset、Comparator信息、Root Index Block的Entries数量、加密算法类型、Meta Index Block 的Entries数量,整个HFile文件未压缩大小、整个HFile中所包含的KeyValue总个数、压缩算法类型等。

至此,一个完整的HFile已生成,我们可以通过下图简单回顾下Root Index BLock、Leaf Index Block、Data Block 所处的位置以及索引关系:

简单起见,上文中刻意忽略了Bloom Filter部分。Bloom Filter 被用来快速判断一条记录是否存在一个大的集合中存在,采用了多个Hash函数 + 位图的设计方式。写入数据时,一个记录经过X个Hash函数运算后,被映射到位图中的X个位置,判断一条记录是否存在时,也是通过这个X个Hash函数计算后,获得X个位置,如果位图中的这个X个位置都为1,则表明这个记录存在,如果至少有个为0,则该记录“一定不存在”。

可以想像,HFile文件越大,里面存储的KeyValue值越多,位数组就会相应越大。一旦位数组太大就不适合直接加载到内存了,因此HFile V2在设计上将位图数组进行了拆分,拆分成了多个独立的位图数据组(根据Key进行拆分,一部分连续的Key使用一个位数组)。这样,一个HFile中就会包含多个位数组,根据Key进行查询时,首先定位到具体的位数组,只需要加载次位数组到内存进行过滤即可,从而降低内存开销。混合了BloomFilter Block以后的HFile构成如下图所示: