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 前缀压缩方法。
<beginning_of_file>
[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))
<end_of_file>
对于排序后的 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 的数量。