Sumary:

Protobuf

BinarySearch


    本篇主要讲HFileV2的相关内容,包括HFile的构成、解析及怎么样从HFile中快速找到相关的KeyValue.基于Hbase 0.98.1-hadoop2,本文大部分参考了官方的资源,大家可以先阅读下这篇官方文档,Reference Guide:http://hbase.apache.org/book/apes03.html。其实也就是跟我们发行包内dos/book下的其中一篇。dos下有很多有用的文章,有时间的时候建议大家还是细读一下。

    研究HFile也有一些时间了,源码也大概研究了下,做了不少试验,庖丁解牛远远谈不上,但是还是很详细地分享一下HFile的方方面面,像拆零件一样,把它一件一件地拆开看看,究竟是什么东西,怎么组织在一起的。

HBase之HFile解析

                                             图1

    这张图也是摘自上面那篇文章,主要分四部分:Scanned block section,Non-scanned block section,Load-on-open-section,以及Trailer.
    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的字段相关信息。

    在拆解HFile过程中,我们从下而上地开始分析,HBase本身也是这样,首先要知道Version版本,才知道怎么去加载它们。在开始讲解之前,我们应先获得一份HFile数据,其实很简单,直接从hdfs上下载到本地即可,我使用的数据是我上一篇文章中做测试生成的,10W rows, 70W KeyValue,26M左右。

Trailer:

    文件最后4位,即一个整型数字,为version信息,我们知道是V2.而V2的Trailer长度为212字节。除去MagicCode(BlockType) 8字节及 Version 4字节外,剩余206字节记录了整个文件的一些重要的字段信息,而这些字段信息是由protobuf组成的,下面我们尝试山寨一把,自主解析下Trailer的所有信息。
    实践1:
    step1: 准备一份描述Trailer的Protobuf.
    Hbase的源码包下,有一个hbase-protocol sub module.它包含了HBase的所有Protobuf,包括序列化要用到的实体及RPC的定义。我们找到HFile.proto,我们只选取一小部分
    新建我们自已的Protobuf文件 : HFile.proto

option java_package = "com.bdifn.hbase.hfile.proto";
option java_outer_classname = "HFileProtos";
option java_generic_services = true;
option java_generate_equals_and_hash = true;
option optimize_for = SPEED;

message FileTrailerProto {
  optional uint64 file_info_offset = 1;   //fileInfo起始偏移量
  optional uint64 load_on_open_data_offset = 2; //加载到内存区域起始偏移量
  optional uint64 uncompressed_data_index_size = 3; //未压缩的数据索引大小
  optional uint64 total_uncompressed_bytes = 4;  //KeyValue未压缩的总大小
  optional uint32 data_index_count = 5;          //Root DataIndex 的个数,如果只有1级索引的话,往往也是datablock个数
  optional uint32 meta_index_count = 6;          //元数据索引个数
  optional uint64 entry_count = 7;               //KeyValue总个数
  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;            //加密相关
}

    从proto文件可以看出,Trailer主要记录了Load-on-open-section相关的信息,应该花点时间去做些结合和对比。

    step2:使用Protobuf命令生成java代码.(刚好我之前在hadoop环境中编译过源码,安装了protobuf)
    protoc HFile.proto --java_out=.
    将生成的java类拷到我们的项目中.

    step3. 编写java代码解析Trailer.

public static void main(String[] args) throws Exception {
        Configuration config = new Configuration();

        // 我已经将文件拷到了f盘根目录
        String pathStr = "file:///f:/0a99d83b2b0a49c0adbc371d4bfe021e";
        Path path = new Path(pathStr);
        FileSystem fs = FileSystem.get(URI.create(pathStr), config);

        FSDataInputStream input = fs.open(path);

        long length = input.available();
        int trailerSize = 212;

        input.seek(length - trailerSize);
        byte[] trailerBytes = new byte[trailerSize];
        input.read(trailerBytes);
        
        ByteBuffer trailerBuf = ByteBuffer.wrap(trailerBytes);
        trailerBuf.position(trailerSize - 4);
        
        int version = trailerBuf.getInt();
        //3, 0, 0, 2
        //最后三位是majorVersion
        int majorVersion = version & 0x00ffffff;
        //高位是 minorVersion
        int minorVersion = version >>> 24;
        
        String magicCode = Bytes.toString(Arrays.copyOfRange(trailerBytes, 0, 8));

        // 除去头8个字节MagicCode ,除去尾4个字节version信息。咱就是这么暴力。
        FileTrailerProto hfileProtos = FileTrailerProto.PARSER
                .parseDelimitedFrom(new ByteArrayInputStream(trailerBytes, 8,
                        trailerSize - 4));
        System.out.println(String.format("MagicCode:%s,majorVersion:%d,:minorVersion:%d",magicCode,majorVersion,minorVersion));
        System.out.println(hfileProtos);
    }

输出结果:

HBase之HFile解析

至此,Trailer已经完全解析完成,接下来开始下一部分:

Load-on-open-section:  

    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二种。

    OK,下面开始操刀分割下Load-on-open-section的各个小块,看看究竟有什么东西。在开始分析之前,上面提到了一个HFileBlock想先看看。从上面可以看出来,其实基本每个小块都叫HFileBlock(除field for midkey),在Hbase中有一个类叫HFileBlock与之对应。从V2开始,即我们当前用的HFile版本,HFileBlock是支持checksum的,默认地使用CRC32,由此HFileBlock由header,data,checksum三部分内容组成,如下图所示。其中Header占了33个字节,字段是一样的,而每个block的组织会有些小差异.

      HBase之HFile解析

                                    图2

    了解了HFileBlock的结构,我们下面开始正式解析内存区中的各个index的block内容。首先我们根据图2我们抽象出一个简单的HFileBlock实体。

    实验2: HFileBlock的解析.及BlockReader内部类

public class MyHFileBlock {
public static class Header {
        private String magicCode ;
        int onDiskSizeWithoutHeader;
        int unCompressBlockSize;
        long prevBlockOffset;
        byte checkSum;
        int bytesPerChecksum;
        int onDiskDataSizeWithHeader;
    }

private Header header;
private ByteBuffer blockBuf;
private byte [] checkSum ;

....
   public static class BlockIndexReader {
       public BlockIndexReader(MyHFileBlock block) {
        ....
      }
   public BlockIndexReader parseMultiLevel(int numEntries, String expectedMagicCode, int level) throws Exception {
     .....
   }
   .......
   }
}

2.编写HFileBlock遍历器,代码有点长,折叠起来吧,有兴趣可以看看,详细完整代码还是下载附件项目吧,

public class MyHFileBlockIterator {

    private ByteBuffer loadOnOpenBuffer;

    public MyHFileBlockIterator(FSDataInputStream data, long offset, int length) {

        try {
            data.seek(offset);
            byte[] loadOnOpenBytes = new byte[length];
            data.read(loadOnOpenBytes);
            loadOnOpenBuffer = ByteBuffer.wrap(loadOnOpenBytes);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public MyHFileBlockIterator(byte [] data) {
        loadOnOpenBuffer = ByteBuffer.wrap(data);
    }
    
    public MyHFileBlock nextBlock() {

        MyHFileBlock block = new MyHFileBlock(loadOnOpenBuffer);
        Header header = block.getHeader();
        int currentBlockLength = block.getHeader()
                .getOnDiskDataSizeWithHeader();

        int dataSize = currentBlockLength - MyHFileBlock.HARDER_SIZE;

        byte[] dataBlockArray = new byte[dataSize];

        loadOnOpenBuffer.get(dataBlockArray);

        ByteBuffer dataBlock = ByteBuffer.wrap(dataBlockArray);
     
        block.setBlockBuf(dataBlock);

        int checkSumChunks = header.getOnDiskSizeWithoutHeader()
                / header.getBytesPerChecksum();
        if (header.getOnDiskSizeWithoutHeader() % header.getBytesPerChecksum() != 0) {
            checkSumChunks++;
        }
        int checkSumBytes = checkSumChunks * 4;
        byte[] checkSum = new byte[checkSumBytes];

        loadOnOpenBuffer.get(checkSum);

        block.setCheckSum(checkSum);

        return block;
    }
    
    public boolean hasNext(){
        return loadOnOpenBuffer.position() < loadOnOpenBuffer.capacity();
    }
}
View Code

相关文章:

  • 2021-11-14
  • 2021-06-10
  • 2021-10-12
  • 2021-11-30
  • 2021-08-24
  • 2021-09-18
  • 2022-12-23
  • 2022-12-23
猜你喜欢
  • 2021-06-07
  • 2022-12-23
  • 2021-06-05
  • 2021-05-20
  • 2021-06-01
  • 2021-06-09
相关资源
相似解决方案