文章目录

  • 前言
  • comapction流程概述
  • SST 文件细节
    • Footer
    • meta index block
    • filter meta block
    • index meta block
    • Compression Dict meta Block
    • Range del meta Block
    • Properties meta block
    • data block 详细格式 及实现
  • 总结

前言


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的作用先说一下:

  1. Footer 在当前版本 主要是用来索引 meta index block 和 index block
  2. meta index block 主要是为了索引列出的多个meta block
  3. index block是属于一种meta block,它是用来索引data block
  4. 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 blockindex handle: F0E3A034EBCB3F # index blockfooter version: 2 # foot version,不同版本的rocksdb会支持较新的versiontable_magic_number: 9863518390377041911Metaindex Details:
--------------------------------------Filter block handle: A6CBB033C59870Properties block handle: 95B0E034C405Range deletion block handle: E0AFE03430 #range tombstone blockTable Properties:
--------------------------------------# data blocks: 27032# entries: 919072# deletions: 1# merge operands: 0# range deletions: 1...Index Details:
--------------------------------------Block key hex dump: Data block handleBlock key asciiHEX    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: B11FAC1FASCII  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 footerassert(checksum_ == kCRC32c);const size_t original_size = dst->size();metaindex_handle_.EncodeTo(dst);index_handle_.EncodeTo(dst);dst->resize(original_size + 2 * BlockHandle::kMaxEncodedLength);  // PaddingPutFixed32(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);  // PaddingPutFixed32(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 arrayfor (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_restartsuint32_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存在。
大体过程如下:

  1. 初始化 每一个方格表示一个bit 位,代表一个字符(实际可能代表更多的字符)是否存在,初始值都为0
  2. Put: fat,通过hash函数(实际情况会更复杂,这里是直接将字符串对应的字母映射到对应的bit表示的字母之上)映射输入的数据到具体的bit位上,并将对应的bit位置为1
  3. 再次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的大小默认是4KB

    restart_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 NDEBUGSlice 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存储方式。

  1. user key: range 的起始key
  2. sequence number: range deletion操作写入db的时候会给一个seq num表示这个key在db内部的唯一性。
  3. value type: kTypeRangeDeletion 这个是当前key的操作类型,表示是处于range delete之间的操作。除了这个操作之外,rocksdb的value type还有很多,包括kValueType(Put接口下发),kDeletion,
  4. 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: uint32assert(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就轻松有趣多了。

下期见~

Rocksdb Compaction 源码详解(一):SST文件详细格式源码解析相关推荐

  1. vue 源码详解(零):Vue 源码流程图

    vue 源码详解(零):Vue 源码流程图 最近在研究 Vue 的源码, 整理博客, 结果想到的.看到的内容实在是太多了, 不知道从何写起, 故整理了一个大致的流程图,根据这个顺序进行一一整理. 为了 ...

  2. docker镜像指定安装源_详解如何修改docker pull镜像源

    Docker Hub Mirror 为全球最大的Docker Registry(Docker Hub)提供在中国的镜像代理服务.Docker Hub Mirror会为中国的用户在国内的服务器上缓存诸多 ...

  3. Rocksdb Compaction源码详解(二):Compaction 完整实现过程 概览

    文章目录 1. 摘要 2. Compaction 概述 3. 实现 3.1 Prepare keys 过程 3.1.1 compaction触发的条件 3.1.2 compaction 的文件筛选过程 ...

  4. xuelipay 个人即时到账收款平台 原理及源码详解 支持支付宝微信

    现状及解决原理 1.1 现状 1.2 原理 1.3 例子 收款到完成收款过程源码详解 1 上传账户的收款码 2.2 创建订单 2.3 手机app 监听 2.4 服务器处理付款完成通知 漏单原理及漏单的 ...

  5. 【Live555】live555源码详解(九):ServerMediaSession、ServerMediaSubsession、live555MediaServer

    [Live555]live555源码详解系列笔记 继承协作关系图 下面红色表示本博客将要介绍的三个类所在的位置: ServerMediaSession.ServerMediaSubsession.Dy ...

  6. 【Live555】live555源码详解系列笔记

    [Live555]liveMedia下载.配置.编译.安装.基本概念 [Live555]live555源码详解(一):BasicUsageEnvironment.UsageEnvironment [L ...

  7. 【Live555】live555源码详解(八):testRTSPClient

    [Live555]live555源码详解系列笔记 继承协作关系图 下面红色表示本博客将要介绍的testRTSPClient实现的三个类所在的位置: ourRTSPClient.StreamClient ...

  8. 【Live555】live555源码详解(七):GenericMediaServer、RTSPServer、RTSPClient

    [Live555]live555源码详解系列笔记 继承协作关系图 下面红色表示本博客将要介绍的三个类所在的位置: GenericMediaServer.RTSPServer.RTSPClient 14 ...

  9. 【Live555】live555源码详解(六):FramedSource、RTPSource、RTPSink

    [Live555]live555源码详解系列笔记 继承协作关系图 下面红色表示本博客将要介绍的三个类所在的位置: FramedSource.RTPSource.RTPSink 11.FramedSou ...

最新文章

  1. 【每日一算法】二叉搜索树结点最小距离
  2. DeepMind的新强化学习系统是迈向通用AI的下一步吗?
  3. word公式和文字不在一行上,错位了如何解决
  4. BOOST_TEST_TRAIT_SAME的用法实例
  5. linux snmpwalk版本,snmpwalk的Linux的击不返回
  6. 调整窗口大小时进行页面刷新(设定定时器)
  7. ES6 Reflect使用笔记
  8. Jquery页面跳转
  9. C# MD5 加密算法
  10. AcWing 885. 求组合数 I(递推式预处理)
  11. 精选| 2019年7月R新包推荐(第32期)
  12. kettle使用数据库来生成序列_kettle 生成 ktr
  13. canvas对象arcTo函数的使用-遁地龙卷风
  14. PS 2022,PR 2018,AE 2017【百度网盘链接,没套路】
  15. 借助百度识图爬取数据集
  16. 新零售企业构建智慧营销体系
  17. ubuntu系统下,Firefox火狐浏览器播放网页视频失败,显示未安装视频插件
  18. (10万+浏览量)语句覆盖、条件覆盖(分支覆盖)、判定覆盖、条件-判定覆盖、组合覆盖、路径覆盖 的区别
  19. Krita源码分析(一)——项目结构
  20. 计算机毕业设计ssm文学阅读平台

热门文章

  1. LeetCode 力扣 算法题解 1109. 航班预订统计(Corporate Flight Bookings) n 个航班,它们分别从 1 到 n 进行编号,请返回每个航班预定的座位总数。
  2. DTOJ 1486:分数(score)
  3. 选手通过评委打分,输出名次问题
  4. 软件 互操作性测试,软件兼容性测试与互操作性测试辩析
  5. 《Editing Text in the wild》学习笔记
  6. PD866EZ-12D/YCZ多用户预付费电表 上传至西安市能耗平台
  7. 小白学python.1
  8. JAVA SE面试题(全)
  9. 国内大陆有哪些芯片公司处于世界前10?一起看看!
  10. 每日C语言代码(The fourth day)——冒泡排序与地址传递