HBase之HFile解析
Sumary:
Protobuf
BinarySearch
? ? 本篇主要講HFileV2的相關(guān)內(nèi)容,包括HFile的構(gòu)成、解析及怎么樣從HFile中快速找到相關(guān)的KeyValue.基于Hbase 0.98.1-hadoop2,本文大部分參考了官方的資源,大家可以先閱讀下這篇官方文檔,Reference Guide:http://hbase.apache.org/book/apes03.html。其實(shí)也就是跟我們發(fā)行包內(nèi)dos/book下的其中一篇。dos下有很多有用的文章,有時(shí)間的時(shí)候建議大家還是細(xì)讀一下。
? ? 研究HFile也有一些時(shí)間了,源碼也大概研究了下,做了不少試驗(yàn),庖丁解牛遠(yuǎn)遠(yuǎn)談不上,但是還是很詳細(xì)地分享一下HFile的方方面面,像拆零件一樣,把它一件一件地拆開(kāi)看看,究竟是什么東西,怎么組織在一起的。
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ???圖1
? ? 這張圖也是摘自上面那篇文章,主要分四部分:Scanned block section,Non-scanned block section,Load-on-open-section,以及Trailer.
? ? Scanned block section: 即存儲(chǔ)數(shù)據(jù)block部分
? ? Non-scanned block section:元數(shù)據(jù)block部分,主要存放meta信息,即BloomFilter信息。
? ? Load-on-open-section:這部分?jǐn)?shù)據(jù)在RegionServer啟動(dòng)時(shí),實(shí)例化Region并創(chuàng)建HStore時(shí)會(huì)將所有StoreFile的Load-on-open-section加載進(jìn)內(nèi)存,主要存放了Root Data Index,meta Index,File Info及BooleamFilter的metadata等。除了Fields for midkey外,每部分都是一個(gè)HFileBlock.下面會(huì)詳細(xì)去講這塊。
? ? Trailer:文件尾,主要記錄version版本,不同的版本Trailer的字段不一樣,及Trailer的字段相關(guān)信息。
? ? 在拆解HFile過(guò)程中,我們從下而上地開(kāi)始分析,HBase本身也是這樣,首先要知道Version版本,才知道怎么去加載它們。在開(kāi)始講解之前,我們應(yīng)先獲得一份HFile數(shù)據(jù),其實(shí)很簡(jiǎn)單,直接從hdfs上下載到本地即可,我使用的數(shù)據(jù)是我上一篇文章中做測(cè)試生成的,10W rows, 70W KeyValue,26M左右。
Trailer:
? ? 文件最后4位,即一個(gè)整型數(shù)字,為version信息,我們知道是V2.而V2的Trailer長(zhǎng)度為212字節(jié)。除去MagicCode(BlockType) 8字節(jié)及 Version 4字節(jié)外,剩余206字節(jié)記錄了整個(gè)文件的一些重要的字段信息,而這些字段信息是由protobuf組成的,下面我們嘗試山寨一把,自主解析下Trailer的所有信息。
? ? 實(shí)踐1:
? ? step1: 準(zhǔn)備一份描述Trailer的Protobuf.
? ? Hbase的源碼包下,有一個(gè)hbase-protocol sub module.它包含了HBase的所有Protobuf,包括序列化要用到的實(shí)體及RPC的定義。我們找到HFile.proto,我們只選取一小部分
? ? 新建我們自已的Protobuf文件 : HFile.proto
? ? 從proto文件可以看出,Trailer主要記錄了Load-on-open-section相關(guān)的信息,應(yīng)該花點(diǎn)時(shí)間去做些結(jié)合和對(duì)比。
? ? step2:使用Protobuf命令生成java代碼.(剛好我之前在hadoop環(huán)境中編譯過(guò)源碼,安裝了protobuf)
? ? protoc HFile.proto --java_out=.
? ? 將生成的java類(lèi)拷到我們的項(xiàng)目中.
? ? step3. 編寫(xiě)java代碼解析Trailer.
public static void main(String[] args) throws Exception {Configuration config = new Configuration();// 我已經(jīng)將文件拷到了f盤(pán)根目錄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//最后三位是majorVersionint majorVersion = version & 0x00ffffff;//高位是 minorVersionint minorVersion = version >>> 24;String magicCode = Bytes.toString(Arrays.copyOfRange(trailerBytes, 0, 8));// 除去頭8個(gè)字節(jié)MagicCode ,除去尾4個(gè)字節(jié)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);}輸出結(jié)果:
至此,Trailer已經(jīng)完全解析完成,接下來(lái)開(kāi)始下一部分:
Load-on-open-section:??
? ? RegionServer托管著0...n個(gè)Region,Region管理著一個(gè)或多個(gè)HStore,其中HStore就管理著一個(gè)MemStore及多個(gè)StoreFile.
? ? 所在RegionServer啟動(dòng)時(shí),會(huì)掃描所StoreFile,加載StoreFile的相關(guān)信息到內(nèi)存,而這部分內(nèi)容就是Load-on-open-section,主要包括 Root數(shù)據(jù)索引,miidKyes(optional),Meta索引,File Info,及BloomFilter metadata等.
? ? 數(shù)據(jù)索引:
? ? ? ? ? 數(shù)據(jù)索引是分層的,可以1-3層,其中第一層,即Root level Data Index,這部分?jǐn)?shù)據(jù)是處放在內(nèi)存區(qū)的。一開(kāi)始,文件比較小,只有single-level,rootIndex直接定位到了DataBlock。當(dāng)StoreFile變大時(shí),rootIndex越來(lái)越大,隨之所耗內(nèi)存增大,會(huì)以多層結(jié)構(gòu)存儲(chǔ)數(shù)據(jù)索引.當(dāng)采用multi-level方式,level=2時(shí),使用root index和leaf index chunk,即內(nèi)存區(qū)的rootIndex定位到的是 leafIndex,再由leafIndex定位到Datablock。當(dāng)一個(gè)文件的datablock非常多,采用的是三級(jí)索引,即rootIndex定位到intermediate index,再由intermediate index定位到leaf index,最后定位到data block.可以看看上面圖1所示,各個(gè)level的index都是分布在不同的區(qū)域的。但每部分index是以HFileBlock格式存放的,后面會(huì)比較詳細(xì)地講HFileBlock,說(shuō)白了,就是HFile中的一個(gè)塊。
? ? Fileds for midKey:
? ? ? ? ? 這部分?jǐn)?shù)據(jù)是Optional的,保存了一些midKey信息,可以快速地定位到midKey,常常在HFileSplit的時(shí)候非常有用。
? ? MetaIndex:
? ? ? ? ? ?即meta的索引數(shù)據(jù),和data index類(lèi)似,但是meta存放的是BloomFilter的信息,關(guān)于BloomFilter由于篇幅就不深入討論了.
? ? FileInfo:
? ? ? ? ? ? 保存了一些文件的信息,如lastKey,avgKeylen,avgValueLen等等,一會(huì)我們將會(huì)寫(xiě)程序?qū)⑦@部分內(nèi)容解析出來(lái)并打印看看是什么東西。同樣,FileInfo使用了Protobuf來(lái)進(jìn)行序列化。
? ? Bloom filter metadata:
? ? ? ? ? ? 分為GENERAL_BLOOM_META及DELETE_FAMILY_BLOOM_META二種。
? ? OK,下面開(kāi)始操刀分割下Load-on-open-section的各個(gè)小塊,看看究竟有什么東西。在開(kāi)始分析之前,上面提到了一個(gè)HFileBlock想先看看。從上面可以看出來(lái),其實(shí)基本每個(gè)小塊都叫HFileBlock(除field for midkey),在Hbase中有一個(gè)類(lèi)叫HFileBlock與之對(duì)應(yīng)。從V2開(kāi)始,即我們當(dāng)前用的HFile版本,HFileBlock是支持checksum的,默認(rèn)地使用CRC32,由此HFileBlock由header,data,checksum三部分內(nèi)容組成,如下圖所示。其中Header占了33個(gè)字節(jié),字段是一樣的,而每個(gè)block的組織會(huì)有些小差異.
? ? ??
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 圖2
? ? 了解了HFileBlock的結(jié)構(gòu),我們下面開(kāi)始正式解析內(nèi)存區(qū)中的各個(gè)index的block內(nèi)容。首先我們根據(jù)圖2我們抽象出一個(gè)簡(jiǎn)單的HFileBlock實(shí)體。
? ? 實(shí)驗(yàn)2: HFileBlock的解析.及BlockReader內(nèi)部類(lèi)
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.編寫(xiě)HFileBlock遍歷器,代碼有點(diǎn)長(zhǎng),折疊起來(lái)吧,有興趣可以看看,詳細(xì)完整代碼還是下載附件項(xiàng)目吧,
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? ? 開(kāi)始解析Root Data Index和metaIndex .在Trailer解析后,我們可以得到Load-on-open-section內(nèi)容的相關(guān)信息,可以構(gòu)造字節(jié)數(shù)組,將這部分字節(jié)碼load進(jìn)內(nèi)存進(jìn)行解析,在解析之前先講下FileInfo
? ? FileInfo的內(nèi)容是以ProtoBuf放式存放的,與Trailer類(lèi)似,我們先創(chuàng)建FileInfo.proto
編譯: protoc FileInfo.proto --java_out=.
編寫(xiě)測(cè)試類(lèi):
.... 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); //解析出來(lái)root data index MyHFileBlock dataIndex = inter.nextBlock(); int dataIndexLevels = hfileProtos.getNumDataIndexLevels(); int dataIndexEntries = hfileProtos.getDataIndexCount(); //創(chuàng)建root data index reader MyHFileBlock.BlockIndexReader rootDataReader = dataIndex.createBlockIndexReader().parseMultiLevel(dataIndexEntries,"IDXROOT2", dataIndexLevels); //解析出來(lái)root meta index MyHFileBlock metaIndex = inter.nextBlock(); ..... //獲取file info MyHFileBlock fileInfo = inter.nextBlock(); //解析讀取FileInfo內(nèi)容 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部分的各個(gè)fileblock內(nèi)容,完整代碼請(qǐng)下載附帶的地址。
? ? Scanned block section: 關(guān)于bloomfilter先不分析了。
? ? ?Non-scanned block section:
? ? 這部分內(nèi)容就是真正的數(shù)據(jù)塊,從圖1看出,這部分?jǐn)?shù)據(jù)是分datablock存儲(chǔ)的,默認(rèn)地,每個(gè)datablock占64K,如果是多層的index的話,部分index block也會(huì)存放在這里,由于我的測(cè)試數(shù)據(jù),是single-level的,所以只針對(duì)單級(jí)的index分析。
的single-level情況下,內(nèi)存的rootDataIndex記錄了每個(gè)datablock的偏移量,大小及startKey信息,主要是為了快速地定位到KeyValue的位置,在HFile中查找或者seek到某個(gè)KeyValue時(shí),首先會(huì)在內(nèi)存中,對(duì)rootDataIndex進(jìn)行二分查找,單級(jí)的index可以直接定位DataBlock,然后通過(guò)迭代datablock定位到KeyValue所在的位置,而2-3層時(shí),上面也略有提及,大家有時(shí)間的話,可以做多點(diǎn)研究這部分。
? ? 弱弱提句:在HStore中,會(huì)有cache將這些datablock緩存起來(lái),使用LRU算法,這樣會(huì)提高不少性能。
? ? 每個(gè)DataBlock同樣也是一個(gè)HFileBlock,也包括header,data,checksum信息,可以用我們之前寫(xiě)的BlockIterator就可以搞定。下面使用代碼,去遍歷一個(gè)datablock看看。
實(shí)驗(yàn)3:
? ? 編寫(xiě)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 memTSincrementOffset(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;} }? ? ?編寫(xiě)測(cè)試代碼:
? ?
//從rootDataReader中獲取第一塊的offset及數(shù)據(jù)大小 long offset = rootDataReader.getBlockOffsets()[0]; int size = rootDataReader.getBlockDataSizes()[0];byte[] dataBlockArray = new byte[size]; input.seek(offset); input.read(dataBlockArray); //圖方便,直接用iterator來(lái)解析出來(lái)FileBlock MyHFileBlockIterator dataBlockIter = new MyHFileBlockIterator(dataBlockArray); MyHFileBlock dataBlock1 = dataBlockIter.nextBlock(); //將data內(nèi)容給一個(gè)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,基本上是這些內(nèi)容了。有點(diǎn)抱歉一開(kāi)篇講得有點(diǎn)大了,其實(shí)沒(méi)有方方面面都講得很詳細(xì)。meta,bloomfilter部分沒(méi)有詳細(xì)分析,大家有時(shí)間可以研究后,分享一下。
? ? 源碼我將我測(cè)試的Hfile也附帶上傳了,壓縮后有3M多,完整代碼請(qǐng)下載:下載源碼
?
轉(zhuǎn)載于:https://www.cnblogs.com/bdifn/p/3793806.html
《新程序員》:云原生和全面數(shù)字化實(shí)踐50位技術(shù)專(zhuān)家共同創(chuàng)作,文字、視頻、音頻交互閱讀總結(jié)
以上是生活随笔為你收集整理的HBase之HFile解析的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: puppet完全攻略(一)puppet应
- 下一篇: 每日记载内容总结32