首页 > 代码库 > HFile存储格式

HFile存储格式


Table of Contents

HFile存储格式
Block块结构

HFile存储格式

HFile是參照谷歌的SSTable存储格式进行设计的。全部的数据记录都是通过它来完毕持久化,其内部主要採用分块的方式进行存储,如图所看到的:

技术分享

每一个HFile内部包括多种不同类型的块结构,这些块结构从逻辑上来讲可归并为两类。分别用于数据存储和数据索引(简称数据块和索引块),当中数据块包括:

(1) DATA_BLOCK:存储表格数据

(2) BLOOM_CHUNK:存储布隆过滤器的位数组信息

(3) META_BLOCK:存储元数据信息

(4) FILE_INFO:存储HFile文件信息

索引块包括:

  • 表格数据索引块(ROOT_INDEX、INTERMEDIATE_INDEX、LEAF_INDEX)

    在早期的HFile版本号中(version-1),表格数据是採用单层索引结构进行存储的。这样当数据量上升到一定规模时,索引数据便会消耗大量内存,导致的结果是Region载入效率低下(A region is not considered opened until all of its block index data is loaded)。

    因此在version-2版本号中。索引数据採用多层结构进行存储,载入HFile时仅仅将根索引(ROOT_INDEX)数据载入内存,中间索引(INTERMEDIATE_INDEX)和叶子索引(LEAF_INDEX)在读取数据时按需载入,从而提高了Region的载入效率。

  • 元数据索引块(META_INDEX)

    新版本号的元数据索引依旧是单层结构,通过它来获取元数据块信息。

  • 布隆索引信息块(BLOOM_META)

    通过索引信息来遍历要检索的数据记录是通过哪一个BLOOM_CHUNK进行映射处理的。

从存储的角度来看,这些数据块会划分到不同的区域进行存储。

  1. Trailer区域

    该区域位于文件的最底部。HFile主要通过它来实现相关数据的定位功能,因此须要最先载入,其数据内容是採用protobuf进行序列化处理的,protocol声明例如以下:

    message FileTrailerProto {
        optional uint64 file_info_offset = 1; 技术分享
        optional uint64 load_on_open_data_offset = 2; 技术分享
        optional uint64 uncompressed_data_index_size = 3; 技术分享
        optional uint64 total_uncompressed_bytes = 4; 技术分享
        optional uint32 data_index_count = 5; 技术分享
        optional uint32 meta_index_count = 6; 技术分享
        optional uint64 entry_count = 7; 技术分享
        optional uint32 num_data_index_levels = 8; 技术分享
        optional uint64 first_data_block_offset = 9; 技术分享
        optional uint64 last_data_block_offset = 10; 技术分享
        optional string comparator_class_name = 11; 
        optional uint32 compression_codec = 12;
        optional bytes encryption_key = 13;
    }
    				

    技术分享

    FileInfo数据块在HFile中的偏移量信息;

    技术分享

    Load-on-open区域在HFile中的偏移量信息;

    技术分享

    全部表格索引块在压缩前的总大小;

    技术分享

    全部表格数据块在压缩前的总大小;

    技术分享

    根索引块中包括的索引实体个数;

    技术分享

    元数据索引块中包括的索引实体个数;

    技术分享

    文件所包括的KeyValue总数;

    技术分享

    表格数据的索引层级数。

    技术分享

    第一个表格数据块在HFile中的偏移量信息;

    技术分享

    最后一个表格数据块在HFile中的偏移量信息;

    在代码层面上Trailer是通过FixedFileTrailer类来封装的,可通过其readFromStream方法用来读取指定HFile的Trailer信息。

  2. Load-on-open区域

    HFile被载入之后。位于该区域中的数据将会被载入内存,该区域的起始位置通过Trailer来定位(通过其load_on_open_data_offset属性)。从该位置起依次保存的数据信息为:根索引快、元数据索引块、文件信息块以及布隆索引块。

  3. Scanned-Block区域

    在运行HFile顺序扫描时,位于该区域中的全部块信息都须要被载入,包括:表格数据块、布隆数据块和叶子索引块(后两者称之为InlineBlock)。

  4. Non-Scanned-Block区域

    在运行HFile顺序扫描时,位于该区域中的存储块可不被载入。包括:元数据块和中间索引块。

Block块结构

每一个Block块是由3部分信息组成的。各自是:header信息、data信息以及用于data校验的checksum信息。

不同类型的block仅仅是在data信息的存储结构上存在差异,而header信息和checksum信息存储结构基本一致。

  1. header主要用于存储每一个Block块的元数据信息

    这些信息包括:

    (1)blockType:块类型。HFile一共对外声明了10种不同类型的Block。各自是:DATA(表格数据块)、META(元数据块)、BLOOM_CHUNK(布隆数据块)、FILE_INFO(文件信息块)、TRAILER、LEAF_INDEX(叶子索引块)、INTERMEDIATE_INDEX(中间索引块)、ROOT_INDEX(根索引快)、BLOOM_META(布隆索引块)、和META_INDEX(元数据索引块)。

    (2)onDiskSizeWithoutHeader:data信息与checksum信息所占用的磁盘空间大小;

    (3)onDiskDataSizeWithHeader:data信息与header信息所占用的磁盘空间大小。

    (4)uncompressedSizeWithoutHeader:每一个block块在完毕解压缩之后的大小(不包括header和checksum占用的空间);

    (5)prevBlockOffset:距离上一个同类型block块的存储偏移量大小。

    在v2版本号中,header的长度为固定的33字节。

  2. data主要用于封装每一个block块的核心数据内容

    • 假设是根索引块其数据内容例如以下:

      技术分享

      主要包括多条索引实体信息(索引实体的个数记录在Trailer中)以及midKey相关信息。当中每条索引实体信息是由3部分数据组成的。分别为:

      (1)Offset:索引指向的Block块在文件里的偏移量位置;

      (2)DataSize:索引指向的Block块所占用的磁盘空间大小(在HFile中的长度);

      (3)Key:假设索引指向的是表格数据块(DATA_BLOCK)。该值为目标数据块中第一条数据记录的rowkey值(0.95版本号之前是这种,之后的版本号參考HBASE-7845);假设索引指向的是其它索引块,该值为目标索引块中第一条索引实体的blockKey值。

      而midKey信息主要用于定位HFile的中间位置,以便于对该HFile运行split拆分处理,其数据内容相同由3部分信息组成,分别为:

      (1)midLeafBlockOffset:midKey所属叶子索引块在HFile中的偏移量位置。

      (2)midLeafBlockOnDiskSize:midKey所属叶子索引块的大小(在HFile中的长度)。

      (3)midKeyEntry:midKey在其所属索引块中的偏移量位置。

    • 假设是非根索引块其数据内容例如以下:

      技术分享

      相同包括多条索引实体信息。但不包括midKey信息。除此之外还包括了索引实体的数量信息以及每条索引实体相对于首个索引实体的偏移量位置。

    • 假设是表格数据块其数据内容为多条KeyValue记录,每条KeyValue的存储结构可參考Memstore组件实现章节。

    • 假设是元数据索引块其数据内容同叶子索引块相似。仅仅只是索引实体引向的是META数据块。

    • 假设是布隆数据块其数据内容为布隆过滤器的位数组信息。

    • 假设是布隆索引块其数据内容例如以下:

      技术分享

      同其它索引块相似,包括多条索引实体信息,每条索引实体引向布隆数据块(BLOOM_CHUNK)。除此之外还包括与布隆过滤器相关的元数据信息。包括:

      (1)version:布隆过滤器版本号,在新版本号HBase中布隆过滤器通过CompoundBloomFilter类来实现,其相应的版本号号为3。

      (2)totalByteSize:全部布隆数据块占用的磁盘空间总大小;

      (3)hashCount:元素映射过程中所使用的hash函数个数。

      (4)hashType:元素映射过程中所採用的hash函数类型(通过hbase.hash.type属性进行声明)。

      (5)totalKeyCount:全部布隆数据块中已映射的元素数量;

      (6)totalMaxKeys:在满足指定误报率的情况下(默觉得百分之中的一个),全部布隆数据块可以映射的元素总量。

      (7)numChunks:眼下已有布隆数据块的数量;

      (8)comparator:所映射元素的排序比較类,默觉得org.apache.hadoop.hbase.KeyValue.RawBytesComparator

    • 假设是文件信息块其数据内容採用protobuf进行序列化,相关protocol声明例如以下:

      message FileInfoProto {
          repeated BytesBytesPair map_entry = 1; // Map of name/values
      }
      						
  3. checksum信息用于校验data数据是否正确

块信息读取

数据块的读取操作主要是通过FSReader类的readBlockData方法来实现的。在运行数据读取操作之前。须要首先知道目标数据块在HFile中的偏移量位置。有一些数据块的偏移量信息是可通过Trailer进行定位的。如:

  • 根索引块(ROOT_INDEX)的偏移量信息可通过Trailer的load_on_open_data_offset属性来定位,在知道了根索引块的存储信息之后,便可通过它来定位全部DATA_BLOCK在HFile中的偏移量位置;

  • 首个DATA_BLOCK的偏移量信息可通过Trailer的first_data_block_offset属性来定位。

在不知道目标数据块大小的情况下须要对HFile运行两次查询才干读取到终于想要的HFileBlock数据。第一次查询主要是为了读取目标Block的header信息,由于header具有固定的长度(HFileV2版本号为33字节)。因此在知道目标Block的偏移量之后,便可通过读取指定长度的数据来将header获取。

获取到header之后便可通过其onDiskSizeWithoutHeader属性来得知目标数据块的总大小。

totalSize = headerSize + onDiskSizeWithoutHeader

然后再次从Block的偏移量处读取长度为totalSize字节的数据。以此来构造完整的HFileBlock实体。

由以上逻辑来看,假设在读取数据块之前。可以事先知道该数据块的大小。那么便可省去header的查询过程。从而有效减少IO次数。

为此,HBase採用的做法是在读取指定Block数据的同一时候。将下一个Block的header也一并读取出来(通过读取totalSize + headerSize长度的数据),并通过ThreadLocal将该header进行缓存。

这样假设当前线程所訪问的数据是通过两个连续的Block进行存储的,那么针对第二个Block的訪问仅仅需运行一次IO就可以。

获取到HFileBlock实体之后,可通过其getByteStream方法来获取内部数据的输入流信息,在依据不同的块类型来选择相应的API进行信息读取:

(1)假设block为根索引块。其信息内容可通过BlockIndexReader进行读取,通过其readMultiLevelIndexRoot方法;

(2)假设为元数据索引块。相同採用BlockIndexReader进行读取,通过其readRootIndex方法;

(3)假设为非根索引块,可通过BlockIndexReader的locateNonRootIndexEntry方法来将数据指针定位到目标block的索引位置上。从而对目标block的偏移量、大小进行读取;

(4)假设为文件信息块,通过FileInfo类的read方法进行读取。

(5)假设为布隆索引块,通过HFile.Reader实体的getGeneralBloomFilterMetadata方法进行读取。

(6)假设为布隆数据块,通过该HFileBlock实体的getBufferWithoutHeader方法来获取布隆数据块的位数组信息(參考CompoundBloomFilter类的实现)。

块数据生成

Block数据在写入HFile之前是暂存于内存中的。通过字节数组进行存储,当其数据量大小达到指定阀值之后,在開始向HFile进行写入。

写入成功后,须要再次开启一个全新的Block来接收新的数据记录。该逻辑通过HFileBlock.Writer类的startWriting方法来封装。方法运行后。会首先开启ByteArrayOutputStream输出流实例,然后在将其包装成DataOutputStream对象,用于向目标字节数组写入要加入的Block实体信息。

在HFile.Writer内部。不同类型的数据块是通过不同的Writer进行写入的,其内部封装了3种不同类型的子Writer(这些Writer共用一个FSDataOutputStream用于向HFile写入Block数据),分别例如以下:

  • HFileBlock.Writer

    通过该Writer完毕表格数据块(DataBlock)向HFile的写入逻辑。大致流程例如以下:

    每当运行HFile.Writer类的append方法进行加入数据时。会检測当前DataBlock的大小是否已经超过目标阀值。假设没有,直接将数据写入DataBlock,否则须要进行例如以下处理:

    1. 将当前DataBlock持久化写入HFile

      写入之前须要首先生成目标数据块的header和checksum信息。当中checksum信息可通过ChecksumUtil的generateChecksums方法进行获取。而header信息可通过putHeader方法来生成。

    2. 生成当前DataBlock的索引信息

      索引信息是由索引key。数据块在HFile中的偏移量位置和数据块的总大小3部分信息组成的,当中索引key可通过CellComparator.getMidpoint方法进行获取。方法会试图返回一条数据记录可以满足例如以下约束条件:

      (1)索引key在排序上大于上一个DataBlock的最后一条记录。

      (2)索引key在排序上小于当前DataBlock的第一条记录;

      (3)索引key的size是最小的。

      经过这样处理之后可以总体减少索引块的数据量大小,从而节省了内存空间的使用,并提高了载入效率。

    3. 将索引信息写入索引块

      通过HFileBlockIndex.BlockIndexWriter的addEntry方法。

    4. 推断是否有必要将InlineBlock进行持久化

      InlineBlock包括叶子索引块和布隆数据块。它们的持久化逻辑分别通过BlockIndexWriter和CompoundBloomFilterWriter来完毕。

    5. 开启新的DataBlock进行数据写入,同一时候将老的数据块退役

      假设集群开启了hbase.rs.cacheblocksonwrite配置,须要将老数据块缓存至BlockCache中。

  • HFileBlockIndex.BlockIndexWriter

    通过该Writer完毕索引数据块(IndexBlock)向HFile的写入逻辑。

    在HFile内部。索引数据是分层级进行存储的,包括根索引块、中间索引块和叶子索引块。

    当中叶子索引块又称之为InlineBlock,由于它会穿插在DataBlock之间进行存储。同DataBlock相似。IndexBlock一開始也是缓存在内存里的。每当DataBlock写入HFile之后。都会向当前叶子索引块加入一条索引实体信息。

    假设叶子索引块的大小超过hfile.index.block.max.size限制,便開始向HFile进行写入。写入格式为:索引实体个数、每条索引实体相对于块起始位置的偏移量信息。以及每条索引实体的具体信息(參考Block块结构)。

    这主要是叶子索引块的写入逻辑。而根索引块和中间索引块的写入则主要在HFile.Writer关闭的时候进行。通过BlockIndexWriter的writeIndexBlocks方法。

    在HFile内部,每一个索引块是通过BlockIndexChunk对象进行封装的,其对内声明了例如以下数据结构:

    (1)blockKeys,封装每一条索引所指向的Block中第一条记录的key值;

    (2)blockOffsets,封装每一条索引所指向的Block在HFile中的偏移量位置;

    (3)onDiskDataSizes,封装每一条索引所指向的Block在HFile中的长度。

    除此之外。根索引块还比較特殊。其对内声明了numSubEntriesAt集合,集合类型为List<Long>,每当有叶子索引块写入HFile之后都会向该集合加入一条实体信息,实体的index为当前叶子索引块的个数。value为索引实体总数。这样。通过numSubEntriesAt集合便能确定midKey(中间索引)处在哪个叶子索引块上,在通过blockKeys、blockOffsets和onDiskDataSizes便可以获取最后的midkey信息。

    然后将其作为根索引块的一部分写入HFile。并通过FixedFileTrailer来标记根索引块的写入位置。

    须要注意的是根索引块的大小也是受上限约束的,假设其大小大于hfile.index.block.max.size參数阀值(默觉得128kb),须要将其拆分成多个中间索引块,然后在对这些中间索引块创建根索引,以此来减少根索引块的大小,具体逻辑可參考BlockIndexWriter类的writeIntermediateLevel方法实现。

  • CompoundBloomFilterWriter

    通过该Writer完毕布隆数据向HFile的写入逻辑。

    布隆数据在HFile内部相同是分块进行存储的,每一个数据块通过ByteBloomFilter类来封装,负责存储指定区间的数据集映射信息(參考布隆过滤器实现章节)。

    每当运行HFile.Writer的append方法向DataBlock加入KeyValue数据之前。都要调用ByteBloomFilter的add方法来生成该KeyValue的布隆映射信息,为了满足目标容错率,每一个ByteBloomFilter实体可以映射的KeyValue数量是受上限约束的,假设达到目标上限值须要将其持久化到HFile中进行存储,然后开启新的ByteBloomFilter实例来接管之前的逻辑。

    每当布隆数据块写入成功之后。都会运行BlockIndexWriter的addEntry方法来创建一条布隆索引实体,实体的key值为布隆数据块所映射的第一条KeyValue的key值。

    同叶子索引块一样,布隆数据块也被称之为InlineBlock,在写入DataBlock的同一时候会对该类型的数据块进行穿插写入。这主要是布隆数据块的写入逻辑,而布隆索引块主要是在HFile.Writer关闭的时候进行创建的。通过CompoundBloomFilterWriter.MetaWriter的write方法。将布隆索引数据连同meta信息一同写入HFile。

HFile存储格式