什么是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。

  1. 增加了树状结构的数据块索引(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。
  2. 之前的bloom filter数据是存放在单独的一个meta block里,新版里它将可以被存为多个block。这就允许在处理一个查询时不必将所有的数据都load到内存。
  3. 对压缩数据的支持,记录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的各种操作,比较重要。

过程中关键类:

  1. ByteBuffer :block在内存的载体
  2. HFileReaderV2:包含对HFile进行读写操作的各种方法
  3. Bytes :块内二分查找方法
  4. HFileScanner 由reader构造出来,负责读取HFile操作
  5. HFileBlockIndex 记录块的索引信息,如每个块的起始key,midKey等信息
  6. BlockCache、BlockCacheKey
  7. FixedFileTrailer
  8. HFileBlock block对象
  9. 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://www.slideshare.net/schubertzhang/hfile-a-blockindexed-file-format-to-store-sorted-keyvalue-pairs

http://duanple.blog.163.com/blog/static/709717672011916103311428/

HFile解析 基于0.96的更多相关文章

  1. String,StringBuffer和StringBuilder源码解析[基于JDK6]

    最近指导几位新人,学习了一下String,StringBuffer和StringBuilder类,从反馈的结果来看,总体感觉学习的深度不够,没有读出东西.其实,JDK的源码是越读越有味的.下面总结一下 ...

  2. 【甘道夫】HBase(0.96以上版本号)过滤器Filter具体解释及实例代码

    说明: 本文參考官方Ref Guide,Developer API和众多博客.并结合实測代码编写.具体总结HBase的Filter功能,并附上每类Filter的对应代码实现. 本文尽量遵从Ref Gu ...

  3. HBase(0.96以上版本)过滤器Filter详解及实例代码

    说明: 本文参考官方Ref Guide,Developer API和众多博客,并结合实测代码编写,详细总结HBase的Filter功能,并附上每类Filter的相应代码实现. 本文尽量遵从Ref Gu ...

  4. Hbase 0.96 比 hbase 0.94的改变

    转载:http://blog.csdn.net/hxpjava1/article/details/20043703 环境: hadoop:hadoop-2.2.0 hbase:hbase-0.96.0 ...

  5. 灵感手环第一步——0.96寸OLED显示实验

    这算是我这个系列的第一篇博客吧.首先要解决的就是屏幕显示问题.我选择了目前新兴起的OLED显示模块. OLED(OrganicLightEmittingDiode),中文译作有机发光二极管,目前被广泛 ...

  6. netty源码解解析(4.0)-11 Channel NIO实现-概览

      结构设计 Channel的NIO实现位于io.netty.channel.nio包和io.netty.channel.socket.nio包中,其中io.netty.channel.nio是抽象实 ...

  7. netty源码解解析(4.0)-10 ChannelPipleline的默认实现--事件传递及处理

    事件触发.传递.处理是DefaultChannelPipleline实现的另一个核心能力.在前面在章节中粗略地讲过了事件的处理流程,本章将会详细地分析其中的所有关键细节.这些关键点包括: 事件触发接口 ...

  8. Hadoop 2.2 & HBase 0.96 Maven 依赖总结

    由于Hbase 0.94对Hadoop 2.x的支持不是非常好,故直接添加Hbase 0.94的jar依赖可能会导致问题. 但是直接添加Hbase0.96的依赖,由于官方并没有发布Hbase 0.96 ...

  9. netty源码解解析(4.0)-17 ChannelHandler: IdleStateHandler实现

    io.netty.handler.timeout.IdleStateHandler功能是监测Channel上read, write或者这两者的空闲状态.当Channel超过了指定的空闲时间时,这个Ha ...

随机推荐

  1. 判断文件结束,feof……

    因为文本文件中存储的是ASCII码,而ASCII码中FF代表空值(blank),一般不使用,所以如果读文件返回了FF,说明已经到了文本文件的结尾.但是如果是二进制文件,其中可能会包含FF,因此不能把读 ...

  2. Spark系列(五)Master主备切换机制

    Spark Master主备切换主要有两种机制,之中是基于文件系统,一种是基于Zookeeper.基于文件系统的主备切换机制需要在Active Master挂掉后手动切换到Standby Master ...

  3. ActiveReport资料

    1. ActiveReports for .NET 2 Online | ActiveReports for .NET 3 Online 2.GroupHeader块 ①GroupHeader块为每个 ...

  4. Codeforces 372

    A (被装的袋鼠不可以装的袋鼠)贪心,排序,从n/2分成两部分. B 好一道前缀和的题目. C 标准算法不难想,m^2的算法见http://codeforces.com/blog/entry/9907 ...

  5. Spring Named Parameters examples in SimpleJdbcTemplate

    In JdbcTemplate, SQL parameters are represented by a special placeholder "?" symbol and bi ...

  6. codeforce 621A(水题)

    A. Wet Shark and Odd and Even time limit per test 2 seconds memory limit per test 256 megabytes inpu ...

  7. codeforces 630C Lucky Numbers

    C. Lucky Numbers time limit per test 0.5 seconds memory limit per test 64 megabytes input standard i ...

  8. baseadapter.getItemId的使用方法:实现listview筛选、动态删除

    转载:http://www.lai18.com/content/1631131.html 这里的listview筛选是指listview的adapter实现filter来过滤数据. “动态删除&quo ...

  9. SQL Prompt 5.3.4.1

    Red Gate系列之三 SQL Server 开发利器 SQL Prompt 5.3.4.1 Edition T-SQL智能感知分析器 完全破解+使用教程   Red Gate系列之三 SQL Se ...

  10. when not exists 用法

    USE [ChangHong_612]GO/****** Object: StoredProcedure [dbo].[st_MES_UpdateInspectResult] Script Date: ...