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

前言


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 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存在。
大体过程如下:

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

  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: 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就轻松有趣多了。

下期见~

  • 16
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 14
    评论
Loki是一个开源的分布式日志聚合系统,它可以将多个不同来源的日志数据聚合到一个统一的地方,并提供了强大的搜索和查询功能。Loki的配置文件包含了Loki的所有配置信息,下面是对Loki配置文件详细解释: 1. server段 server段包含了Loki服务器的配置信息,包括监听地址和端口、存储位置、日志级别等。 示例: ``` server: http_listen_port: 3100 grpc_listen_port: 9095 log_level: info config: auth_enabled: false server: http_listen_port: 3100 grpc_listen_port: 9095 ``` 2. schema_config段 schema_config段用于配置Loki的日志格式,包括日志标签和日志字段。可以使用正则表达式来匹配日志标签和字段。 示例: ``` schema_config: configs: - from: 2022-01-01 store: boltdb object_store: filesystem schema: v11 index: prefix: index_ period: 24h retention_policy: 24h max_look_back_period: 0s storage_config: boltdb: directory: /var/lib/loki/boltdb filesystem: directory: /var/lib/loki/chunks ``` 3. storage_config段 storage_config段用于配置Loki的存储方式,包括数据存储路径、存储类型等。可以使用多种存储类型,如local、s3、gcs等。 示例: ``` storage_config: boltdb: directory: /var/lib/loki/boltdb filesystem: directory: /var/lib/loki/chunks ``` 4. ingester段 ingester段用于配置Loki的数据输入方式,包括文件输入、stdin输入、http输入等。可以配置多个输入方式。 示例: ``` ingester: lifecycler: address: 127.0.0.1 ring: kvstore: store: inmemory replication_factor: 1 heartbeat_timeout: 1m election_timeout: 10s retry_period: 5s startup_timeout: 5s chunk_idle_period: 5m chunk_retain_period: 30m max_transfer_retries: 0 fail_on_unhandled_request: false client: url: http://localhost:3100/api/prom/push ``` 5. querier段 querier段用于配置Loki的查询方式,包括查询语法、查询参数等。可以配置多个查询方式。 示例: ``` querier: query_range: max_samples: 50000000 query: max_concurrent: 20 cache: cache: max_size_items: 0 index: max_size_bytes: 0 ``` 6. compactor段 compactor段用于配置Loki的压缩方式,包括压缩周期、压缩方式等。可以配置多个压缩方式。 示例: ``` compactor: working_directory: /var/lib/loki/compactor shared_store: filesystem shared_storage_config: filesystem: directory: /var/lib/loki/compactor compaction: working_directory: /var/lib/loki/compactor shared_store: filesystem shared_storage_config: filesystem: directory: /var/lib/loki/compactor compaction_window: 1h ``` 7. ruler段 ruler段用于配置Loki的告警规则,包括告警条件、告警方式等。可以配置多个告警规则。 示例: ``` ruler: storage: type: local local: directory: /etc/loki/rules rule_path: /etc/loki/rules alertmanager_url: http://localhost:9093 ring: kvstore: store: inmemory replication_factor: 1 heartbeat_timeout: 1m election_timeout: 10s retry_period: 5s startup_timeout: 5s ``` 这些是Loki配置文件的主要部分,通过配置文件可以自定义Loki的各种配置项,满足不同的需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值