2

MemStore中数据落盘之后会形成一个文件写入HDFS,这个文件称为HFile。HFile参考BigTable的SSTable和Hadoop的TFile实现。从HBase诞生到现在,HFile经历了3个版本,其中V2在0.92引入,V3在0.98引入。HFile V1版本在实际使用过程中发现占用内存过多,HFile V2版本针对此问题进行了优化,HFile V3版本和V2版本基本相同,只是在cell层面添加了对Tag数组的支持。鉴于此,本文主要针对V2版本进行分析,对V1和V3版本感兴趣的读者可以参考社区官方文档。

HFile逻辑结构

HFile V2的逻辑结构如图所示
image.png

HFile文件主要分为4个部分:Scanned block部分、Non-scanned block部分、Load-on-open部分和Trailer。

•Scanned Block部分:顾名思义,表示顺序扫描HFile时所有的数据块将会被读取。这个部分包含3种数据块:Data Block,Leaf Index Block以及BloomBlock。其中Data Block中存储用户的KeyValue数据,Leaf Index Block中存储索引树的叶子节点数据,Bloom Block中存储布隆过滤器相关数据。

•Non-scanned Block部分:表示在HFile顺序扫描的时候数据不会被读取,主要包括Meta Block和Intermediate Level Data Index Blocks两部分。

•Load-on-open部分:这部分数据会在RegionServer打开HFile时直接加载到内存中,包括FileInfo、布隆过滤器MetaBlock、Root Data Index和MetaIndexBlock。

•Trailer部分:这部分主要记录了HFile的版本信息、其他各个部分的偏移值和寻址信息。

HFile物理结构

HFile物理结构如图所示。
image.png
实际上,HFile文件由各种不同类型的Block(数据块)构成,虽然这些Block的类型不同,但却拥有相同的数据结构。

Block的大小可以在创建表列簇的时候通过参数blocksize=> '65535'指定,默认为64K。通常来讲,大号的Block有利于大规模的顺序扫描,而小号的Block更有利于随机查询。因此用户在设置blocksize时需要根据业务查询特征进行权衡,默认64K是一个相对折中的大小。

HFile中所有Block都拥有相同的数据结构,HBase将所有Block统一抽象为HFile-Block。HFileBlock支持两种类型,一种类型含有checksum,另一种不含有checksum。为方便讲解,本节所有HFileBlock都选用不含有checksum的HFileBlock。HFileBlock结构如图所示。
image.png

HFileBlock主要包含两部分:BlockHeader和BlockData。其中BlockHeader主要存储Block相关元数据,BlockData用来存储具体数据。Block元数据中最核心的字段是BlockType字段,表示该Block的类型,HBase中定义了8种BlockType,每种BlockType对应的Block都存储不同的内容,有的存储用户数据,有的存储索引数据,有的存储元数据(meta)。对于任意一种类型的HFileBlock,都拥有相同结构的BlockHeader,但是BlockData结构却不尽相同。下表罗列了最核心的几种BlockType。

image.png

HFile的基础Block

1. Trailer Block

Trailer Block主要记录了HFile的版本信息、各个部分的偏移值和寻址信息,图为Trailer Block的数据结构,其中只显示了部分核心字段。
image.png

RegionServer在打开HFile时会加载所有HFile的Trailer部分以及load-on-open部分到内存中。实际加载过程会首先会解析Trailer Block,然后再进一步加载load-on-open部分的数据,具体步骤如下:

1)加载HFile version版本信息,HBase中version包含majorVersion和minorVersion两部分,前者决定了HFile的主版本——V1、V2还是V3;后者在主版本确定的基础上决定是否支持一些微小修正,比如是否支持checksum等。不同的版本使用不同的文件解析器对HFile进行读取解析。

2)HBase会根据version信息计算Trailer Block的大小(不同version的TrailerBlock大小不同),再根据Trailer Block大小加载整个HFileTrailer Block到内存中。Trailer Block中包含很多统计字段,例如,TotalUncompressedBytes表示HFile中所有未压缩的KeyValue总大小。NumEntries表示HFile中所有KeyValue总数目。Block中字段CompressionCodec表示该HFile所使用的压缩算法,HBase中压缩算法主要有lzo、gz、snappy、lz4等,默认为none,表示不使用压缩。

3)Trailer Block中另两个重要的字段是LoadOnOpenDataOffset和LoadOnOpenDataSize,前者表示load-on-open Section在整个HFile文件中的偏移量,后者表示load-on-open Section的大小。根据此偏移量以及大小,HBase会在启动后将load-on-open Section的数据全部加载到内存中。load-on-open部分主要包括FileInfo模块、Root Data Index模块以及布隆过滤器Metadata模块,FileInfo是固定长度的数据块,主要记录了文件的一些统计元信息,比较重要的是AVG_KEY_LEN和AVG_VALUE_LEN,分别记录了该文件中所有Key和Value的平均长度。Root Data Index表示该文件数据索引的根节点信息,布隆过滤器Metadata记录了HFile中布隆过滤器的相关元数据。

2. Data Block
Data Block是HBase中文件读取的最小单元。Data Block中主要存储用户的KeyValue数据,而KeyValue结构是HBase存储的核心。HBase中所有数据都是以KeyValue结构存储在HBase中。

内存和磁盘中的Data Block结构如图所示。
image.png

KeyValue由4个部分构成,分别为Key Length、Value Length、Key和Value。其中,Key Length和Value Length是两个固定长度的数值,Value是用户写入的实际数据,Key是一个复合结构,由多个部分构成:Rowkey、Column Family、Column Qualif ier、TimeStamp以及KeyType。其中,KeyType有四种类型,分别是Put、Delete、DeleteColumn和DeleteFamily。

由Data Block的结构可以看出,HBase中数据在最底层是以KeyValue的形式存储的,其中Key是一个比较复杂的复合结构,这点最早在第1章介绍HBase数据模型时就提到过。因为任意KeyValue中都包含Rowkey、Column Family以及ColumnQualif ier,因此这种存储方式实际上比直接存储Value占用更多的存储空间。这也是HBase系统在表结构设计时经常强调Rowkey、Column Family以及ColumnQualif ier尽可能设置短的根本原因。

HFile中与布隆过滤器相关的Block

布隆过滤器对HBase的数据读取性能优化至关重要。HBase是基于LSM树结构构建的数据库系统,数据首先写入内存,然后异步f lush到磁盘形成文件。这种架构天然对写入友好,而对数据读取并不十分友好,因为随着用户数据的不断写入,系统会生成大量文件,用户根据Key获取对应的Value,理论上需要遍历所有文件,在文件中查找指定的Key,这无疑是很低效的做法。使用布隆过滤器可以对数据读取进行相应优化,对于给定的Key,经过布隆过滤器处理就可以知道该HFile中是否存在待检索Key,如果不存在就不需要遍历查找该文件,这样就可以减少实际IO次数,提高随机读性能。布隆过滤器通常会存储在内存中,所以布隆过滤器处理的整个过程耗时基本可以忽略。

HBase会为每个HFile分配对应的位数组,KeyValue在写入HFile时会先对Key经过多个hash函数的映射,映射后将对应的数组位置为1,get请求进来之后再使用相同的hash函数对待查询Key进行映射,如果在对应数组位上存在0,说明该get请求查询的Key肯定不在该HFile中。当然,如果映射后对应数组位上全部为1,则表示该文件中有可能包含待查询Key,也有可能不包含,需要进一步查找确认。

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

在文件结构上每个位数组对应HFile中一个Bloom Block,因此多个位数组实际上会对应多个Bloom Block。为了方便根据Key定位对应的位数组,HFile V2又设计了相应的索引Bloom Index Block,对应的内存和逻辑结构如图所示。

image.png
Bloom Index Block结构

整个HFile中仅有一个Bloom Index Block数据块,位于load-on-open部分。Bloom Index Block从大的方面看由两部分内容构成,其一是HFile中布隆过滤器的元数据基本信息,其二是构建了指向Bloom Block的索引信息。

Bloom Index Block结构中TotalByteSize表示位数组大小,NumChunks表示Bloom Block的个数,HashCount表示hash函数的个数,HashType表示hash函数的类型,TotalKeyCount表示布隆过滤器当前已经包含的Key的数目,TotalMaxKeys表示布隆过滤器当前最多包含的Key的数目。

Bloom Index Entry对应每一个Bloom Block的索引项,作为索引分别指向scanned block部分的Bloom Block,Bloom Block中实际存储了对应的位数组。Bloom Index Entry的结构见图5-13中间部分,其中BlockKey是一个非常关键的字段,表示该Index Entry指向的Bloom Block中第一个执行Hash映射的Key。BlockOffset表示对应Bloom Block在HFile中的偏移量。

因此,一次get请求根据布隆过滤器进行过滤查找需要执行以下三步操作:

1)首先根据待查找Key在Bloom Index Block所有的索引项中根据BlockKey进行二分查找,定位到对应的Bloom Index Entry。

2)再根据Bloom Index Entry中BlockOffset以及BlockOndiskSize加载该Key对应的位数组。

3)对Key进行Hash映射,根据映射的结果在位数组中查看是否所有位都为1,如果不是,表示该文件中肯定不存在该Key,否则有可能存在。

HFile中索引相关的Block

根据索引层级的不同,HFile中索引结构分为两种:single-level和multi-level,前者表示单层索引,后者表示多级索引,一般为两级或三级。HFile V1版本中只有single-level一种索引结构,V2版本中引入多级索引。之所以引入多级索引,是因为随着HFile文件越来越大,Data Block越来越多,索引数据也越来越大,已经无法全部加载到内存中,多级索引可以只加载部分索引,从而降低内存使用空间。同布隆过滤器内存使用问题一样,这也是V1版本升级到V2版本最重要的因素之一。

V2版本Index Block有两类:Root Index Block和NonRoot Index Block。NonRoot Index Block又分为Intermediate Index Block和Leaf Index Block两种。HFile中索引是树状结构,Root Index Block表示索引数根节点,Intermediate Index Block表示中间节点,Leaf Index Block表示叶子节点,叶子节点直接指向实际Data Block,如图所示。

image.png
HFile文件索引

需要注意的是,这三种Index Block在HFile中位于不同的部分,Root Index Block位于“ load-on-open”部分,会在RegionServer打开HFile时加载到内存中。Intermediate Index Block位于“Non-Scanned block”部分,Leaf Index Block位于“scanned block”部分。

HFile中除了Data Block需要索引之外,Bloom Block也需要索引,Bloom索引结构实际上采用了单层结构,Bloom Index Block就是一种Root Index Block。

对于Data Block,由于HFile刚开始数据量较小,索引采用单层结构,只有RootIndex一层索引,直接指向Data Block。当数据量慢慢变大,Root Index Block大小超过阈值之后,索引就会分裂为多级结构,由一层索引变为两层,根节点指向叶子节点,叶子节点指向实际Data Block。如果数据量再变大,索引层级就会变为三层。

下面针对Root index Block和NonRoot index Block两种结构进行解析(Intermediate Index Block和Ieaf Index Block在内存和磁盘中存储格式相同,都为NonRoot Index Block格式)。

1. Root Index Block
Root Index Block表示索引树根节点索引块,既可以作为Bloom Block的直接索引,也可以作为Data Block多极索引树的根索引。对于单层和多级这两种索引结构,对应的Root Index Block结构略有不同,单层索引结构是多级索引结构的一种简化场景。本书以多级索引结构中的Root Index Block为例进行分析,图为Root Index Block的结构图。
image.png

图中,Index Entry表示具体的索引对象,每个索引对象由3个字段组成:Block Offset表示索引指向Data Block的偏移量,BlockDataSize表示索引指向Data Block在磁盘上的大小,BlockKey表示索引指向Data Block中的第一个Key。

除此之外,还有另外3个字段用来记录MidKey的相关信息,这些信息用于在对HFile进行split操作时,快速定位HFile的切分点位置。需要注意的是单层索引结构和多级索引结构相比,仅缺少与MidKey相关的这三个字段。

Root Index Block位于整个HFile的“ load-on-open ”部分,因此会在RegionServer打开HFile时直接加载到内存中。此处需要注意的是,在Trailer Block中有一个字段为DataIndexCount,表示Root Index Block中Index Entry的个数,只有知道Entry的个数才能正确地将所有Index Entry加载到内存。

2. NonRoot Index Block

当HFile中Data Block越来越多,单层结构的根索引会不断膨胀,超过一定阈值之后就会分裂为多级结构的索引结构。多级结构中根节点是Root Index Block。而索引树的中间层节点和叶子节点在HBase中存储为NonRoot Index Block,但从Block结构的视角分析,无论是中间节点还是叶子节点,其都拥有相同的结构,如图所示。

image.png

和Root Index Block相同,NonRoot Index Block中最核心的字段也是IndexEntry,用于指向叶子节点块或者Data Block。不同的是,NonRoot Index Block结构中增加了Index Entry的内部索引Entry Offset字段,Entry Offset表示IndexEntry在该Block中的相对偏移量(相对于第一个Index Entry),用于实现Block内的二分查找。通过这种机制,所有非根节点索引块(包括Intermediate Index Block和Leaf Index Block)在其内部定位一个Key的具体索引并不是通过遍历实现,而是使用二分查找算法,这样可以更加高效快速地定位到待查找Key。

文章基于《HBase原理与实践》一书


小明的数据脚印
145 声望41 粉丝