数据结构论坛

首页 » 分类 » 问答 » 图解HBaseHFile写入微观流程
TUhjnbcbe - 2024/4/28 16:42:00

HFile作为HBase底层存储文件,是业务数据真正持久化的体现形式,在HBase的应用场景中,即便发生查询落盘延迟也不会过大,其中的原因就在于HFile的结构设计上。今天带大家一起来看下HFile是如何一步步生成的,进而了解HFile的文件结构是如何设计的,明白了这些细节,我们就可以知道为什么HFile的查询也可以做到快速响应,这些思想其实推广开来我们也可以应用到其他存储场景中来。

为了能让大家更容易、更直观地理解这一过程,我们将源码中的逻辑逐一抽象为可视化的图解流程,帮助大家快速了解相关概念。

01写bloom过滤器信息

这个信息其实主要是对row或者row+col的一个散列后的位图,通过这个位图可以很快判断出给定条件的row或者row+col是否在对应的HFile中,从而提高随机读取的效率。如果设置了bloomFilter属性(这个开关可以通过配置io.storefile.bloom.enabled进行设置,默认为true),那么每个cell都会先写bloomFilter信息。

写入流程可以总结为如下图所示:

Bloom过滤器信息写入整体流程

对cell的bloom信息首先是被写到BloomFilterChunk中的,这个chunk相当于真正写入HDFS之前的一个缓存,主要包含了对cell的bloom信息,其默认大小为KB,可以通过配置io.storefile.bloom.block.size进行设定。

当chunk满了后会加入到一个readyChunks队列中,而不是直接就开始写HDFS,起到一个缓冲作用,这个chunk会在后续首先通过org.apache.hadoop.hbase.io.hfile.CompoundBloomFilterWriter.writeInlineBlock(DataOutput)将对应的bloomBlock以byteArray的形式写入HDFS,然后通过org.apache.hadoop.hbase.io.hfile.CompoundBloomFilterWriter.blockWritten(long,int,int)将该bloomBlock的索引信息写入到BloomMeta中(这个后续再进一步展开),包括firstKey/bolckoffset/onDiskSize。

02写cell数据到datablock

我们按照图中标志的每一步为大家展开讲解,请继续往下看。

datablock的初始构造核心过程

1)会生成一个数据块,用来容纳后续写入的数据内容,调用入口为:org.apache.hadoop.hbase.io.hfile.HFileWriterImpl.newBlock()。2)初始化后的datablock首先是通过ByteArrayOutputStream向block中写入一个header数组,header部分用来存放一个block的元数据信息,一开始的时候为空,只有当这个block写满准备关闭时才会再向这个预留header中写入信息。这个header其实就是一个字节数组,它的长度默认为8+2*4+8+1+2*4=33,其逻辑结构可以用如下图来标示:

header数组大小与功能划分

3)这一步开始写入kv数据,注意如果配置了DataBlockEncoding,则在appendkv时会进行同步编码,编码后的数据不再时单纯的kv模式,datablockencoding是为了降低kv结构数据膨胀而提供的内部编码机制,为了更方便大家理解,这里我们以直接写入kv进行分析。默认编码为DataBlockEncoding.NONE,即不采用任何编码压缩策略。4)这里开始将cellappend到已经就绪的datablock中,值得注意的是,在代码实现层面cell中的tags信息是被单独写的,可以从源码中上述看出,大体顺序其实是分为三部分:1)写key/value。2)写tags。3)写mvcc。其中第一部分写入顺序可用如下图表示:

Cell写入的结构示意图

第二部分的tags为什么要单独写而不是在写kv的时候一并写入呢?源码中的解释是,即使cell中没有tag,但是hfileContext中要求包含tags信息,这种情况下我们仍然需要将tagslength写入。原文如下:

WewritetagsseperatelybecausethoughthereisnotaginKVifthehfilecontextsaysincludetagsweneedthetagslengthtobewritten.

tags信息加上第三部分的mvcc信息,写入的组织结构可以用如下图来表示:

Cell中的tag与MVCC结构组成

到这里为止一个cell要写入的全部信息就处理完毕了。注意在append过程中,每append一个cell都会更新当前Store的最早、最晚时间戳,用于后续读优化。

03写datablock到HDFS

datablock达阈值写HDFS的主要处理过程

5)随着数据块中kv数据的不断写入,datablock的大小会不断增长,当增长到设定阈值大小时(默认64KB),对应block将停止写入,并经历上图所示过程。6)如果有配置启用压缩或者加密特性,将对应的datablock数据按照相应的算法进行压缩和加密。默认对数据不进行压缩和加密。支持的压缩策略有LZO/GZ/SNAPPY/LZ4。7)然后是在预留的Header区写入该datablock的元数据信息,包括{压缩前的大小、压缩后的大小、上一个block的偏移量、checksum元信息}等。一个完整的header信息结构如下图,其中magic部分存的是blocktype相关的信息。

header数组信息的完善

8)生成checksum信息,紧接着DataBlock以及Checksum信息通过HFileWriter中的输出流写入到HDFS中。为输出的datablock生成一条索引记录,这个索引记录包括了当前datablock的{起始key,偏移量,大小},生成的索引实例会被暂时记录到blockIndexchunk中,如下图所示:

生成datablock对应的索引信息

这个chunk的核心数据结构如下:

/**Firstkeysofthekeyrangecorrespondingtoeachindexentry.*/privatefinalListbyte[]blockKeys=newArrayListbyte[]();/**Blockoffsetinbackingstream.*/privatefinalListLongblockOffsets=newArrayListLong();/**On-diskdatasizesoflower-leveldataorindexblocks.*/privatefinalListIntegeronDiskDataSizes=newArrayListInteger();

这里有两点值得注意的是:

这里的firstKey并不一定是这个datablock的真正的第一个key,它是上一个datablock的最后一个key和当前datablock的第一个key之间的一个中间值。至此,第一个datablock已经写入hdfs,并且在blockIndexchunk中记录了这个block的索引信息。每写一个datablock,都会判断是否需要进行写缓存(shouldCacheDataOnWrite),以提升后续读性能,这个可以通过配置hbase.rs.cacheblocksonwrite进行设定,默认为false。如果为true则会写blockcache。

04写blockIndex到HDFS

block索引chunk达阈值后写hdfs前的处理

后续每生成一个datablock,都会在blockIndexChunk中生成一条对应的索引记录,当blockIndexChunk中的记录数量达到一定大小后(默认为KB,可以通过hfile.index.block.max.size进行指定),blockIndexChunk也会经与datablock一样的处理流程输出到HDFS,此时便生成了第一个leafindexblock。至此已经输出的ScanBlockSection部分的构成如下:

HFile中的ScanBlockSection结构

后续datablock和leafindexblock总是这样按顺序的、交叉的写入的scannedblocksection区,因此leafindexblock又被称为inlineblock。

在写完leafindexblock后,在内存中还会有一个RootIndexchunk来记录每个leafindexblock的索引信息。这个过程与上面leafindexblock记录datablock的索引信息过程是类似的,用到的是统一的处理方法,如下图:

构造leafindexblock的索引信息

到这里我们基本上梳理清楚了HFile内部的datablock的索引结构,从rootindex到leafindexblock再到datablock的索引关系可以用如下图开表示:

HFile的主要索引关系示意图

继续往下之前,先来总结一个问题:我们注意到,这里的rootindexchunk在add实例后并没有触发检查写HDFS的过程,而dataindexblock则是每次有新的datablockindex索引信息写入就会触发检查写HDFS,为什么呢?我们接着看后续的流程,聪明的你肯定会发现答案。

05处理HFile写的收尾工作

当memStore中所有的keyvalue全部写完之后(即performFlush动作完成后,HFile.Writer开始在close方法中处理当前HFile写的收尾工作,我们先不考虑bloomfilter相关的信息,则主要分为以下几步:

写最后一个datablock。写入最后一个leafindexblock。(到这里为止,scannedblocksection部分的收尾工作就完成了)。如果有metadata则写入位于Non-ScannedBlockSection区域的MetaBlocks,源码中看这部分事实上是为空的,很多相关的元信息实际上是被写入到FileInfo中。关于appendMetaBlock的注释原文如下:Addametablocktotheendofthefile.Callbeforeclose().Metadatablocksareexpensive.Fillonewithabunchofserializeddataratherthandoametadatablockpermetadatainstance.Ifmetadataissmall,consideraddingtofileinfousing{

link#appendFileInfo(byte[],byte[])}.写rootblockindexchunk部分:如果rootblockindexchunk大小超过了预设大小(默认的KB)则输出位于Non-ScannedBlockSection区域的intermediateindexblocks,这些blocks的索引信息又会被记录到rootblockindexchunk中。如果没有超出大小,则直接生成rootindexblock到Load-On-OpenSection区域。写入用来索引metablock的metaindex。(实际上这里只是写入一个空的block)。写入FileInfo信息,FileInfo中包含:最大的TimeStampKeyValue版本最后一个rowkey平均的rowkey长度HFile的create_time_ts平均的value长度MAX_TAGS_LEN(前缀树编码时或包含tags信息时写入该项)Tags是否被压缩存储(TAGS_COMPRESSED)写入BloomFilter元数据和索引信息,这里主要包括bloomfilterblock的索引信息。这里写入block的过程与前面的写block形式其实是相同的,都包含header和data部分,header结构也是相同的,只是有各自不同的blockType,不同类型的block的data部分可以有自己的定义。写入Trialer部分信息,Trailer信息依次包括:DataBlockIndex的层级DataBlockIndex数据总大小第一个DataBlock的Offset最后一个DataBlock的OffsetComparator类信息RootIndexBlock的Entries数量(howmanyblockindexentriesthereareintherootlevel)加密算法类型MetaIndexBlock的Entries数量整个HFile文件未压缩大小整个HFile中所包含的KeyValue总个数压缩算法类型FileInfo的offset至此,一个完整的HFile已经生成。

06总结与思考

我们可以通过下图来简单回顾一下rootindexblock、leafindexblock和datablock所处的位置以及索引关系:

HFile核心结构总结

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

BloomFilter包含Bloom元数据(Hash函数类型,Hash函数个数等)与位图数据(BloomData),为了避免每一次读取时加载所有的BloomData,HFileV2中将BloomData部分分成了多个小的BloomBlock。BloomData数据也被当成一类InlineBlock,与DataBlock、LeafIndexBlock交叉存在,而关于BloomFilter的元数据与多个BloomBlock的索引信息,被存放在Load-On-OpenSection部分。但需要注意的是,在FileInfo部分,保存了关于BloomFilter配置类型信息,共包含三种类型:不启用,基于Row构建BloomFilter,基于Row+Column构建BloomFilter。混合了BloomFilterBlock以后的HFile构成如下图所示:

HFile整体结构
1
查看完整版本: 图解HBaseHFile写入微观流程