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 V2的逻辑结构如图所示数据库
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的版本信息、其余各个部分的偏移值和寻址信息。app
HFile物理结构如图所示。
实际上,HFile文件由各类不一样类型的Block(数据块)构成,虽然这些Block的类型不一样,但却拥有相同的数据结构。异步
Block的大小能够在建立表列簇的时候经过参数blocksize=> '65535'指定,默认为64K。一般来说,大号的Block有利于大规模的顺序扫描,而小号的Block更有利于随机查询。所以用户在设置blocksize时须要根据业务查询特征进行权衡,默认64K是一个相对折中的大小。函数
HFile中全部Block都拥有相同的数据结构,HBase将全部Block统一抽象为HFile-Block。HFileBlock支持两种类型,一种类型含有checksum,另外一种不含有checksum。为方便讲解,本节全部HFileBlock都选用不含有checksum的HFileBlock。HFileBlock结构如图所示。oop
HFileBlock主要包含两部分:BlockHeader和BlockData。其中BlockHeader主要存储Block相关元数据,BlockData用来存储具体数据。Block元数据中最核心的字段是BlockType字段,表示该Block的类型,HBase中定义了8种BlockType,每种BlockType对应的Block都存储不一样的内容,有的存储用户数据,有的存储索引数据,有的存储元数据(meta)。对于任意一种类型的HFileBlock,都拥有相同结构的BlockHeader,可是BlockData结构却不尽相同。下表罗列了最核心的几种BlockType。
1. Trailer Block
Trailer Block主要记录了HFile的版本信息、各个部分的偏移值和寻址信息,图为Trailer Block的数据结构,其中只显示了部分核心字段。
RegionServer在打开HFile时会加载全部HFile的Trailer部分以及load-on-open部分到内存中。实际加载过程会首先会解析Trailer Block,而后再进一步加载load-on-open部分的数据,具体步骤以下:
1)加载HFile version版本信息,HBase中version包含majorVersion和minorVersion两部分,前者决定了HFile的主版本——V一、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结构如图所示。
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尽量设置短的根本缘由。
布隆过滤器对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,对应的内存和逻辑结构如图所示。
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中索引结构分为两种: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,如图所示。
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的结构图。
图中,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结构的视角分析,不管是中间节点仍是叶子节点,其都拥有相同的结构,如图所示。
和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原理与实践》一书