本篇文章分析下leveldb写sst文件的源码,本质上就是为immemtable compaction到leveldb0文件提供接口,主要是插入。如果要理解这部分的源码,首先必须先将上篇sst文件格式搞清楚,否则,看源码会非常吃力,或者说毫无头绪。
这部分源码涉及到table文件下的block_builder.h/.cc,filter_block.h/.cc和table_builder.h/.cc。先分析下block_builder.h/.cc文件,主要功能就是用于写data block和index block。向外提供主要接口就是void BlockBuilder::Add(const Slice& key, const Slice& value) .
首先,看这个类的类名就知道这个类是用来构造一个块的,data block和index block都是通过这个类构造出来的。来看下这个类的成员变量有哪些:
1
2
3
4
5
6
7
|
const Options* options_;
std::
string buffer_;
std::
vector<
uint32_t> restarts_;
int counter_;
bool finished_;
std::
string last_key_;
|
因为这个类就要是为了构造块,所以这个类首先要提供add键值对的接口,其次是要有返回这个块所有数据的接口,便于上层接口将数据写到磁盘中,所以主要接口如下:
1
2
3
4
5
6
7
8
9
10
|
explicit (const Options* options);
void Reset();
void Add(const Slice& key, const Slice& value);
Slice Finish();
size_t CurrentSizeEstimate() const;
|
接下来,分析每个函数的源码,构造函数如下;
1
2
3
4
5
6
7
8
|
BlockBuilder::BlockBuilder(
const Options* options)
: options_(options),
restarts_(),
counter_(0),
finished_(
false) {
assert(options->block_restart_interval >=
1);
restarts_.push_back(
0);
}
|
发现构造函数没有什么好分析的,就最后一句。因为第一条肯定是Restart点,所以把0地址添加进restarts。
重置函数源码如下:
1
2
3
4
5
6
7
8
|
kBuilder::Reset() {
buffer_.
clear();
restarts_.
clear();
restarts_.push_back(
0);
counter_ =
0;
finished_ =
false;
last_key_.
clear();
}
|
接下来是这个块内容大小的估计函数
1
2
3
4
5
|
size_t BlockBuilder::CurrentSizeEstimate()
const {
return (buffer_.size() +
restarts_.size() *
sizeof(
uint32_t) +
sizeof(
uint32_t));
|
这个函数主要用于判断某个块的容量是否到达上限,到达之后,要把数据刷新到磁盘,然后重新开始写下一个块。
接下来是这个类最重要的函数,add添加键值对函数,这里还是把记录格式在贴出来,方便对照:
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
|
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()
|| options_->comparator->Compare(
key, last_key_piece) >
0);
size_t shared =
0;
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 {
restarts_.push_back(buffer_.
size());
counter_ =
0;
}
const size_t non_shared =
key.
size() - shared;
PutVarint32(&buffer_, shared);
PutVarint32(&buffer_, non_shared);
PutVarint32(&buffer_, value.
size());
buffer_.
append(
key.data() + shared, non_shared);
buffer_.
append(value.data(), value.
size());
last_key_.resize(shared);
last_key_.
append(
key.data() + shared, non_shared);
assert(Slice(last_key_) ==
key);
counter_++;
}
|
这个函数需要注意的是,每个Restart节点的共享部分为0,,因为没有上一条记录嘛。然后按协议封装好一条完整记录添加到buffer_即可,接下来,就是finish函数做的事了。
1
2
3
4
5
6
7
8
9
|
Slice BlockBuilder::Finish() {
for (
size_t i =
0; i < restarts_.size(); i++) {
PutFixed32(&buffer_, restarts_[i]);
}
PutFixed32(&buffer_, restarts_.size());
finished_ =
true;
return Slice(buffer_);
}
|
这个函数主要是向table_builder提供返回这个块内容的接口,然后由table_builder调用函数写回磁盘。
FilterBlockBuilder类
这个类用于写Meta block,也就是创建过滤器。先来分析主要成员变量:
1
2
3
4
5
6
|
const FilterPolicy* policy_;
std::
string keys_;
std::
vector<
size_t> start_;
std::
string result_;
std::
vector<Slice> tmp_keys_;
std::
vector<
uint32_t> filter_offsets_;
|
接下来介绍下主要函数:
开始创建Fliter条目函数
1
2
3
4
5
6
7
|
void FilterBlockBuilder::StartBlock(
uint64_t block_offset) {
uint64_t filter_index = (block_offset / kFilterBase);
assert(filter_index >= filter_offsets_.size());
while (filter_index > filter_offsets_.size()) {
GenerateFilter();
}
}
|
在table_builder.cc中,当一个块被刷新到磁盘时,就调用一次start_block函数,而触发块刷新的条件是,这个块的大小>=r->options.block_size=4096,所以每次都创建一个Filter,但是Filter有两个数组指向>=2的Filter条目。
创建Filer条目函数:
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
|
void FilterBlockBuilder::GenerateFilter() {
const size_t num_keys = start_.
size();
if (num_keys ==
0) {
filter_offsets_.push_back(result_.
size());
return;
}
start_.push_back(keys_.
size());
tmp_keys_.resize(num_keys);
for (size_t i =
0; i < num_keys; i++) {
const
char* base = keys_.data() + start_[i];
size_t length = start_[i+
1] - start_[i];
tmp_keys_[i] = Slice(base, length);
}
filter_offsets_.push_back(result_.
size());
policy_->CreateFilter(&tmp_keys_[
0], num_keys, &result_);
tmp_keys_.
clear();
keys_.
clear();
start_.
clear();
}
|
Filter i添加键值的函数:
1
2
3
4
5
|
void FilterBlockBuilder::AddKey(
const Slice&
key) {
Slice k =
key;
start_.push_back(keys_.
size());
keys_.
append(k.data(), k.
size());
}
|
表示Meta block块写结束的函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
Slice FilterBlockBuilder::Finish() {
if (!start_.empty()) {
GenerateFilter();
}
const
uint32_t array_offset = result_.size();
for (
size_t i =
0; i < filter_offsets_.size(); i++) {
PutFixed32(&result_, filter_offsets_[i]);
}
PutFixed32(&result_, array_offset);
result_.push_back(kFilterBaseLg);
return Slice(result_);
}
|
TableBuilder类
这个类主要功能就是创建一个sst文件,它调用了block_builder和filerblockbuilder。这个类属性有点多,需要好好记清楚了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
struct TableBuilder::Rep {
Options options;
Options index_block_options;
WritableFile* file;
uint64_t offset;
Status status;
BlockBuilder data_block;
BlockBuilder index_block;
std::
string last_key;
int64_t num_entries;
bool closed;
FilterBlockBuilder* filter_block;
bool pending_index_entry;
BlockHandle pending_handle;
std::
string compressed_output;
|
关键还是data_block,因为data_block要用多次,写块,刷新到磁盘,重置块等等。C++中用class代替struct,这里展示了struct用的场景之一,就是类里成员变量太多时,可以用struct封装。
接下来,主要介绍table_builder主要函数。
往data block添加一条记录函数:
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
|
void TableBuilder::Add(const Slice& key, const Slice& value) {
Rep* r = rep_;
assert(!r->closed);
if (!ok()) return;
if (r->num_entries >
0) {
assert(r->
options.comparator->C
ompare(key, Slice(r->last_key)) >
0);
}
if (r->pending_index_entry) {
assert(r->data_block.empty());
r->
options.comparator->F
indShortestSeparator(&r->last_key, key);
std::string handle_encoding;
r->pending_handle.EncodeTo(&handle_encoding);
r->
index_block.Add(r->last_key, Slice(handle_encoding));
r->pending_index_entry =
false;
}
if (r->filter_block != NULL) {
r->
filter_block->AddKey(key);
}
r->last_key.assign(key.
data(), key.size());
r->num_entries++;
r->data_block.Add(key, value);数据块添加记录
const size_t estimated_block_size = r->data_block.CurrentSizeEstimate();
if (estimated_block_size >= r->options.block_size) {
Flush();
}
}
|
刷新函数为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
void TableBuilder::Flush() {
Rep* r = rep_;
assert(!r->closed);
if (!ok()) return;
if (r->data_block.empty()) return;
assert(!r->pending_index_entry);
W
riteBlock(&r->
data_block, &r->pending_handle);
if (ok()) {
r->pending_index_entry =
true;
r->
status = r->
file->Flush();
}
if (r->filter_block != NULL) {
r->
filter_block->S
tartBlock(r->offset);
}
}
|
刷新操作主要有以下步骤:
- 将这个块的数据刷新到磁盘。因为底层调用的是c标准io流,所以数据是先写到用户态的缓存中,然后调用flush,再刷新到磁盘。
- 在WriteBlock函数内部还在index block添加一条记录。
- 重新开启一条Filter条目。
接下来是WriteBlock函数:
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
|
void TableBuilder::WriteBlock(BlockBuilder*
block, BlockHandle* handle) {
assert(ok());
Rep* r = rep_;
S
lice raw = block->Finish();
Slice block_contents;
C
ompressionType type = r->options.compression;
switch (type) {
case kNoCompression:
block_contents = raw;
break;
case kSnappyCompression: {
std::string* compressed = &r->compressed_output;
if (port::Snappy_Compress(raw.
data(), raw.size(), compressed) &&
compressed->size() < raw.size() - (raw.size() /
8u)) {
block_contents = *compressed;
}
else {
block_contents = raw;
type = kNoCompression;
}
break;
}
}
WriteRawBlock(block_contents, type, handle);
r->compressed_output.clear();
block->Reset();
}
|
这个函数主要是用于判断data block的数据是否要压缩存储,真正下操作在下面函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
void TableBuilder::WriteRawBlock(const Slice& block_contents,
CompressionType type,
BlockHandle* handle) {
Rep* r = rep_;
handle->
set_offset(r->offset);
handle->set_size(block_contents.size());
r->
status = r->
file->Append(block_contents);
if (r->status.ok()) {
char trailer[kBlockTrailerSize];
trailer[
0] = type;
uint32_t crc = crc32c::Value(block_contents.
data(), block_contents.size());
crc = crc32c::Extend(crc, trailer,
1);
EncodeFixed32(trailer+
1, crc32c::Mask(crc));
r->
status = r->
file->Append(Slice(trailer, kBlockTrailerSize));
if (r->status.ok()) {
r->offset += block_contents.size() + kBlockTrailerSize;
}
}
}
|
这个函数主要作用就是将数据写进用户态缓冲区,添加类型和CRC码,更新偏移量。
最后还有一个sst文件写完成函数,用于上层函数调用:
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
|
Status TableBuilder::Finish() {
Rep* r = rep_;
Flush();
assert(!r->closed);
r->closed =
true;
BlockHandle filter_block_handle, metaindex_block_handle, index_block_handle;
if (ok() && r->filter_block != NULL) {
W
riteRawBlock(r->
filter_block->Finish(), kNoCompression,
&filter_block_handle);
}
if (ok()) {
B
lockBuilder meta_index_block(&r->options);
if (r->filter_block != NULL) {
std::string key =
"filter.";
key.append(r->
options.filter_policy->Name());
std::string handle_encoding;
filter_block_handle.EncodeTo(&handle_encoding);
meta_index_block.Add(key, handle_encoding);
}
WriteBlock(&meta_index_block, &metaindex_block_handle);
}
if (ok()) {
if (r->pending_index_entry) {
r->
options.comparator->F
indShortSuccessor(&r->last_key);
std::string handle_encoding;
r->pending_handle.EncodeTo(&handle_encoding);
r->
index_block.Add(r->last_key, Slice(handle_encoding));
r->pending_index_entry =
false;
}
W
riteBlock(&r->index_block, &index_block_handle);
}
if (ok()) {
Footer footer;
footer.set_metaindex_handle(metaindex_block_handle);
footer.set_index_handle(index_block_handle);
std::string footer_encoding;
footer.EncodeTo(&footer_encoding);
r->
status = r->
file->Append(footer_encoding);
if (r->status.ok()) {
r->offset += footer_encoding.size();
}
}
return r->status;
}
|
至此,一个sst文件就建好了。最后还有一个函数,用于调用table_builder来创建sst文件,在builder.h/.cc里,这个等到compaction是再分析。
leveldb将immemtable compcation到sst0就这样分析结束,接下来,就是读sst文件,读总是比写更复杂。。。
原文:大专栏 leveldb源码分析之写sst文件