LevelDB .ldb 文件 key 前缀压缩方法

LevelDB 的 table 文件以 .ldb 作为文件扩展名,包括若干个 block,data block 存储按照 key 的字母表顺序排序的 KV 对数据,meta block 存储 filter 数据,metaindex block 存储 每个 meta block 的 offset 信息,index block 存储每个 data block 的 offset 信息。对于 data block,引入了前缀压缩技术减少磁盘空间占用。本文结合 LevelDB 代码,探讨 .ldb 文件的 data block key 前缀压缩方法。


[data block 1]
[data block 2]
...
[data block N]
[meta block 1]
...
[meta block K]
[metaindex block]
[index block]
[Footer]        (fixed size; starts at file_size - sizeof(Footer))

对于排序后的 KV 对,相邻的 key 通常会具有相似的前缀。LevelDB 基于相邻 key 的共享前缀对 key 进行了压缩。更小的 data block 可以实现更快的读取,也对 page cache 更为友好。

Layout

[k1, v1] -- offset1
[k2', v2] 
[k3', v3]
...
[k14', v14]
[k15', v15]
[k16, v16] -- offset2
[k17', v17]
...
[offset1, offset2, offset3, ...]
[num of restart points]

key 压缩后的 data block 如上所示,由 KV 对和 restart point 构成。采用类似于稀疏索引的方式,部分 KV 对记录完整的 key,作为 restart point。restart point 之间的 key 则通过共同前缀进行压缩。在 data block 的最后,保存了各个 restart point 的 offset 以及 restart point 的数量。

Writing Flow


void BlockBuilder::Add(const Slice& key, const Slice& value) {
  size_t shared = 0;
    // 每16个 key 为一个 restart point,restart point 保存完整的 key
  if (counter_ < options_->block_restart_interval) {
    // 计算共同前缀长度
    const size_t min_length = std::min(last_key_piece.size(), key.size());
    while ((shared < min_length) && (last_key_piece[shared] == key[shared])) {
      shared++;
    }
  } else {
    // 记录 restart point
    restarts_.push_back(buffer_.size());
    counter_ = 0;
  }
  const size_t non_shared = key.size() - shared;

  // 写入 KV 对
  buffer_.append(key.data() + shared, non_shared);
  buffer_.append(value.data(), value.size());

  // 记录 last key
  last_key_.resize(shared);
  last_key_.append(key.data() + shared, non_shared);
  counter_++;
}

Slice BlockBuilder::Finish() {
  // Append restart array
  for (size_t i = 0; i < restarts_.size(); i++) {
    PutFixed32(&buffer_, restarts_[i]);
  }
  PutFixed32(&buffer_, restarts_.size());
  finished_ = true;
  return Slice(buffer_);
}

data block 的生成流程如上所示,以第每16个 key 作为 restart point,记录完整的 key。restart point 间的 key 则只在前一个 key 的基础之上,记录不同的后缀。当 block 写入完成时,在 block 的最后追加写入 restart point。

Reading Flow

virtual void Seek(const Slice& target) {
    // 二分查找最大的 restart point,满足 restart point key < target
    uint32_t left = 0;
    uint32_t right = num_restarts_ - 1;
    while (left < right) {
      uint32_t mid = (left + right + 1) / 2;
      uint32_t region_offset = GetRestartPoint(mid);
      uint32_t shared, non_shared, value_length;
      const char* key_ptr =
          DecodeEntry(data_ + region_offset, data_ + restarts_, &shared,
                      &non_shared, &value_length);
      if (key_ptr == nullptr || (shared != 0)) {
        CorruptionError();
        return;
      }
      Slice mid_key(key_ptr, non_shared);
      if (Compare(mid_key, target) < 0) {
        left = mid;
      } else {
        right = mid - 1;
      }
    }

    // 从 restart point 后的 key 开始线性搜索
    SeekToRestartPoint(left);
    while (true) {
      if (!ParseNextKey()) {
        return;
      }
      if (Compare(key_, target) >= 0) {
        return;
      }
    }
  }

数据的读取,是一定需要完整的 key 进行比较的。数据读取流程为了避免不必要的 decode,先通过 restart point 进行二分查找,找到最大的 key,再进行线性搜索。最差情况下的搜索次数为 Log(n) + 15,其中 n 为 data block 中 key 的数量。

参考文献

leveldb/table_format

你可能感兴趣的:(LevelDB .ldb 文件 key 前缀压缩方法)