LevelDB 源码|Block的构建

LevelDB 源码分析:Block 的构建

block_builder.h/.cc

在 LevelDB 的 SSTable 中,数据不是一条一条顺序写入文件,而是先组织成一个个 Block,再由上层写入 SSTable。

block_builder.h/.cc 的职责,就是负责在内存中构建一个 Block

1
2
3
4
5
6
7
8
9
不断 Add(key, value)

编码并追加到 buffer_ [entry01, entry02..]

Block 达到合适大小

Finish()

得到一个完整 Block

1. BlockBuilder 的核心状态

BlockBuilder 中的几个成员包括:

1
2
3
4
5
std::string buffer_;             // 保存当前 Block 已经编码好的内容;
std::vector<uint32_t> restarts_; // 保存 restart point 在 buffer_ 中的偏移;
int counter_; // 记录距离上一次 restart 已经写入了多少个 key;
std::string last_key_; // 上一条完整 key,用于计算公共前缀;
bool finished_; // 标记当前 Block 是否已经调用过 `Finish()`。

2. 一条 Entry 是如何编码的

每次上层调用:Add(key, value), BlockBuilder 都会向 buffer_ 中追加一条 Entry。

一条 Entry 的基本格式是:

1
2
3
4
5
<shared>        // 与上一条key的公共部分的大小
<non_shared> // 非..
<value_size>
<key_delta> // 通常是key的不同部分
<value>

前三个字段使用 Varint32 编码。

例如:

1
2
last_key = "apple001"
key = "apple002"

它们共享:

1
"apple00"

因此:

1
2
3
shared = 7
non_shared = 1
key_delta = "2"

最终并不需要再次保存完整的 "apple002"


3. 前缀压缩

BlockBuilder 中一个很重要的设计就是相邻 key 的前缀压缩

由于 LevelDB 写入 SSTable 时,key 本身是有序的,相邻 key 往往会拥有较长的公共前缀。

因此代码会比较 last_key_ 与当前 key。

并计算二者的公共前缀长度: shared

随后计算non_shared

1
const size_t non_shared = key.size() - shared;

真正写入 key 的代码:

1
buffer_.append(key.data() + shared, non_shared);

含义就是:

跳过前面已经共享的部分,只保存当前 key 的剩余部分。

因此在不断调用 Add() 时,buffer_ 中的数据大致就是:

1
2
3
4
<shared><non_shared><value_size><key_delta><value>
<shared><non_shared><value_size><key_delta><value>
<shared><non_shared><value_size><key_delta><value>
...

4. 为什么需要 Restart Point

如果所有 key 都一直依赖上一条 key,那么虽然压缩率很高,但如果想直接读取 Block 中间的某个 key,就必须从最前面开始,一个个恢复 key。

因此 LevelDB 引入了 restart point。

Options_ 配置中存在:
block_restart_interval = 16

即每16bytes记录一次完整的key

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
size_t shared = 0;
if (counter_ < options_->block_restart_interval) {
// See how much sharing to do with previous string
// 找公共前缀的size
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 compression
// Entries 到达了 restart_interval,重置 counter_,将当前 buffer 大小记录。
// ** shared 未改变,后续逻辑复用。由于shared=0,buffer中直接记录了完整的key。
restarts_.push_back(buffer_.size());
counter_ = 0;
}

restart entry 并没有特殊的编码格式。

它依旧是:

只是此时:
shared = 0

所以 key_delta 本身就是完整 key。


5. Finish

在不断 Add() 的过程中,buffer_ 中主要保存的是一条条 Entry。

restart point 的位置则暂时单独保存在:
restarts_

直到调用:

1
Finish()

才会把这些 restart offset 依次追加到 buffer_ 尾部,最后再保存 restart point 的数量。

因此,一个最终完成的 Block 大致可以表示为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
+-----------------------------+
| Entry 1 |
+-----------------------------+
| Entry 2 |
+-----------------------------+
| Entry 3 |
+-----------------------------+
| ... |
+-----------------------------+
| Entry N |
+-----------------------------+
| Restart Offset 0 |
+-----------------------------+
| Restart Offset 1 |
+-----------------------------+
| ... |
+-----------------------------+
| Number of Restarts |
+-----------------------------+

其中 restart offsets 和 restart 数量使用 fixed32 保存。

1
2
3
4
5
6
7
8
9
10
// 完成 Block 的构建后调用,在 Block 末尾记录 restart 的偏移信息
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_);
}

6. 总结

BlockBuilder 的职责:

将一批有序 key-value 编码成 LevelDB 所需要的 Block 格式。

其中最核心的设计主要有三个:

  1. 使用相邻 key 的公共前缀进行压缩;
  2. 使用 restart point 在压缩率和随机读取效率之间做平衡;
  3. 使用 Varint 保存长度信息,进一步减少空间占用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
void BlockBuilder::Add(const Slice& key, const Slice& value) {
Slice last_key_piece(last_key_);
assert(!finished_);
assert(counter_ <= options_->block_restart_interval);
assert(buffer_.empty() // No values yet?
|| options_->comparator->Compare(key, last_key_piece) > 0);
size_t shared = 0;
if (counter_ < options_->block_restart_interval) {
// See how much sharing to do with previous string
// 找公共前缀的size
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 compression
// Entries 到达了 restart_interval,重置 counter_,将当前 buffer 大小记录。
// ** shared 未改变,后续逻辑复用。由于shared=0,buffer中直接记录了完整的key。
restarts_.push_back(buffer_.size());
counter_ = 0;
}
// 独有部分的大小
const size_t non_shared = key.size() - shared;

// Add "<shared><non_shared><value_size>" to buffer_
PutVarint32(&buffer_, shared);
PutVarint32(&buffer_, non_shared);
PutVarint32(&buffer_, value.size());

// 添加 key 的 delta 部分。
buffer_.append(key.data() + shared, non_shared);
buffer_.append(value.data(), value.size());

// Update state
last_key_.resize(shared);
last_key_.append(key.data() + shared, non_shared);
assert(Slice(last_key_) == key);
counter_++;
}


LevelDB 源码|Block的构建
http://example.com/2026/09/11/LevelDB-源码|Block的构建/
作者
lorixyu
发布于
2026年9月11日
许可协议