文章目录
前言
compaction 作为单机引擎rocksdb/leveldb LSM tree 实现中的一个关键步骤,用来对底层存储的SST文件中的key进行排序去重,同时对其中针对key的不同操作进行处理。总之,就是保证了底层数据存储的有序性。
接下来的compaction相关的实现细节是基于rocksdb6.4.6 版本进行的描述。
关于compaction 原理实现,看到网络上已经有很多的描述,像基本的分层实现,以及如何触发compaction 这一些基础实现就不再赘述。如果想要了解,可以参考官网rocksdb – compaction概述,以及针对默认compaction 策略level compaction 过程描述。
comapction流程概述
先描述一下整体的过程,拿之前rocksdb的一个写流程来说,如下图:
compaction是在整个写流程的右下角部分,是针对磁盘上LSM tree管理的分层结构中的SST 文件进行的处理。
- key-value 数据 从immutable memtable 经过 pagecache + blockcache(direct方式写的话则不经过page cache) 写入到磁盘的L0 层,形成一个一个SST文件。此时不同SST文件之间的key-value 是无序的,因为数据在memtable之中由跳表组织(有序),但是写入到SST 文件之后,不同的SST 文件之间是无序的。
- 此时如果SST文件个数达到L0 触发compaction的条件level0_file_num_compaction_trigger 的个数之后,会触发compaction工作线程。
- compaction前期是进行一系列的准备工作,主要功能是 提取compaction的input,并规划好output,这一些input在用户看来就是一个个SST文件,但是在其内部就是一个个key-value。此时这一些input存在于内存,且之间无序。
- 拿着input 进行堆排序和merge操作,形成有序且最新的key的集合。
- 最终按照SST文件内部的数据存储格式写入到output level的 SST文件内部。
看似是一个文件从输入,做了合并排序,到最后输出,其实内部实现有相当多的细节:保证key的有序,对不同类型的key(put/delete/merge)进行对应的处理,保证同一个key的不同snapshot得到合并,保证最终写入的key是按照SST 文件本身的格式写入(basedtableformat)…
SST 文件细节
为了后续能够对compaction过程中的细节描述理解的足够透彻,本篇先对SST 文件的详细格式从源码层面做一个总结。关于SST文件内部数据存储格式,rocksdb默认的是Block-BasedTable format,关于SST文件存储格式为什么要进行一些独特的细节设计呢?
LSM tree保证了数据是有序写入(memtable – skiplist),提高了写性能,但是因为其本身的分层结构,牺牲了读性能(一个key如存储在了低级别的level,从上到下每一层都要进行查找,代价极大)。所以,针对读的性能提升有了很多的优化:bloom filter(高效判读一个key是否不存在),index-filter (二分查找,消耗低内存的情况下)索引key-value数据。这一些数据都需要存储在SST文件之中,用来进行k-v数据的有序管理。
话不多说,先上图(这个图是社区block-base table类型的SST文件格式概览图)
这个是一个SST文件的格式,可以看到有如下几个区域
- Footer
- meta index block
- meta block (多个)
- data block(多个)
具体每个block的作用先说一下:
- Footer 在当前版本 主要是用来索引 meta index block 和 index block
- meta index block 主要是为了索引列出的多个meta block
- index block是属于一种meta block,它是用来索引data block
- metablock除了上面说到底index block之外还有 filter block、range_del block,compression block,properties blockh这几种。其中filter block之前介绍 rocksdb提升读性能过程中已经说过了,用来保存一些bloom filter用来加速查找; range_del block是保存 客户端针对key有DeleteRange的操作而标记的一批key; compression block保存了通过字典压缩的key的前缀数据,也是为了加速读; properties block保存了当前SST文件内部的属性数据,像有多少个datablock,多少个index block,整个SST文件有多大…等等各个维度的数据。
2021.1.15更新
下文中之前存在一些block分布 的描述错误,这里给感兴趣的伙伴推荐一下rocksdb自带的sst_dump工具,能够非常直观得看到每一个sst文件内部的block分布 以及 完整的key-value数据。
以下数据为我打印出来的一个sst文件的数据,从Footer 到 metaindex block 的相关handle 以及 更底层的block细节都能一目了然得看到,大家先对sst文件做一个整体的概览。
可以通过图中的第一个 index block索引的data block偏移地址找到对应的datablock
可以看到index block的key和 datablock的起始key 差异还是比较大的,因为经过了snappy的压缩。
而如果没有开启压缩选项的话,实际的index block 的索引key 和data block 结束key还是有一些细节的处理,即
1号data block的结束key和2号datablock的其实key如果有公共前缀,则索引2号data block的index block 会保存这个公共前缀key作为自己的index key。
如下是我禁止掉压缩之后的sst文件数据内容。
Footer Details:
--------------------------------------
checksum: 1 # 做crc校验,校验失败则无法读取 footer,从而无法读取整个sst文件内容
metaindex handle: DEB5E03473 # meta index block
index handle: F0E3A034EBCB3F # index block
footer version: 2 # foot version,不同版本的rocksdb会支持较新的version
table_magic_number: 9863518390377041911
Metaindex Details:
--------------------------------------
Filter block handle: A6CBB033C59870
Properties block handle: 95B0E034C405
Range deletion block handle: E0AFE03430 #range tombstone block
Table Properties:
--------------------------------------
# data blocks: 27032
# entries: 919072
# deletions: 1
# merge operands: 0
# range deletions: 1
...
Index Details:
--------------------------------------
Block key hex dump: Data block handle
Block key ascii
HEX 746573745F67726170685F313030313133: 00AC1F
# 第一个index block的key,会选择下面两个datablock中,第一个datablock 的结束key
# 和第二个data block的起始key的公共前缀,还有结尾的数值处理。
#
# 比如 t e s t _ g r a p h _ 1 0 0 1 1 3
# datablock1 的结尾key: t e s t _ g r a p h _ 1 0 0 1 1 2 2 8 9 3
# datablcok2 的起始key: t e s t _ g r a p h _ 1 0 0 1 1 6 9 4 3 5
#
# 可以看到公共前缀是t e s t _ g r a p h _ 1 0 0 1 1,在公共前缀的下一位不同
# 从两个key的下一位中间随意取一个字符即可。
#
ASCII t e s t _ g r a p h _ 1 0 0 1 1 3
------
HEX 746573745F67726170685F313030323138: B11FAC1F
ASCII t e s t _ g r a p h _ 1 0 0 2 1 8
......
Range deletions:
--------------------------------------
HEX 746573745F67726170685F313030: 746573745F67726170685F31353030
# 下发的DeleteRange 接口的范围,作为一个block保存下来
ASCII t e s t _ g r a p h _ 1 0 0 : t e s t _ g r a p h _ 1 5 0 0
Data Block # 1 @ 00AC1F
--------------------------------------
HEX 746573745F67726170685F31303030303434323831: ......
ASCII t e s t _ g r a p h _ 1 0 0 0 0 4 4 2 8 1 : x x....
......
HEX 746573745F67726170685F31303031313232383933: ......
ASCII t e s t _ g r a p h _ 1 0 0 1 1 2 2 8 9 3 : ......
------
Data Block # 2 @ B11FAC1F
--------------------------------------
HEX 746573745F67726170685F31303031313639343335: ......
ASCII t e s t _ g r a p h _ 1 0 0 1 1 6 9 4 3 5 :......
......
2020.1.15 更新完毕
接下来详细介绍每一种存储格式,相关的源代码都是基于rocksdb-6.4.6 版本。
Footer
其中Footer 的结构主要是用来索引 metaindex block 和data index block,且还有一些魔数和版本号的存储,用来确认是否是rocksdb的footer结构。
详细的存储内容以及对应数据结构 预留的存储空间大小如下:
下面的图中有一条数据记录是padding,这个是如果前面的数据不足指定大小20B,剩余空间就填充0。
这里可以看到footer 在不同的version下面使用的是不同的 存储格式。
这里的较高版本的Footer中多了一个check_sum
的类型,主要是为了保证在创建一个新的sst文件的时候(compaction的output)时,旧的SST文件仍然能够提供读。
除了check_sum 字段,主要的两个字段我们之前也提到过是要能够索引 meta index 和 index 的字段,这里面是两个BlockHandle,可以看到每个blockhandle 中有两个数据结构:offset和size ,分别存放的是对应index的偏移地址和大小。
源码如下(format.cc Footer::EncodeTo):
// legacy footer format:
// metaindex handle (varint64 offset, varint64 size)
// index handle (varint64 offset, varint64 size)
// <padding> to make the total size 2 * BlockHandle::kMaxEncodedLength
// table_magic_number (8 bytes)
// new footer format:
// checksum type (char, 1 byte)
// metaindex handle (varint64 offset, varint64 size)
// index handle (varint64 offset, varint64 size)
// <padding> to make the total size 2 * BlockHandle::kMaxEncodedLength + 1
// footer version (4 bytes)
// table_magic_number (8 bytes)
void Footer::EncodeTo(std::string* dst) const {
assert(HasInitializedTableMagicNumber());
if (IsLegacyFooterFormat(table_magic_number())) {
// has to be default checksum with legacy footer
assert(checksum_ == kCRC32c);
const size_t original_size = dst->size();
metaindex_handle_.EncodeTo(dst);
index_handle_.EncodeTo(dst);
dst->resize(original_size + 2 * BlockHandle::kMaxEncodedLength); // Padding
PutFixed32(dst, static_cast<uint32_t>(table_magic_number() & 0xffffffffu));
PutFixed32(dst, static_cast<uint32_t>(table_magic_number() >> 32));
assert(dst->size() == original_size + kVersion0EncodedLength);
} else {
const size_t original_size = dst->size();
dst->push_back(static_cast<char>(checksum_));
metaindex_handle_.EncodeTo(dst);
index_handle_.EncodeTo(dst);
dst->resize(original_size + kNewVersionsEncodedLength - 12); // Padding
PutFixed32(dst, version());
PutFixed32(dst, static_cast<uint32_t>(table_magic_number() & 0xffffffffu));
PutFixed32(dst, static_cast<uint32_t>(table_magic_number() >> 32));
assert(dst->size() == original_size + kNewVersionsEncodedLength);
}
}
meta index block
meteindex block其实是一组block,保存了多个metablock的handle ,可以用来访问具体的metablock
实现源码如下(block_based_table_builder.cc):
函数为BlockBasedTableBuilder::Finish()
虽然上图中的indexblock 的相关handle信息的写入是在这里调用的函数,但实际是存放在footer中的(通过上文中sst_dump打印的sst文件数据能够比较清晰的看到各个block的数据分布)。
可以看到这里是将对应的metablock 相关信息写入到meta_index_builder之中,最后通过finish函数固化。
finish函数实现如下:
//将builer中的数据按照格式添加的meta_inex_block中
Slice MetaIndexBuilder::Finish() {
for (const auto& metablock : meta_block_handles_) {
meta_index_block_->Add(metablock.first, metablock.second);
}
return meta_index_block_->Finish();
}
// 生成index block的格式,作为参数由以上截图中的函数WriteRawBlock写入磁盘。
Slice BlockBuilder::Finish() {
// Append restart array
for (size_t i = 0; i < restarts_.size(); i++) {
PutFixed32(&buffer_, restarts_[i]);
}
uint32_t num_restarts = static_cast<uint32_t>(restarts_.size());
BlockBasedTableOptions::DataBlockIndexType index_type =
BlockBasedTableOptions::kDataBlockBinarySearch;
if (data_block_hash_index_builder_.Valid() &&
CurrentSizeEstimate() <= kMaxBlockSizeSupportedByHashIndex) {
data_block_hash_index_builder_.Finish(buffer_);
index_type = BlockBasedTableOptions::kDataBlockBinaryAndHash;
}
// footer is a packed format of data_block_index_type and num_restarts
uint32_t block_footer = PackIndexTypeAndNumRestarts(index_type, num_restarts);
PutFixed32(&buffer_, block_footer);
finished_ = true;
return Slice(buffer_);
}
filter meta block
这里的filter meta block主要是用来存储bloom filter相关的数据,格式如下:
filter可能有多个,每个对应一个data block,用来确认datablock中的key数据是否存在。它是在compaction过程中生成,会为每一个datablock增加一个对应的filter block和对应的index block。
最终通过WriteFilterBlock 编码到对应的meta_index_builder之中,同时在该过程中会为所有的filter block增加一个index,这个index包含了当前编码datablock的filterblock名称,每个fiterblock的偏移地址和大小。这个index会在最后添加到filterblock所对应的meta index block之中。
以上过程实现代码如下:
这段代码是在compaction过程中期,sub_compaction线程构建的Iterator,用来对参与compaction的key进行处理。
block_based_table_builder.cc EnterUnbuffered函数
void BlockBasedTableBuilder::EnterUnbuffered() {
...
/*针对每一个datablock,构建其filterblock和index block*/
for (size_t i = 0; ok() && i < r->data_block_and_keys_buffers.size(); ++i) {
const auto& data_block = r->data_block_and_keys_buffers[i].first;
auto& keys = r->data_block_and_keys_buffers[i].second;
assert(!data_block.empty());
assert(!keys.empty());
/*filter block的构建过程需要依据datablock中一个个key来进行*/
for (const auto& key : keys) {
if (r->filter_builder != nullptr) {
//这里只是创建存在于内存中的fitler_builder结构,且将key只是作为一个个string类型的entry保存起来
// 这个过程并未增加filter的一些算法处理,后续在WriteFilterBlock会使用当前构造好的entry通过一系列hash函数构造bloom filter功能。
r->filter_builder->Add(ExtractUserKey(key));
}
r->index_builder->OnKeyAdded(key);
}
WriteBlock(Slice(data_block), &r->pending_handle, true /* is_data_block */);
if (ok() && i + 1 < r->data_block_and_keys_buffers.size()) {
Slice first_key_in_next_block =
r->data_block_and_keys_buffers[i + 1].second.front();
Slice* first_key_in_next_block_ptr = &first_key_in_next_block;
r->index_builder->AddIndexEntry(&keys.back(), first_key_in_next_block_ptr,
r->pending_handle);
}
}
r->data_block_and_keys_buffers.clear();
}
block_based_table_builder.cc WriteFilterBlock函数
将之前收集的key的数据进行整合,按照filter本身应有的格式进行构建,并格式化到metaindex builer之中
void BlockBasedTableBuilder::WriteFilterBlock(
MetaIndexBuilder* meta_index_builder) {
BlockHandle filter_block_handle;
bool empty_filter_block = (rep_->filter_builder == nullptr ||
rep_->filter_builder->NumAdded() == 0);
if (ok() && !empty_filter_block) {
Status s = Status::Incomplete();
while (ok() && s.IsIncomplete()) {
// Finish函数 通过filter_builder中的key数据 完成多次filter content的构建,一下步骤是循环进行,直到builder数据为空
// 步骤包括:
// 1.从之前添加的key/prefix key的entries 取出数据
// 2. 针对每一条entries 通过对应filter策略(目前有full和partition两种)的hash函数
// 映射出一个能表示该key是否存在的一个值,编码到 filter之中
// 3. 清除临时取出来的entries
// 4. 将建立好的fitler的偏移量写入到filter handle之中
Slice filter_content =
rep_->filter_builder->Finish(filter_block_handle, &s);
assert(s.ok() || s.IsIncomplete());
rep_->props.filter_size += filter_content.size();
WriteRawBlock(filter_content, kNoCompression, &filter_block_handle);
}
}
// 完成之后将 filter的类型以及策略名称组合成一个key,和filter_block_handle一起添加到meta_index_builder之中
// 用来一起创建索引
if (ok() && !empty_filter_block) {
// Add mapping from "<filter_block_prefix>.Name" to location
// of filter data.
std::string key;
if (rep_->filter_builder->IsBlockBased()) {
key = BlockBasedTable::kFilterBlockPrefix;
} else {
key = rep_->table_options.partition_filters
? BlockBasedTable::kPartitionedFilterBlockPrefix
: BlockBasedTable::kFullFilterBlockPrefix;
}
key.append(rep_->table_options.filter_policy->Name());
meta_index_builder->Add(key, filter_block_handle);
}
}
以上详细的Finish函数的实现是在:block_based_filter_builder.cc 之中,其中还包括通过指定的hash函数创建bloom filter的过程。这里bloom filter的实现就不多说了,网络上很多人已经讲的很明白了。总之就是 能够100%确认一个key 不在当前data block之中,而概率性确认一个key存在。
大体过程如下:
- 初始化 每一个方格表示一个bit 位,代表一个字符(实际可能代表更多的字符)是否存在,初始值都为0
- Put: fat,通过hash函数(实际情况会更复杂,这里是直接将字符串对应的字母映射到对应的bit表示的字母之上)映射输入的数据到具体的bit位上,并将对应的bit位置为1
- 再次Put:end ,同第二步的映射结果如下:
此时如果我们想要在以上已有的filter之中查找eat字符串,发现e a t对应的bit位都已经被置为1了,但是本身这个字符串并不在filter对应的底层存储之中。
但是如果判断一个duck 的字符串是否存在,只要有一位不是1,那这个字符串肯定就不存在了。
所以bloom filter主要还是确认一个字符串不存在。时间复杂度是O(k),k代表输入的key的长度。
index meta block
上面在介绍filter block的时候对 index block也做了一个简单的描述,上面的block_based_table_builder.cc EnterUnbuffered函数中index block的添加和filter block是在一起的,filter 是为了过滤不存在的key,而index block则是为了加速查找key。所以,这里针对每一个data block也会创建一个index block,保存这个data block 的key的范围。
在EnterUnbuffered 也是保存一些index block所需要的key数据的enties信息。
index block的数据存储格式如下:
这里简单通过图来介绍一下 index的格式(这里的index_type是kBinarySearchWithFirstKey):
-
index_block 的存储格式是如上图左下角的形态,有多个1 level的index block和1个 2level的index block。 2level的index block用来索引 1 level 的index lock。
-
具体的1 level中的存储结构如 下部分:
以上只列举出了一个restart_point,一个1 level的index block包含多个restart_point,间隔通过index_block_restart_interval
默认1B控制,即下一个restart 距离上一个restart 间隔多少字节的偏移。一个index block的大小默认是4KBrestart_point内部的每一条record都记录的是一个类似于k-v的数据存储结构,key是data_block中的第一个key,value是当前索引的datablock 的偏移地址,大小,以及保存一个裁剪后的key(first_key),其表示当前比data_block的最后一个key小,但又比下一个data_block的起始key大 的一个前缀key。
以上的编码过程是通过AddIndexEntry实现,也就是在block_based_table_builder.cc EnterUnbuffered函数中,添加完filter_block之后,添加index_entry
for (size_t i = 0; ok() && i < r->data_block_and_keys_buffers.size(); ++i) {
/*添加filter block entry*/
......
WriteBlock(Slice(data_block), &r->pending_handle, true /* is_data_block */);
if (ok() && i + 1 < r->data_block_and_keys_buffers.size()) {
Slice first_key_in_next_block =
r->data_block_and_keys_buffers[i + 1].second.front();
Slice* first_key_in_next_block_ptr = &first_key_in_next_block;
r->index_builder->AddIndexEntry(&keys.back(), first_key_in_next_block_ptr,
r->pending_handle);
}
}
后续统一通过WriteIndexBlock函数写入到存储之中,并添加索引信息到meta_index_builder之中
void BlockBasedTableBuilder::WriteIndexBlock(
MetaIndexBuilder* meta_index_builder, BlockHandle* index_block_handle) {
IndexBuilder::IndexBlocks index_blocks;
auto index_builder_status = rep_->index_builder->Finish(&index_blocks);
if (index_builder_status.IsIncomplete()) {
// We we have more than one index partition then meta_blocks are not
// supported for the index. Currently meta_blocks are used only by
// HashIndexBuilder which is not multi-partition.
assert(index_blocks.meta_blocks.empty());
} else if (ok() && !index_builder_status.ok()) {
rep_->status = index_builder_status;
}
if (ok()) {
for (const auto& item : index_blocks.meta_blocks) {
BlockHandle block_handle;
WriteBlock(item.second, &block_handle, false /* is_data_block */);
if (!ok()) {
break;
}
meta_index_builder->Add(item.first, block_handle);
}
}
......
}
Compression Dict meta Block
这个block是字典压缩block,这个数据结构同样是在EnterUnbuffered 函数之中进行数据区域的创建的,这个部分的是为了节约datablock/filterblock/indexblock的存储空间而 设置的一个针对key的字典压缩后的数据存放区域。主要通过参数compression_opts.enble,compression_opts.max_dict_bytes ,compression_opts.strategy等
相关compression参数进行配置。
当前支持四种类型的字典压缩算法:
kZlibCompression, kLZ4Compression, kLZ4HCCompression, 和 kZSTDNotFinalCompression
可以通过strategy来进行配置。
大多数情况下,只有当key-value数据写入到了最后一层的时候才会开始进行压缩,且压缩的对象是SST文件最大的而且其中key-value数据最为稳定。
void BlockBasedTableBuilder::EnterUnbuffered() {
Rep* r = rep_;
assert(r->state == Rep::State::kBuffered);
r->state = Rep::State::kUnbuffered;
const size_t kSampleBytes = r->compression_opts.zstd_max_train_bytes > 0
? r->compression_opts.zstd_max_train_bytes
: r->compression_opts.max_dict_bytes;
Random64 generator{r->creation_time};
std::string compression_dict_samples;
std::vector<size_t> compression_dict_sample_lens;
if (!r->data_block_and_keys_buffers.empty()) {
while (compression_dict_samples.size() < kSampleBytes) {
size_t rand_idx =
static_cast<size_t>(
generator.Uniform(r->data_block_and_keys_buffers.size()));
size_t copy_len =
std::min(kSampleBytes - compression_dict_samples.size(),
r->data_block_and_keys_buffers[rand_idx].first.size());
compression_dict_samples.append(
r->data_block_and_keys_buffers[rand_idx].first, 0, copy_len);
compression_dict_sample_lens.emplace_back(copy_len);
}
}
// final data block flushed, now we can generate dictionary from the samples.
// OK if compression_dict_samples is empty, we'll just get empty dictionary.
std::string dict;
if (r->compression_opts.zstd_max_train_bytes > 0) {
dict = ZSTD_TrainDictionary(compression_dict_samples,
compression_dict_sample_lens,
r->compression_opts.max_dict_bytes);
} else {
dict = std::move(compression_dict_samples);
}
r->compression_dict.reset(new CompressionDict(dict, r->compression_type,
r->compression_opts.level));
r->verify_dict.reset(new UncompressionDict(
dict, r->compression_type == kZSTD ||
r->compression_type == kZSTDNotFinalCompression));
......
/*借用压缩后的key的信息, 来构造filter entry和index entry*/
最后通过WriteCompressionDictBlock 函数对最终的压缩数据进行整合固化到meta_index_builder之中。
void BlockBasedTableBuilder::WriteCompressionDictBlock(
MetaIndexBuilder* meta_index_builder) {
if (rep_->compression_dict != nullptr &&
rep_->compression_dict->GetRawDict().size()) {
BlockHandle compression_dict_block_handle;
if (ok()) {
WriteRawBlock(rep_->compression_dict->GetRawDict(), kNoCompression,
&compression_dict_block_handle);
#ifndef NDEBUG
Slice compression_dict = rep_->compression_dict->GetRawDict();
TEST_SYNC_POINT_CALLBACK(
"BlockBasedTableBuilder::WriteCompressionDictBlock:RawDict",
&compression_dict);
#endif // NDEBUG
}
if (ok()) {
meta_index_builder->Add(kCompressionDictBlock,
compression_dict_block_handle);
}
}
}
Range del meta Block
Range delete block的数据保存的是上层客户端下发的接口DeleteRange
中处于当前compaction文件中的keys以及key对应的sequence num。
这里为什么不能将range del 的k-v数据和datablock集成到一块呢?这是因为如果放到datablock中就无法对range del 的key进行二分查找了,从而无法快速判断一个key是否处于rangedel而对客户端的Get相关操作作出对应的反馈。
Range del 相关的数据获取时机是在compaction 的 ProcessKeyValueCompaction
过程中,最后的组合格式仍然还是一个标准的block存储方式。
- user key: range 的起始key
- sequence number: range deletion操作写入db的时候会给一个seq num表示这个key在db内部的唯一性。
- value type: kTypeRangeDeletion 这个是当前key的操作类型,表示是处于range delete之间的操作。除了这个操作之外,rocksdb的value type还有很多,包括kValueType(Put接口下发),kDeletion,
- value: range的结束key
最后通过函数进行固化,并添加到meta_index_builder之中
void BlockBasedTableBuilder::WriteRangeDelBlock(
MetaIndexBuilder* meta_index_builder) {
if (ok() && !rep_->range_del_block.empty()) {
BlockHandle range_del_block_handle;
// 写入之前,先通过Finish函数 对range_del_block数据进行格式的封装。
WriteRawBlock(rep_->range_del_block.Finish(), kNoCompression,
&range_del_block_handle);
meta_index_builder->Add(kRangeDelBlock, range_del_block_handle);
}
}
Properties meta block
这个meta block我们之前也说过,保存了一些当前SST文件的属性信息,同时也包括其他的各个block属性数据。
属性信息的更新是在compaction的各个阶段伴随着各个block创建维护而更新的。
实现如下(具体项就不说了,函数中的变量名称已经描写的很清楚了,同时也可以使用sst_dump
指定db ,加上-show_properties 选项也能看到完整的打印信息):
void BlockBasedTableBuilder::WritePropertiesBlock(
MetaIndexBuilder* meta_index_builder) {
BlockHandle properties_block_handle;
if (ok()) {
PropertyBlockBuilder property_block_builder;
rep_->props.column_family_id = rep_->column_family_id;
rep_->props.column_family_name = rep_->column_family_name;
rep_->props.filter_policy_name =
rep_->table_options.filter_policy != nullptr
? rep_->table_options.filter_policy->Name()
: "";
rep_->props.index_size =
rep_->index_builder->IndexSize() + kBlockTrailerSize;
rep_->props.comparator_name = rep_->ioptions.user_comparator != nullptr
? rep_->ioptions.user_comparator->Name()
: "nullptr";
rep_->props.merge_operator_name =
rep_->ioptions.merge_operator != nullptr
? rep_->ioptions.merge_operator->Name()
: "nullptr";
rep_->props.compression_name =
CompressionTypeToString(rep_->compression_type);
rep_->props.compression_options =
CompressionOptionsToString(rep_->compression_opts);
rep_->props.prefix_extractor_name =
rep_->moptions.prefix_extractor != nullptr
? rep_->moptions.prefix_extractor->Name()
: "nullptr";
......
data block 详细格式 及实现
终于到了我们最后的实际存储key-value数据的部分,整个SST文件的设计可以说环环相扣,很严谨也很巧妙。
data block的存储格式如下:
一个data block中会存储多个record,每个record保存一个key-value数据。record之中按照上图detail 后面的格式进行数据的保存,这里说一下共享key,我们存储到datablock中的key-value数据都是按照key有序的,一般key都是字符串的形态。所以前一个record中的key 可能会和后面record 之中的key有公共前缀。类似于 record1的key:abcde和 record2的key:abcdh,这里的abcd就是共享key部分。
在record之后存储的是restart的点,这里restart的意思上面也说了,当共享key的长度为0 的时候当前record的偏移地址会被记为一个restart点。
为什么会有restart这样的uinit类型的存储结构呢?核心还是为了加速k-v查找,两个restart 点之间的record都是有公共前缀的,可以通过restart点快速在一个datablock中定位到存放key-value的record。
此外,还有一点可以看到rocksdb的数据存储,key和value是存放到一块的,也就是我们只要找到了key就能够找到对应的value。
触发写datablock的时机是在compaction最后一个阶段,固化key-value数据到output的文件之中的时候,会调用函数Status BlockBasedTableBuilder::Finish() 进行table builder结构的创建并按照各个block格式固化到SST文件之中。
而datablock就是在刚开始就会被Flush到存储中,过程中涉及到针对datablock的一些压缩和解压缩的过程,详细的步骤大家可以看看下面的实现。
源码实现如下:
void BlockBasedTableBuilder::Flush() {
Rep* r = rep_;
assert(rep_->state != Rep::State::kClosed);
if (!ok()) return;
if (r->data_block.empty()) return;
WriteBlock(&r->data_block, &r->pending_handle, true /* is_data_block */);
}
void BlockBasedTableBuilder::WriteBlock(BlockBuilder* block,
BlockHandle* handle,
bool is_data_block) {
WriteBlock(block->Finish(), handle, is_data_block);
block->Reset();
}
// 最终执行是通过如下函数执行
void BlockBasedTableBuilder::WriteBlock(const Slice& raw_block_contents,
BlockHandle* handle,
bool is_data_block) {
// File format contains a sequence of blocks where each block has:
// block_data: uint8[n]
// type: uint8
// crc: uint32
assert(ok());
Rep* r = rep_;
auto type = r->compression_type;
uint64_t sample_for_compression = r->sample_for_compression;
Slice block_contents;
bool abort_compression = false;
StopWatchNano timer(
r->ioptions.env,
ShouldReportDetailedTime(r->ioptions.env, r->ioptions.statistics));
......
}
总结
到此我们对整个SST文件的格式就有了一个较为全面的了解了,为了适配LSM tree的存储方式,加速读,拥有良好的可维护和可扩展性(上面的metablock类型可以持续增加),当然也存在一定的复杂度,需要结合社区给出的wiki设计文档结合详细的源码实现来分析。
当然rocksdb本身也提供了一些工具来查看底层sst文件的结构sst_dump
,这个工具的详细用法可以参考sst_dump,编译完rocksdb的tools就可以看到这个工具了。
当我们对整个SST文件的详细存储格式有了了解之后,接下来的compaction就轻松有趣多了。
下期见~