HFile解析 基于0.96
什么是HFile
HBase、BigTable以及其他分布式存储、查询系统的底层存储都采用SStable的思想,HBase的底层存储是HFile,他要解决的问题就是如果将内容存储到磁盘,以及如何高效的读取数据,HFile文件格式对特定字节存储什么内容做了规定,以便可以按照格式读取数据。
sstable就是sorted string table,存储的基本数据是key-value对,string意为着key和value都是以string/bytes的形式组织的,当然也可以是其他数据结构经序列化后的二进制流形式。
sorted意为数据的存储顺序是按照key的字符串的字典排列顺序升序组织的,并且对key构建了索引。检索时,把data index加载到内存中,通过二分查找,在data index中查找当前查询的key可能在哪个data block中,接着顺序遍历这个data block,找到key及value。
对HFile添加了布隆过滤器,这样可以过滤掉Table中不存在的key,(布隆过滤器有一定误判性,即将集合中本来不存在的数据,判断为存在,不过在这里还是可用的)
写HFile
流程
HFile问题分为写文件、读文件,而分析文件结构,得先看下写了什么,怎么写的:
1、写KeyValue之前一般会先new一个KeyValue;
分析 new KeyValue(keyBytes, "info".getBytes(), "city".getBytes(), valueBytes);
可以看到HBase会按照以下组织方式将KeyValue数据加载到内存中。
this.bytes = createByteArray(row, roffset, rlength, family, foffset, flength, qualifier, qoffset, qlength, timestamp, type, value, voffset, vlength); KeyValue维护了一个byte数组,里面保存了这个KeyValue要写入HFile的全部信息。 /** * Write KeyValue format into a byte array. * * @param row row key * @param roffset row offset * @param rlength row length * @param family family name * @param foffset family offset * @param flength family length * @param qualifier column qualifier * @param qoffset qualifier offset * @param qlength qualifier length * @param timestamp version timestamp * @param type key type * @param value column value * @param voffset value offset * @param vlength value length * @return The newly created byte array. */ private static byte [] createByteArray(final byte [] row, final int roffset, final int rlength, final byte [] family, final int foffset, int flength, final byte [] qualifier, final int qoffset, int qlength, final long timestamp, final Type type, final byte [] value, final int voffset, int vlength) { checkParameters(row, rlength, family, flength, qualifier, qlength, value, vlength); // Allocate right-sized byte array. int keyLength = (int) getKeyDataStructureSize(rlength, flength, qlength); byte [] bytes = new byte[(int) getKeyValueDataStructureSize(rlength, flength, qlength, vlength)]; // Write key, value and key row length. int pos = 0; pos = Bytes.putInt(bytes, pos, keyLength);//4B的KeyLength pos = Bytes.putInt(bytes, pos, vlength);//4B valueLength pos = Bytes.putShort(bytes, pos, (short)(rlength & 0x0000ffff));//2B RowLength pos = Bytes.putBytes(bytes, pos, row, roffset, rlength);//Row内容 pos = Bytes.putByte(bytes, pos, (byte)(flength & 0x0000ff));//1B FamilyLength if(flength != 0) { pos = Bytes.putBytes(bytes, pos, family, foffset, flength); }//将famliy内容加载到bytes if(qlength != 0) { pos = Bytes.putBytes(bytes, pos, qualifier, qoffset, qlength); }//将qualifer内容加载到bytes pos = Bytes.putLong(bytes, pos, timestamp);//timestamp pos = Bytes.putByte(bytes, pos, type.getCode());//type if (value != null && value.length > 0) { pos = Bytes.putBytes(bytes, pos, value, voffset, vlength); }//value return bytes; }
可分析出KeyValue的这个byte数组,开始是两个固定长度的数值,分别表示Key的长度和Value的长度。紧接着是Key,开始是2自己长度数据,表示Row的长度,紧接着是 Row,然后是1字节的数据,表示Family的长度,然后是Family的字节内容,接着是Qualifier,然后是两个固定长度的数值,表示(8B)Time Stamp和(1B)Key Type(Put/Delete)。Value就是纯粹的二进制数据。
2、指定压缩方式,块大小,写目录等参数,创建一个HFileWriterV2对象,写入KeyValue是调用HFileWriterV2 的append方法实现的
由于HFile的写入必须顺序写入,所以首先会checkKey做校验,写入过程会将上次写入的key保存在lastKeyBuffer,当前的key和上次写入的key作比较,保证(row,family,coqualifier,ts)顺序。
可以看到HBase写入HFile其实就是将KeyValue中的bytes相应内容写到HFile,最终的格式如下(memStoreTS):
在操作过程中,根据上面的offset以及length等可以定位到任何一部分内容。比如获取row --- Bytes(bytes,10,rlength),便可以对rowkey进行读操作,
上面操作并没有真正的写到磁盘上 (fsBlockWriter.getUserDataStream())是对ByteArrayOutputStream包装 ,write操作其实是写到了baosInMemory对象下面的buf字节数组里,而newblock()操作是对其构造参数添加了一个假的Header.
private void append(final long memstoreTS, final byte[] key, final int koffset, final int klength, final byte[] value, final int voffset, final int vlength) throws IOException { boolean dupKey = checkKey(key, koffset, klength);//校验是否符合规则 if (!dupKey) { checkBlockBoundary(); }//检查边界 if (!fsBlockWriter.isWriting()) newBlock();//第一次写入,会进行这个操作,之后在checkBlockBoundary()内执行此操作 { DataOutputStream out = fsBlockWriter.getUserDataStream(); out.writeInt(klength); totalKeyLength += klength; out.writeInt(vlength); totalValueLength += vlength; out.write(key, koffset, klength); out.write(value, voffset, vlength); if (this.includeMemstoreTS) { WritableUtils.writeVLong(out, memstoreTS); } } // Are we the first key in this block? if (firstKeyInBlock == null) { // Copy the key. firstKeyInBlock = new byte[klength]; System.arraycopy(key, koffset, firstKeyInBlock, 0, klength); } lastKeyBuffer = key;//记录最后一个key lastKeyOffset = koffset; lastKeyLength = klength; entryCount++; }
2、随着不断地加入新的KeyValue对,当到调用checkBlockBoundary()检查当前DataBlock是否超过达一定的阀值(fsBlockWriter.blockSizeWritten() < blockSize),则执行finishBlock()等操作,
在finishBlock阶段:
取出写入byte[]的uncompressedBytesWithHeader = baosInMemory.toByteArray();数据
1、按照指定压缩方式,对DataBlock(KeyValue部分)进行编码和压缩
2、组装Header,循环冗余校验信息添加等操作,将这个块加入输出流
3、dataBlockIndexWriter.addEntry(indexKey, lastDataBlockOffset, onDiskSize);
添加索引项
跟踪 writeHeaderAndData()函数
/** * Put the header into the given byte array at the given offset. * @param onDiskSize size of the block on disk header + data + checksum * @param uncompressedSize size of the block after decompression (but * before optional data block decoding) including header * @param onDiskDataSize size of the block on disk with header * and data but not including the checksums */ private void putHeader(byte[] dest, int offset, int onDiskSize, int uncompressedSize, int onDiskDataSize) { offset = blockType.put(dest, offset);//DATABLK*(8B) offset = Bytes.putInt(dest, offset, onDiskSize - HConstants.HFILEBLOCK_HEADER_SIZE);//在磁盘上block的大小(不包含头,主要用来处理压缩文件) offset = Bytes.putInt(dest, offset, uncompressedSize - HConstants.HFILEBLOCK_HEADER_SIZE);//未压缩时block的大小(不包含头) offset = Bytes.putLong(dest, offset, prevOffset);//前一个块的标识 offset = Bytes.putByte(dest, offset, checksumType.getCode());//校验和类型 offset = Bytes.putInt(dest, offset, bytesPerChecksum); Bytes.putInt(dest, offset, onDiskDataSize);// }
|
分析出Header的结构,进而得出Data Block的结构如下:
3、上面提到每次finish block会添加一条索引记录,将这个块的firstKey,lastDataBlockOffset,onDiskDataSize加入LEAF_INDEX索引项,(索引大小大概(56+AvgKeySize)*NumBlocks)
索引记录内容如下:
/** * Add one index entry to the current leaf-level block. When the leaf-level * block gets large enough, it will be flushed to disk as an inline block. * * @param firstKey the first key of the data block * @param blockOffset the offset of the data block * @param blockDataSize the on-disk size of the data block ({@link HFile} * format version 2), or the uncompressed size of the data block ( * {@link HFile} format version 1). */ public void addEntry(byte[] firstKey, long blockOffset, int blockDataSize) { curInlineChunk.add(firstKey, blockOffset, blockDataSize); ++totalNumEntries; }
得出 Data Block的块索引项:
key=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaZI/info:city/LATEST_TIMESTAMP/Put offset=2155038, dataSize=38135 key=aaaaaaaaaaaaaaaaabbbabbaaaabbbbaTU/info:city/LATEST_TIMESTAMP/Put offset=4343199, dataSize=38358 key=aaaaaaaaaaaaaaaabbbababbbbababaaxe1T70L5R6F2/info:city/LATEST_TIMESTAMP/Put offset=6539008, dataSize=38081 .....
|
并将其加入curInlineChunk (BlockIndexChunk用来保存HFileBlockIndex的索引信息,此时索引类型为为 LEAF_INDEX)
当索引的内容达到一定阀值(默认HFileBlockIndex.DEFAULT_MAX_CHUNK_SIZE 128KB)或者关闭时,便会调用 writeInlineBlocks 方法通过InlineBlockWriter将之前累积的索引项写入输出流,
会在DataBlock之后写入一个Leaf index block,其写入过程和DataBlock基本一致,值得注意的是索引也是被压缩的。
LEAF_INDEX归根到底是一个InlineBlock,所以包含和DataBlock 结构一致的Header,写入过程参见:HFileBlockIndex.BlockIndexChunk.writeNonRoot(DataOutput out)方法
LEAF_INDEX主要包含:
- 索引项的个数;
-
currentSecondaryIndexes //TODO 为了提高查询速度
- 多个索引项,每个索引项在磁盘的顺序为
- lastDataBlockOffset
- onDiskDataSize
- firstKey
同时将LEAF_INDEX的信息加入ROOT_INDEX,rootChunk.add(firstKey, offset, onDiskSize, totalNumEntries);
可以看到ROOT_INDEX,的每一个索引项包含了firstKey, offset, onDiskSize, totalNumEntries等内容。
4、当写完之后,调用write的close方法,首先执行finishBlock()将剩余的数据加入输出流,并写InlineBlockIndex,添加ROOT_INDEX对这个InlineBlockIndex的索引项。
之后顺序添加metadata、rootIndex、metaBlockIndex、FileInfo、Trailer等信息。
我比较关心的是ROOT_INDEX的构造,是由HFlieBlockIndex.IndexBlockWriter.writeIndexBlock来完成:
while (rootChunk.getRootSize() > maxChunkSize) { rootChunk = writeIntermediateLevel(out, rootChunk); numLevels += 1; }
发现如果root_index的size达到一定阀值(128K),便会加一级索引,达到3级;
之后设置 trailer.setLoadOnOpenOffset(rootIndexOffset);从这里我们便可以知道如果读取索引必须先读取Trailer。
HFile的fix trailer 包含了指向其它块的offsets信息
各个模块的内容:
执行 bin/hbase org.apache.hadoop.hbase.io.hfie.HFile -m -f /opt/local/hbase/uuid_rtgeo/06faaf5e0ea384d277061bf00757710e/info/a596111cbc8f4e1b96da230fd588f8a5
Block index size as per heapsize: 1528 reader=/opt/local/hbase/uuid_rtgeo/06faaf5e0ea384d277061bf00757710e/info/a596111cbc8f4e1b96da230fd588f8a5, compression=lzo, cacheConf=CacheConfig:enabled [cacheDataOnRead= true ] [cacheDataOnWrite= false ] [cacheIndexesOnWrite= false ] [cacheBloomsOnWrite= false ] [cacheEvictOnClose= false ] [cacheCompressed= false ], firstKey=F40000408A862DC6426C8E32B0F9971AAAA7B386C2F43FD841B40A362EA09259/info:recent/ 1385032809401 /Put, lastKey=F4FCC94050C968B4CFDE49EA4F4A1F185FB1D72D7346005A227A40A9A7358BC8/info:recent/ 1382710394100 /Put, avgKeyLen= 85 , avgValueLen= 68 , entries= 4186248 , length= 142479945 Trailer: fileinfoOffset= 142479169 , loadOnOpenDataOffset= 142478318 , dataIndexCount= 9 , metaIndexCount= 0 , totalUncomressedBytes= 683010319 , entryCount= 4186248 , compressionCodec=LZO, uncompressedDataIndexSize= 1060555 , numDataIndexLevels= 2 , firstDataBlockOffset= 0 , lastDataBlockOffset= 142336376 , comparatorClassName=org.apache.hadoop.hbase.KeyValue$KeyComparator, majorVersion= 2 , minorVersion= 0 Fileinfo: BLOOM_FILTER_TYPE = ROW DATA_BLOCK_ENCODING = NONE DELETE_FAMILY_COUNT = \x00\x00\x00\x00\x00\x00\x00\x00 EARLIEST_PUT_TS = \x00\x00\x01A\x9D\x18\xE6\x8E LAST_BLOOM_KEY = F4FCC94050C968B4CFDE49EA4F4A1F185FB1D72D7346005A227A40A9A7358BC8 MAJOR_COMPACTION_KEY = \xFF MAX_SEQ_ID_KEY = 304738752 TIMERANGE = 1381320156814 .... 1386635121792 hfile.AVG_KEY_LEN = 85 hfile.AVG_VALUE_LEN = 68 hfile.LASTKEY = \x00 @F4FCC94050C968B4CFDE49EA4F4A1F185FB1D72D7346005A227A40A9A7358BC8 \x04inforecent\x00\x00\x01A\xEF\xF6<\xF4\x04 Mid-key: \x00 @F47AB33AB35809CB16CEE830C04F5A7FB530C4E6AC75EAB8C67E4ABD7F488100 \x04inforecent\x00\x00\x01B(q2)\x04\x00\x00\x00\x00\x04:h*\x00\x005l Bloom filter: BloomSize: 131072 No of Keys in bloom: 94949 Max Keys for bloom: 109306 Percentage filled: 87 % Number of chunks: 1 Comparator: ByteArrayComparator Delete Family Bloom filter: Not present |
之于HFileV1
V1:
HFileV2相对于V1:主要做了几个改进,其中2个主个改进都主要是为了降低内存使用和启动时间,思路都是将它们切分为多个block。
- 增加了树状结构的数据块索引(data block index)支持。原因是在数据块的索引很大时,很难全部load到内存,比如当前的一个data block会在data block indxe区域对应一个数据项,假设每个block 64KB,每个索引项64Byte,这样如果每条机器上存放了6TB数据,那么索引数据就得有6GB,因此这个占用的内存还是很高的。通过对这些索引以树状结构进行组织,只让顶层索引常驻内存,其他索引按需读取并通过LRU cache进行缓存,这样就不需要将全部索引加载到内存。参见https://issues.apache.org/jira/browse/HBASE-3856。
相当于把原先平坦的索引结构以树状的结构进行分散化的组织。现在的index block也是与data block一样是散布到整个文件之中,而不再是单纯的在结尾处。同时为支持对序列化数据进行二分查找,还为非root的block index设计了新的"non-root index block"。
具体实现来看,比如在写入HFile时,在内存中会存放当前的inline block index,当inline block index大小达到一定阈值(默认128KB)时就直接flush到磁盘,而不再是最后做一次flush,这样就不需要在内存中一直保持所有的索引数据。当所有的inline block index生成之后,HFile writer会生成更上一级的block index,它里面的内容就是这些inline block index的offset,依次递归,逐步生成更上层的block index,上层的包含的就是下层的offset,直到最顶层大小小于阈值时为止。所以整个过程就是自底向上的通过下层index block逐步构建出上层index block。 - 之前的bloom filter数据是存放在单独的一个meta block里,新版里它将可以被存为多个block。这就允许在处理一个查询时不必将所有的数据都load到内存。
- 对压缩数据的支持,记录compressed size可以在扫描HFile时不用解压,直接skip过当前块。
读HFile
读操作比较简单,在创建reader的时候会先加载trailer,seek的过程中首先会根据各级BlockIndex通过二分查找,找到key所在的HFileBlock,之后判断HFileBlock是否在内存,不在则将HFileBlock加载到内存,之后顺序扫描这个块,进而seek到对应的key。
在创建Reader时会首先加载trailler,并根据trailer加载rootBlockIndex,FileInfo等信息,
读取Trailer,首先将磁盘上Trailer加载到内存,之后按照Trailer固定的格式解析出Trailer的内容
trailer = FixedFileTrailer.readFromStream(fsdis.getStream(isHBaseChecksum), size);
public static FixedFileTrailer readFromStream(FSDataInputStream istream, long fileSize) throws IOException { int bufferSize = MAX_TRAILER_SIZE; long seekPoint = fileSize - bufferSize; //trailer 位置 if (seekPoint < 0) { // It is hard to imagine such a small HFile. seekPoint = 0; bufferSize = (int) fileSize; } //加载trailer istream.seek(seekPoint); ByteBuffer buf = ByteBuffer.allocate(bufferSize); istream.readFully(buf.array(), buf.arrayOffset(), buf.arrayOffset() + buf.limit()); ..... FixedFileTrailer fft = new FixedFileTrailer(majorVersion, minorVersion); fft.deserialize(new DataInputStream(new ByteArrayInputStream(buf.array(), buf.arrayOffset() + bufferSize - trailerSize, trailerSize))); return fft; }
trailer内容:
fileinfoOffset=43649909, loadOnOpenDataOffset=43649226, dataIndexCount=20, metaIndexCount=3, totalUncomressedBytes=78307153, entryCount=600000, compressionCodec=GZ, uncompressedDataIndexSize=2608931, numDataIndexLevels=2, firstDataBlockOffset=0, lastDataBlockOffset=43613833, comparatorClassName=org.apache.hadoop.hbase.KeyValue$KeyComparator, majorVersion=2, minorVersion=3
读取trailer之后便可以获取rootIndex的位置,加载rootIndex size=2
key=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaZI/info:city/LATEST_TIMESTAMP/Put offset=3944715, dataSize=38949 key=aaaaaaaaaaaaaaaabbbaaabbaabaabbaiSje/info:city/LATEST_TIMESTAMP/Put offset=6825203, dataSize=27845
获取rootLevel的索引
int rootLevelIndex = rootBlockContainingKey(key, keyOffset, keyLength);
查找的过程实质上就是对各级索引进行二分查找,并根据BlockType来判断块是索引还是Data已决定是否需要继续向下寻找。
int pos = Bytes.binarySearch(blockKeys, key, offset, length, comparator);
public static int binarySearch(byte [][]arr, byte []key, int offset, int length, RawComparator<?> comparator) { int low = 0; int high = arr.length - 1; while (low <= high) { int mid = (low+high) >>> 1; // we have to compare in this order, because the comparator order // has special logic when the 'left side' is a special key. int cmp = comparator.compare(key, offset, length, arr[mid], 0, arr[mid].length); // key lives above the midpoint if (cmp > 0) low = mid + 1; // key lives below the midpoint else if (cmp < 0) high = mid - 1; // BAM. how often does this really happen? else return mid; } return - (low+1); }
|
进而获取下一级索引的offset等信息,
接着继续向下寻找,直到
if (block.getBlockType().equals(BlockType.DATA)
即查找的块是一个DataBlock时结束;
之后 loadBlockAndSeekToKey ,然后块内查找:
BlockWithScanInfo初始化会产生一个ByteBuffer对象,是去掉了头的block块,之后在block内的查找操作均在ByteBuffer中进行.
/** * Returns a buffer that does not include the header. The array offset points * to the start of the block data right after the header. The underlying data * array is not copied. Checksum data is not included in the returned buffer. * * @return the buffer with header skipped */ public ByteBuffer getBufferWithoutHeader() { return ByteBuffer.wrap(buf.array(), buf.arrayOffset() + headerSize(), buf.limit() - headerSize() - totalChecksumBytes()).slice(); } |
值得注意的是:如果开启cache则首先会 HFileBlock cachedBlock = (HFileBlock) cacheConf.getBlockCache().getBlock(cacheKey, cacheBlock, useLock)从cache中获取块;
如果没有则load:HFileBlock hfileBlock = fsBlockReader.readBlockData(dataBlockOffset, onDiskBlockSize, -1,pread);
并加入BlockCache
// Cache the block if necessary if (cacheBlock && cacheConf.shouldCacheBlockOnRead(hfileBlock.getBlockType().getCategory())) { cacheConf.getBlockCache().cacheBlock(cacheKey, hfileBlock, cacheConf.isInMemory()); } |
0.96中Cache包含:BuketCache(性能不错,值得推广,后续会仔细研究下),LRUBlockCache,SlabCache,DoubleBlockCache等。
//TODO HBase中的Cache可以作为一个切入点来研究下,CacheConfig类记录了配置Cache的详细参数及默认值。
blockSeek(key, offset, length, seekBefore)函数其实就是做了一个顺序查找,seek到指定位置。
private int blockSeek( byte [] key, int offset, int length, boolean seekBefore) { int klen, vlen; long memstoreTS = 0 ; int memstoreTSLen = 0 ; int lastKeyValueSize = - 1 ; //循环扫描block 查找key do { blockBuffer.mark(); klen = blockBuffer.getInt(); //key的长度 vlen = blockBuffer.getInt(); //value的长度 blockBuffer.reset(); if ( this .reader.shouldIncludeMemstoreTS()) { if ( this .reader.decodeMemstoreTS) { try { int memstoreTSOffset = blockBuffer.arrayOffset() + blockBuffer.position() + KEY_VALUE_LEN_SIZE + klen + vlen; memstoreTS = Bytes.readVLong(blockBuffer.array(), memstoreTSOffset); memstoreTSLen = WritableUtils.getVIntSize(memstoreTS); } catch (Exception e) { throw new RuntimeException( "Error reading memstore timestamp" , e); } } else { memstoreTS = 0 ; memstoreTSLen = 1 ; } } int keyOffset = blockBuffer.arrayOffset() + blockBuffer.position() + KEY_VALUE_LEN_SIZE; int comp = reader.getComparator().compareFlatKey(key, offset, length, blockBuffer.array(), keyOffset, klen); //比较 if (comp == 0 ) { if (seekBefore) { if (lastKeyValueSize < 0 ) { throw new IllegalStateException( "blockSeek with seekBefore " + "at the first key of the block: key=" + Bytes.toStringBinary(key) + ", blockOffset=" + block.getOffset() + ", onDiskSize=" + block.getOnDiskSizeWithHeader()); } blockBuffer.position(blockBuffer.position() - lastKeyValueSize); readKeyValueLen(); return 1 ; // non exact match. } currKeyLen = klen; currValueLen = vlen; if ( this .reader.shouldIncludeMemstoreTS()) { currMemstoreTS = memstoreTS; currMemstoreTSLen = memstoreTSLen; } return 0 ; // indicate exact match } else if (comp < 0 ) { if (lastKeyValueSize > 0 ) blockBuffer.position(blockBuffer.position() - lastKeyValueSize); readKeyValueLen(); if (lastKeyValueSize == - 1 && blockBuffer.position() == 0 && this .reader.trailer.getMinorVersion() >= MINOR_VERSION_WITH_FAKED_KEY) { return HConstants.INDEX_KEY_MAGIC; } return 1 ; } // The size of this key/value tuple, including key/value length fields. lastKeyValueSize = klen + vlen + memstoreTSLen + KEY_VALUE_LEN_SIZE; blockBuffer.position(blockBuffer.position() + lastKeyValueSize); } while (blockBuffer.remaining() > 0 ); //扫描到块末尾时结束 blockBuffer.position(blockBuffer.position() - lastKeyValueSize); readKeyValueLen(); return 1 ; // didn't exactly find it. } |
seek 到之后,进而获取value
public KeyValue getKeyValue() {
if (!isSeeked())
return null;
KeyValue ret = new KeyValue(blockBuffer.array(),
blockBuffer.arrayOffset() + blockBuffer.position(),
KEY_VALUE_LEN_SIZE + currKeyLen + currValueLen,
currKeyLen);
if (this.reader.shouldIncludeMemstoreTS()) {
ret.setMvccVersion(currMemstoreTS);
}
return ret;
}
这里用到比较关键的类包含ByteBuffer的各种操作,比较重要。
过程中关键类:
- ByteBuffer :block在内存的载体
- HFileReaderV2:包含对HFile进行读写操作的各种方法
- Bytes :块内二分查找方法
- HFileScanner 由reader构造出来,负责读取HFile操作
- HFileBlockIndex 记录块的索引信息,如每个块的起始key,midKey等信息
- BlockCache、BlockCacheKey
- FixedFileTrailer
- HFileBlock block对象
-
ByteArrayOutputStream
阅读HBase代码时maven依赖hadoop源码无法查看,解决办法:1、下载hadoop-1.1.2.tar.gz 2、找到其源码包,打包jar,buildpath添加、刷新即可
思考
1、如果建表时,一个row对应多个列,或者多个版本,那么一个HFileBlock所能存放的row数就会越少,这样一来块内seek速度会变快,但是对于按顺序查找或者扫描同样个数的row来说,需要不停地load块,反而效率变低。
2、扫描操作会不停地将块加到缓存,如果多个进行扫描缓存块,而只有一个进程来淘汰块,那么很可能会发生内存溢出,所以在线下计算需要大量扫描的情况下,最好能关闭cache.
目前默认缓存策略为LRU
1.一个Block从被缓存至被淘汰,基本就伴随着Heap中的位置从New区晋升到Old区
2.晋升在Old区的Block被淘汰后,最终由CMS进行垃圾回收,随之带来的是Heap碎片
3.因为碎片问题,随之而来的是GC时晋升失败的FullGC。
4.对于高频率的,如果缓存的速度比淘汰的速度快,有OOM的风险
3、StoreFile过多会影响读性能,因为一个读操作很可能会需要打开多个HFile,io次数太多,会影响性能
4、HFile block越大越适合顺序扫描(大的快解压速度会变慢)但是随机读写性能降低,小的HFile block更加适合随机读写,但是需要更多地内存来存放index.默认(64 * 1024B)
5、HFileV2的分级索引,虽然可以减少内存的使用量,加快启动速度,但是多了1-2次IO,尤其是HFile文件较大时,随机读很可能比V1要多2次IO操作,进而读取速度变慢。
6、gz压缩方式比lzo好,但是压缩和解压时消耗的CPU大于lzo
参考:
http://hbase.apache.org/book/hfilev2.html
http://duanple.blog.163.com/blog/static/709717672011916103311428/