入口

org.apache.hadoop.hbase.io.hfile.HFileReaderImpl.HFileScannerImpl#seekTo(org.apache.hadoop.hbase.Cell, boolean) :

public int seekTo(Cell key, boolean rewind) throws IOException {
  HFileBlockIndex.BlockIndexReader indexReader = reader.getDataBlockIndexReader();
  BlockWithScanInfo blockWithScanInfo = indexReader.loadDataBlockWithScanInfo(key, curBlock,
                                                                              cacheBlocks, pread, isCompaction, getEffectiveDataBlockEncoding(), reader);
  if (blockWithScanInfo == null || blockWithScanInfo.getHFileBlock() == null) {
    // This happens if the key e.g. falls before the beginning of the file.
    return -1;
  }
  return loadBlockAndSeekToKey(blockWithScanInfo.getHFileBlock(),
                               blockWithScanInfo.getNextIndexedKey(), rewind, key, false);
}

大逻辑分为两步走:

  1. 检索 index block 定位到 data block 的位置;
  2. 在 data block 中根据 key进行游标定位。

检索 index block

  1. 获取 root index block(该 block 在 load-on-open 阶段就已经被加载并解析完毕);
  2. 通过二分查找在 root index block 种定位 entry,这里 key 的对比逻辑会走 CellComparatorImpl#compare(Cell, Cell),右边是 KeyValue$KeyOnlyKeyValue 如果发现比最小的 key 小,则直接返回 -1(标识游标停在文件初)
  3. 这里会开启一个循环,目的是为了定位到具体的 data block。根据 entry 取下一个要读的 block,从 entry 取出要读的 block 的 offset 和 size,以读取下一个 block,根据 lookupLevel 和 searchTreeLevel 判断要读的 block 的类型:

    1. searchTreeLevel 是索引树的高度,为 1 说明只有 root index block,为 2 说明 root index block 指向 leaf index block,大于 2 则中间有 n-2 层 intermediate-level block,以此类推;
    2. lookupLevel 是当前读到的 level,如果小于 searchTreeLevel 说明还要读 index block,否则就要读 data block;
  4. 查看读出来的 block 类型,如果是 data block 则跳出循环,否则:

    1. 查看该 key 在该 index block 中的位置,这里也使用二分,右边的实现是 BytebufferKeyOnlyKeyValue:HFileBlockIndex#locateNonRootIndexEntry -> HFileBlockIndex#binarySearchNonRootIndex -> PrivateCellUtil#compareKeyIgnoresMvcc
    2. 如果小于 0(即小于 index block 记录的最小值),则抛出异常:The key xxx is before the first key of the non-root index block,因为在上一步已经确定该 key 在该 hfile 的范围里;
    3. 定位到具体的 entry(这里会修改 index block 里 buffer 的 position),用于检索下一层 block;
    4. 回到上一层的第三步,直到找到 data block。

检索 data block

调用 HFileReaderImpl.HFileScannerImpl#loadBlockAndSeekToKey -> HFileReaderImpl.HFileScannerImpl#blockSeek,遍历 data block 中的数据(BytebufferKeyOnlyKeyValue)和指定 key 对比:

  1. blockSeek 有个参数 seekBefore(默认为 false),用于控制当找到匹配上的 data entry 时,是否需要回退一个单位,如果回退,则返回 1,否则返回 0(0 表示精准匹配,游标停在匹配的位置)
  2. 如果第一次匹配就发现 cmp <0(即比 data block 的第一个 data entry 都要小),则返回 -2;
  3. 不断往下匹配,直至找到第一个大于指定 key 的 data entry(cmp<0),将该 block 的 buffer position 回滚一个单位,返回 1(表示游标停在最后一个小于 key 的 entry 上,下一个 entry 就比 key 大了)

Seek 的使用场景

Seek 主要应用于两类场景:

  1. 点查:给定 key,查找符合条件的该 key 最新版本的数据。考虑到 HBase 的数据会包含 timestamp,需要考虑较为复杂的,同 key 不同 timestamp 的情况:

    • 点查的 key 小于文件第一条数据:底层 scanner seek 返回 -1/-2。此时调用 seekTo 使 scanner 游标停在文件初始位置;
    • 底层 scanner 的 seek 返回 0。精准匹配,直接读取数据即可。
    • 底层 scanner seek 返回 1。由于 scanner 会回退一个数据单位,可直接读取当前数据比对 key。
  2. 区间扫描:给定 key,扫描大于等于该 key 的所有数据。该场景和点查的逻辑有一点不同,即 scanner seek 返回 1 的时候,需要往下推进一个数据单位,因为 HBase 做 seek 操作时都会有一次回退。

Mulavar
33 声望19 粉丝