设为首页 加入收藏

TOP

HBase之HFile
2019-05-15 01:31:28 】 浏览:323
Tags:HBase HFile

原文地址:

https://blog.csdn.net/yulin_hu/article/details/81673695

一.HFile介绍:(前言)

HFile是HBase中重要的一个存在,可以说是HBase架构中最小的结构,HBase的数据都在HFile中。我们知道HBase隶属于Hadoop生态系统,HFile从根本上来说是hdfs中的文件,只是他有自己特殊的格式。从下面这张图中,我们可以看到HFile在整个HBase中的位置以及其与Hadoop的关系。

一般来说,我们都是通过HBase的Client来读写HBase,可是当我们要写大量的数据以及我们需要读取整个HFile的数据的时候,再通过Client就会极大地增加HBase的压力。此时如果我们直接操作HFile,不仅会快,也降低了HBase的压力。

本文主要是对HFile做一个简介,可能偏理论一点,中间也会配合一点源码的说明,了解了HFile的结构之后会对HBase的存储有更加深入的认识,之后我们也会对如何直接操作HFile做一个说明。

二. HFile的变迁及逻辑结构:

从HBase开始到现在,HFile经历了三个版本,其中V2在0.92引入,V3在0.98引入。HFileV1版本的在实际使用过程中发现它占用内存多,HFile V2版本针对此进行了优化,HFile V3版本基本和V2版本相同,只是在cell层面添加了Tag数组的支持。

HFile V1的逻辑数据组织格式如下图,DataBlock区域、MetaBlock(bloomfilter) 与FileInfo、DataBlockIndex、MetaBlockIndex、Trailer分离。 HFileV1版本的在实际使用过程中发现它占用内存多,并且Bloom File和Block Index会变的很大,而引起启动时间变长。其中每个HFile的Bloom Filter可以增长到100MB,这在查询时会引起性能问题,因为每次查询时需要加载并查询Bloom Filter,100MB的Bloom Filer会引起很大的延迟;另一个,Block Index在一个HRegionServer可能会增长到总共6GB,HRegionServer在启动时需要先加载所有这些Block Index,因而增加了启动时间。为了解决这些问题,在0.92版本中引入HFileV2版本:

图 1 hfileV1

HFile V2的逻辑数据组织格式如下图:文件主要分为四个部分:Scanned block section,Non-scanned block section,Opening-time data section和Trailer。

Scanned block section:表示顺序扫描HFile时(包含所有需要被读取的数据)所有的数据块将会被读取,包括Leaf Index Block和Bloom Block;

Non-scanned block section:HFile顺序扫描的时候该部分数据不会被读取,主要包括Meta Block和Intermediate Level Data Index Blocks两部分;

Load-on-open-section:这部分数据在HBase的region server启动时,需要加载到内存中。包括FileInfo、Bloom filter block、data block index和meta block index;

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

图 2 hfileV2

Scanned block section: 即存储数据block部分

Non-scanned block section:元数据block部分,主要存放meta信息,即BloomFilter信息。

Load-on-open-section:这部分数据在RegionServer启动时,实例化Region并创建HStore时会将所有StoreFile的Load-on-open-section加载进内存,主要存放了Root Data Index,meta Index,File Info及BooleamFilter的metadata等。除了Fields for midkey外,每部分都是一个HFileBlock.下面会详细去讲这块。

Trailer:文件尾,主要记录version版本,不同的版本Trailer的字段不一样,及Trailer的字段相关信息。

RegionServer托管着0...n个Region,Region管理着一个或多个HStore,其中HStore就管理着一个MemStore及多个StoreFile.

所在RegionServer启动时,会扫描所有StoreFile,加载StoreFile的相关信息到内存,而这部分内容就是Load-on-open-section,主要包括 Root数据索引,miidKyes(optional),Meta索引,File Info,及BloomFilter metadata等。

数据索引:数据索引是分层的,可以1-3层,其中第一层,即Root level Data Index,这部分数据是处放在内存区的。一开始,文件比较小,只有single-level,rootIndex直接定位到了DataBlock。当StoreFile变大时,rootIndex越来越大,随之所耗内存增大,会以多层结构存储数据索引.当采用multi-level方式,level=2时,使用root index和leaf index chunk,即内存区的rootIndex定位到的是 leafIndex,再由leafIndex定位到Datablock。当一个文件的datablock非常多,采用的是三级索引,即rootIndex定位到intermediate index,再由intermediate index定位到leaf index,最后定位到data block.可以看看上面图1所示,各个level的index都是分布在不同的区域的。但每部分index是以HFileBlock格式存放的,后面会比较详细地讲HFileBlock,说白了,就是HFile中的一个块。

Fileds for midKey:这部分数据是Optional的,保存了一些midKey信息,可以快速地定位到midKey,常常在HFileSplit的时候非常有用。

MetaIndex:即meta的索引数据,和data index类似,但是meta存放的是BloomFilter的信息,关于BloomFilter由于篇幅就不深入讨论了.

FileInfo:保存了一些文件的信息,如lastKey,avgKeylen,avgValueLen等等,一会我们将会写程序将这部分内容解析出来并打印看看是什么东西。同样,FileInfo使用了Protobuf来进行序列化。

Bloom filter metadata:分为GENERAL_BLOOM_META及DELETE_FAMILY_BLOOM_META二种。

FileV3版本基本和V2版本相比,并没有太大的改变,它在KeyValue(Cell)层面上添加了Tag数组的支持;并在FileInfo结构中添加了和Tag相关的两个字段。

我们主要对HFile V2版本进行介绍,这个过程中也会响应说明V1之所以被替换掉的部分原因。

三. HFile的物理结构解析:

如上图所示, HFile会被切分为多个大小相等的block块(注意并不是所有的部分都被抽象成了HFileBlock),每个block的大小可以在创建表列簇的时候通过参数blocksize => ‘65535’进行指定,默认为64k,大号的Block有利于顺序Scan,小号Block利于随机查询,因而需要权衡。

在类HFile中,我们可以看到对HFile的介绍:

同时在这个类中,我们可以得到HFile的存储路径为:ROOT_DIR/TABLE_NAME/REGION_NAME/CF_NAME/HFILE

FileInfo是HFile的一个静态内部类,实现可SortedMap接口。存储的是HFile的文件信息。

如下图左侧所示,HBase将block块抽象为一个统一的HFileBlock;从上图中可以看到多种block都抽象为了HFileBlock。HFileBlock支持两种类型,一种类型不支持checksum,一种支持(在HFileContext类中usesHBaseChecksum属性决定)。为方便讲解,下图选用不支持checksum的HFileBlock内部结构:

从类org.apache.hadoop.hbase.io.hfile.HFileBlock的类说明中,我们可以得到一个Block的结构如下:(上图只是一个简单的表示)

Header:header total size is HFILEBLOCK_HEADER_SIZE

  1. blockType:
  2. onDiskSizeWithoutHeader:
  3. uncompressedSizeWithoutHeader:
  4. prevBlockOffset:The offset of the previous block of the same type (8 bytes). This is used to navigate to the previous block without having to go to the block index。
  5. onDiskDataSizeWithHeader:the size of data 'on disk', including header, excluding checksums (4 bytes)

Raw/Compressed/Encrypted/Encoded data: The compression algorithm is the same for all the blocks in an HFile。If compression is NONE, this is just raw, serialized Cells.

类中其他主要属性:

  1. HFileContext fileContext 包含了这个hfileblock的元数据信息,比如采用的压缩算法,hfile的名字,创建时间,是否使用checksum等。
  2. long offset:The offset of this block in the file. Populated by the reader forconvenience of access. This offset is not part of the block header.
  3. ByteBuffer buf:(这个buf是主要存储数据的),这个buf里面存储的数据应该是包含了所有的数据(header+data+checksum),通过offset来读取。在方法allocateBuffer,这个buf的size为:headerSize + uncompressedSizeWithoutHeader + checksumBytes。而且该类几乎所有的方法操作这个buf都是在其副本上进行操作。

HFileBlock主要包括两部分:BlockHeaderBlockData。其中BlockHeader主要存储block元数据,BlockData用来存储具体数据。block元数据中最核心的字段是BlockType字段,用来标示该block块的类型,HBase中定义了多种BlockType,每种BlockType对应的block都存储不同的数据内容,有的存储用户数据,有的存储索引数据,有的存储meta元数据。对于任意一种类型的HFileBlock,都拥有相同结构的BlockHeader,但是BlockData结构却不相同。整个HFileBlock的结构我们在上面已经给出了。

通过前面的介绍,我们已经知道了HFileBlock通过BlockType这一关键的属性来决定HFileBlock的类型。下面我们将就不同的type分别进行描述。

四. 各种HFileBlock的解析

下面通过一张表简单罗列最核心的几种BlockType

Trailer Block

主要记录了HFile的基本信息、各个部分的偏移值和寻址信息,下图为Trailer内存和磁盘中的数据结构,其中只显示了部分核心字段:

File在读取的时候首先会解析Trailer Block并加载到内存,然后再进一步加载LoadOnOpen区的数据,具体步骤如下:

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

  2. 根据Version信息获取trailer的长度(不同version的trailer长度不同),再根据trailer长度加载整个HFileTrailer Block

  3. 最后加载load-on-open部分到内存中,起始偏移地址是trailer中的LoadOnOpenDataOffset字段,load-on-open部分的结束偏移量为HFile长度减去Trailer长度,load-on-open部分主要包括索引树的根节点以及FileInfo两个重要模块,FileInfo是固定长度的块,它纪录了文件的一些Meta信息,例如:AVG_KEY_LEN, AVG_VALUE_LEN, LAST_KEY, COMPARATOR, MAX_SEQ_ID_KEY等;

在类FixedFileTrailer我们可以看到其对trailer的一个解释:

在这个类中我们可以看到它记录了各种Offset的值。以下是这个类的全部属性

Data Block:

DataBlock是HBase中数据存储的最小单元。DataBlock中主要存储用户的KeyValue数据(KeyValue后面一般会跟一个timestamp,图中未标出),而KeyValue结构是HBase存储的核心,每个数据都是以KeyValue结构在HBase中进行存储。KeyValue结构在内存和磁盘中可以表示为:

每个数据块中KeyValue之间是按Key升序排列的,这里的Key不等同于RowKey。如下:

< rowKey,列族名称,列名=>列值></br>

< zhh2009,用户基本信息,职业=>码农></br>

< zhh2009,用户基本信息,性别=>男></br>

每一行在HBase中对应一个KeyValue,“=>”左边的是KeyValue中的”Key”,”=>”右边对应KeyValue中的”Value”。

布隆过滤器相关块:Bloom Index Block 和 Bloom Block

BloomFilter对于HBase的随机读性能至关重要,对于get操作以及部分scan操作可以剔除掉不会用到的HFile文件,减少实际IO次数,提高随机读性能。这里不对布隆过滤器的原理做介绍(不是本文要点)。

Bloom Filter使用位数组来实现过滤,HBase中每个HFile都有对应的位数组,KeyValue在写入HFile时会先经过几个hash函数的映射,映射后将对应的数组位改为1,get请求进来之后再进行hash映射,如果在对应数组位上存在0,说明该get请求查询的数据不在该HFile中。

HFile中的位数组就是上述Bloom Block中存储的值,可以想象,一个HFile文件越大,里面存储的KeyValue值越多,位数组就会相应越大。一旦太大就不适合直接加载到内存了,因此HFile V2在设计上将位数组进行了拆分,拆成了多个独立的位数组(根据Key进行拆分,一部分连续的Key使用一个位数组) HFile V1 改变到V2的原因之一,V1是全部加载到内存中,极大地占用了内存。这样一个HFile中就会包含多个位数组,根据Key进行查询,首先会定位到具体的某个位数组,只需要加载此位数组到内存进行过滤即可,减少了内存开支。

在结构上每个位数组对应HFile中一个Bloom Block,为了方便根据Key定位具体需要加载哪个位数组,HFile V2又设计了对应的索引Bloom Index Block,对应的内存和逻辑结构图如下:

Bloom Index Block结构中totalByteSize表示位数组的bit数,numChunks表示Bloom Block的个数,hashCount表示hash函数的个数,hashType表示hash函数的类型,totalKeyCount表示bloom filter当前已经包含的key的数目,totalMaxKeys表示bloom filter当前最多包含的key的数目, Bloom Index Entry对应每一个bloom filter block的索引条目,作为索引分别指向’scanned block section’部分的Bloom Block,Bloom Block中就存储了对应的位数组。

Bloom Index Entry的结构见上图左边所示,BlockOffset表示对应Bloom Block在HFile中的偏移量,FirstKey表示对应BloomBlock的第一个Key。根据上文所说,一次get请求进来,首先会根据key在所有的索引条目中进行二分查找,查找到对应的Bloom Index Entry,就可以定位到该key对应的位数组,加载到内存进行过滤判断。也就是说加载到内存中的并不是一个HFile中全部的位数组,减少了内存的占用量。

在trailer中并没有布隆过滤器的偏移,如何读到Bloom Index Block的?在trailer中有一个loadOnOpen区,即这个HFile一开始就会加载到内存的区域,对应在HfileReader中维持了一个List<HFileBlock> loadOnOpenBlocks = new ArrayList<HFileBlock>(); 而这个HFileBlock的list中有布隆过滤器的meta 数据信息。

到此,HFile的逻辑结构和物理存储结构基本阐述完毕,并且将HFile从逻辑上分解为各种类型的Block,再接着从微观的视角分别对Trailer Block、Data Block在结构上进行了解析:通过对trailer block的解析,可以获取hfile的版本以及hfile中其他几个部分的偏移量,在读取的时候可以直接通过偏移量对其进行加载;而对data block的解析可以知道用户数据在hdfs中是如何实际存储的;最后通过介绍Bloom Filter的相关的Block块了解HFile中Bloom Filter的存储结构。接下来会以上文为基础,分析HFile中索引块的结构以及相应的索引机制。

五. HFile索引块及索引机制

前面我们简单地提到了Bloom Index Block,下面我们主要说明一下HFile中Data Block的索引。

HFile中索引结构根据索引层级的不同分为两种:single-level和mutil-level,前者表示单层索引,后者表示多级索引,一般为两级或三级。HFile V1版本中只有single-level一种索引结构,V2版本中引入多级索引。之所以引入多级索引,是因为随着HFile文件越来越大,Data Block越来越多,索引数据也越来越大,已经无法全部加载到内存中(V1版本中一个Region Server的索引数据加载到内存会占用几乎6G空间),多级索引可以只加载部分索引,降低内存使用空间。HFile V1 改变到V2的另一原因,上文我们提到Bloom Filter内存使用问题是促使V1版本升级到V2版本的一个原因,再加上这个原因,这两个原因就是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表示叶子节点,叶子节点直接指向实际数据块。

HFile中除了Data Block需要索引之外,上文提到过Bloom Block也需要索引,索引结构实际上就是采用了single-level结构,文中Bloom Index Block就是一种Root Index Block。

对于Data Block,由于HFile刚开始数据量较小,索引采用single-level结构,只有Root Index一层索引,直接指向数据块。当数据量慢慢变大,Root Index Block满了之后,索引就会变为mutil-level结构,由一层索引变为两层,根节点指向叶子节点,叶子节点指向实际数据块。如果数据量再变大,索引层级就会变为三层。

下面就针对Root index Block和NonRoot index Block两种结构进行解析,因为Root Index Block已经在上文分析过,此处简单带过,重点介绍NonRoot Index Block结构(InterMediate Index Block和Ieaf Index Block在内存和磁盘中存储格式相同,都为NonRoot Index Block格式)。

Root Index Block

Root Index Block表示索引树根节点索引块,可以作为bloom的直接索引,也可以作为data索引的根索引。而且对于single-level和mutil-level两种索引结构对应的Root Index Block略有不同,本文以mutil-level索引结构为例进行分析(single-level索引结构是mutual-level的一种简化场景),在内存和磁盘中的格式如下图所示:

其中Index Entry表示具体的索引对象,每个索引对象由3个字段组成,Block Offset表示索引指向数据块的偏移量,BlockDataSize表示索引指向数据块在磁盘上的大小,BlockKey表示索引指向数据块中的第一个key。除此之外,还有另外3个字段用来记录MidKey的相关信息,MidKey表示HFile所有Data Block中中间的一个Data Block,用于在对HFile进行split操作时,快速定位HFile的中间位置(HFile过大会进行Split)。需要注意的是single-level索引结构和mutil-level结构相比,就只缺少MidKey这三个字段。

Root Index Block会在HFile解析的时候直接加载到内存中,也就是我们前面提到的List<HFileBlock> loadOnOpenBlocks = new ArrayList<HFileBlock>();中,此处需要注意在Trailer Block中有一个字段为dataIndexCount,就表示此处Index Entry的个数。因为Index Entry并不定长,只有知道Entry的个数才能正确的将所有Index Entry加载到内存。

NonRoot Index Block

当HFile中Data Block越来越多,single-level结构的索引已经不足以支撑所有数据都加载到内存,需要分化为mutil-level结构。mutil-level结构中NonRoot Index Block作为中间层节点或者叶子节点存在,无论是中间节点还是叶子节点,其都拥有相同的结构,如下图所示:

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

类HFileBlockIndex分析:该类应该是代表了RootIndex

类中说明,root level index会加载到内存中,同时root-level index 也应该有一个entries个数numEntries,在Root Index Block的图示中并没有给出。

类中有一个静态类BlockIndexReader,其中有一个searchTreeLevel属性,The number of levels in the block index tree. One if there is only root level, two for root and leaf levels, etc。

在BlockIndexReader中,还有以下属性:

其中blockKey应该是各个entry起始的key(First keys of the key range corresponding to each index entry)。BlockKeys,blockOffsets,blockDataSize三者是一一对应的。

在BlockIndexReader中,seekToDataBlock方法用来定位一个给定的key所在的DataBlock。它调用了loadDataBlockWithScanInfo方法,它首先在blockKeys中进行二分查找,得到entry的位置。得到位置之后就可以从blockOffsets中得到这个entry对应的偏移量,从而定位到对应的block。但是这个block可能是中间节点的block(多层索引的时候),也有可能直接就是叶子节点即数据的dataBlock。得到这个block之后,会和当前的block(该方法的参数接受一个HFileBlock)进行比较,避免避免重复读相同的block。得到这个block时,当前的遍历层级lookupLevel为1,根据lookupLevel与searchTreeLevel比较决定得到的这个block具体是什么block。如下:

最后返回的是HFileBlock。

HFile数据完整索引流程图示:

了解了HFile中数据索引块的两种结构之后,就来看看如何使用这些索引数据块进行数据的高效检索。整个索引体系类似于MySQL的B+树结构,但是又有所不同,比B+树简单,并没有复杂的分裂操作。具体见下图所示:

图中上面三层为索引层,在数据量不大的时候只有最上面一层,数据量大了之后开始分裂为多层,最多三层,如图所示。最下面一层为数据层,存储用户的实际keyvalue数据。这个索引树结构类似于InnoSQL的聚集索引,只是HBase并没有辅助索引的概念。

图中红线表示一次查询的索引过程(HBase中相关类为HFileBlockIndex和HFileReaderV2),基本流程可以表示为:

1. 用户输入rowkey为fb,在root index block中通过二分查找定位到fb在’a’和’m’之间,因此需要访问索引’a’指向的中间节点。因为root index block常驻内存,所以这个过程很快。

2. 将索引’a’指向的中间节点索引块加载到内存,然后通过二分查找定位到fb在index ‘d’和’h’之间,接下来访问索引’d’指向的叶子节点。

3. 同理,将索引’d’指向的中间节点索引块加载到内存,一样通过二分查找定位找到fb在index ‘f’和’g’之间,最后需要访问索引’f’指向的数据块节点。

4. 将索引’f’指向的数据块加载到内存,通过遍历的方式找到对应的keyvalue。

上述流程中因为中间节点、叶子节点和数据块都需要加载到内存,所以io次数正常为3次。但是实际上HBase为block提供了缓存机制,可以将频繁使用的block缓存在内存中,可以进一步加快实际读取过程。所以,在HBase中,通常一次随机读请求最多会产生3次io,如果数据量小(只有一层索引),数据已经缓存到了内存,就不会产生io。

索引块的分裂就不再叙述了。

总结

整个HFile的文章介绍基本到这里,全文对HFile的结构进行了一个梳理,了解之后相信对HFile的底层储存方式有了一个深入的认识,希望对于操作HBase的人来说有一点帮助。

】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇Hbase   MapReduce例子 下一篇大数据和云计算技术周报(第88期)

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目