HBase之HFile解析


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的方方面面,像拆零件一樣,把它一件一件地拆開看看,究竟是什么東西,怎么組織在一起的。

                                             圖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);
    }

輸出結果:

至此,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的組織會有些小差異.

      

                                    圖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

    開始解析Root Data Index和metaIndex .在Trailer解析后,我們可以得到Load-on-open-section內容的相關信息,可以構造字節數組,將這部分字節碼load進內存進行解析,在解析之前先講下FileInfo
    FileInfo的內容是以ProtoBuf放式存放的,與Trailer類似,我們先創建FileInfo.proto

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

message BytesBytesPair {
  required bytes first = 1;
  required bytes second = 2;
}

message FileInfoProto {
    repeated BytesBytesPair map_entry = 1;
}

編譯: protoc FileInfo.proto --java_out=.

編寫測試類:

 ....
FileTrailerProto hfileProtos = FileTrailerProto.PARSER.parseDelimitedFrom(new ByteArrayInputStream(trailerBytes, 8,trailerBytes.length - 4));
long loadOnOpenLength = length - trailerSize - hfileProtos.getLoadOnOpenDataOffset();
MyHFileBlockIterator inter = new MyHFileBlockIterator(input,hfileProtos.getLoadOnOpenDataOffset(), (int) loadOnOpenLength);
//解析出來root data index
MyHFileBlock dataIndex = inter.nextBlock();
int dataIndexLevels = hfileProtos.getNumDataIndexLevels();
int dataIndexEntries = hfileProtos.getDataIndexCount();
//創建root data index reader
MyHFileBlock.BlockIndexReader rootDataReader = dataIndex.createBlockIndexReader().parseMultiLevel(dataIndexEntries,"IDXROOT2", dataIndexLevels);
//解析出來root meta index
MyHFileBlock metaIndex = inter.nextBlock();
.....
//獲取file info
MyHFileBlock fileInfo = inter.nextBlock();
//解析讀取FileInfo內容
ByteArrayInputStream in = new ByteArrayInputStream(fileInfo.getBlockBuf().array());
int pblen = ProtobufUtil.lengthOfPBMagic();
byte[] pbuf = new byte[pblen];
if (in.markSupported())
    in.mark(pblen);
int read = in.read(pbuf);
FileInfoProtos.FileInfoProto fileInfoProto = FileInfoProtos.FileInfoProto.parseDelimitedFrom(in);

List<BytesBytesPair> entries = fileInfoProto.getMapEntryList();

for (BytesBytesPair entry : entries) {
    System.out.println(entry.getFirst().toStringUtf8() + ":"+ entry.getSecond().toStringUtf8());
}
//剩下的BloomFileter metadata block.
while (inter.hasNext()) {
    MyHFileBlock block = inter.nextBlock();
    System.out.println(block.getHeader());
}

    以上就是解析HFile Load-on-open-section部分的各個fileblock內容,完整代碼請下載附帶的地址。

    Scanned block section: 關於bloomfilter先不分析了。

     Non-scanned block section:

    這部分內容就是真正的數據塊,從圖1看出,這部分數據是分datablock存儲的,默認地,每個datablock占64K,如果是多層的index的話,部分index block也會存放在這里,由於我的測試數據,是single-level的,所以只針對單級的index分析。
的single-level情況下,內存的rootDataIndex記錄了每個datablock的偏移量,大小及startKey信息,主要是為了快速地定位到KeyValue的位置,在HFile中查找或者seek到某個KeyValue時,首先會在內存中,對rootDataIndex進行二分查找,單級的index可以直接定位DataBlock,然后通過迭代datablock定位到KeyValue所在的位置,而2-3層時,上面也略有提及,大家有時間的話,可以做多點研究這部分。
    弱弱提句:在HStore中,會有cache將這些datablock緩存起來,使用LRU算法,這樣會提高不少性能。

    每個DataBlock同樣也是一個HFileBlock,也包括header,data,checksum信息,可以用我們之前寫的BlockIterator就可以搞定。下面使用代碼,去遍歷一個datablock看看。
實驗3:

    編寫KeyValue遍歷器

public class KeyValueIterator {
    public static final int KEY_LENGTH_SIZE = 4;
    public static final int VALUE_LENGTH_SIZE = 4;
    
    private byte [] data ;
    private int currentOffset ;
    
    public KeyValueIterator(byte [] data) {
        this.data = data;
        currentOffset = 0;
    }
    
    public KeyValue nextKeyValue(){
        KeyValue kv = null;
        int keyLen = Bytes.toInt(data,currentOffset,4);
        incrementOffset(KEY_LENGTH_SIZE);
        
        int valueLen = Bytes.toInt(data,currentOffset,4);
        incrementOffset(VALUE_LENGTH_SIZE);

        //1 is memTS
        incrementOffset(keyLen,valueLen,1);
        
        int kvSize = KEY_LENGTH_SIZE + VALUE_LENGTH_SIZE +  keyLen + valueLen ;

        kv = new KeyValue(data , currentOffset - kvSize - 1, kvSize);
        return kv;
    }
    public void incrementOffset(int ... lengths) {
        for(int length : lengths)
            currentOffset = currentOffset + length;
    }
    
    public boolean hasNext() {
        return currentOffset < data.length;
    }
}

     編寫測試代碼:

   

//從rootDataReader中獲取第一塊的offset及數據大小
long offset = rootDataReader.getBlockOffsets()[0];
int size = rootDataReader.getBlockDataSizes()[0];

byte[] dataBlockArray = new byte[size];
input.seek(offset);
input.read(dataBlockArray);
//圖方便,直接用iterator來解析出來FileBlock
MyHFileBlockIterator dataBlockIter = new MyHFileBlockIterator(dataBlockArray);
MyHFileBlock dataBlock1 = dataBlockIter.nextBlock();
//將data內容給一個keyvalue迭代器
KeyValueIterator kvIter = new KeyValueIterator(dataBlock1.getBlockBuf().array());
while (kvIter.hasNext()) {
    KeyValue kv = kvIter.nextKeyValue();
    //do some with keyvalue. like print the kv.
    System.out.println(kv);
}


    OK,基本上是這些內容了。有點抱歉一開篇講得有點大了,其實沒有方方面面都講得很詳細。meta,bloomfilter部分沒有詳細分析,大家有時間可以研究后,分享一下。

    源碼我將我測試的Hfile也附帶上傳了,壓縮后有3M多,完整代碼請下載:下載源碼

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM