RocksDB Compaction(二)源码分析

        本文的主要目的是(1)了解RocksDB源码中Flush和Compaction的基本流程(2)了解Compaction/FLush过程中是在何处、如何产生I/O的。

目录

线程调度过程&compaction流程:

1. 触发和调度        

2. compaction流程及读写I/O

DIRECT_IO

RocksDB的compaction 和 flush 的触发机制补充

IO_URING


        RocksDB的源码由C++撰写而且代码量非常巨大,程序调用栈很复杂。在学习过程中发现这篇文章写得非常详细透彻,放上链接。Rocksdb Compaction源码详解(二):Compaction 完整实现过程 概览_天行健,地势坤-CSDN博客

线程调度过程&compaction流程:

1. 触发和调度        

        db_impl_compaction_flush.cc中具有MaybeScheduleFlushOrCompaction()函数,它常与SchedulePendingCompaction()一起出现,在RocksDB变更SuperVersion(增加memtable,增加sst,compaction)时调用。

  // Whenever we install new SuperVersion, we might need to issue new flushes or
  // compactions.
  SchedulePendingCompaction(cfd);
  MaybeScheduleFlushOrCompaction();

        SchedulePendingCompaction()函数中调用NeedsCompaction()判断一个cf有无需要compaction的level,将新增的column family加入等待队列。

// compaction调度的触发条件
bool LevelCompactionPicker::NeedsCompaction(
    const VersionStorageInfo* vstorage) const {
  // 有超时的sst
  if (!vstorage->ExpiredTtlFiles().empty()) {
    return true;
  }
  // 定期compaction
  if (!vstorage->FilesMarkedForPeriodicCompaction().empty()) {
    return true;
  }
  if (!vstorage->BottommostFilesMarkedForCompaction().empty()) {
    return true;
  }
  if (!vstorage->FilesMarkedForCompaction().empty()) {
    return true;
  }
  // 依次判断每个sst的score,score >= 1则加入队列
  for (int i = 0; i <= vstorage->MaxInputLevel(); i++) {
    if (vstorage->CompactionScore(i) >= 1) {
      return true;
    }
  }
  return false;
}

        随后MaybeScheduleFlushOrCompaction() 中,Schedule()从线程池中取出一个线程,绑定工作函数(DBImpl::BGWorkCompaction())、参数等,执行线程。compaction的线程优先级为LOW。

        DBImpl::BGWorkCompaction()中又经过 DBImpl::BackgroundCallCompaction() 调用进入  DBImpl::BackgroundCompaction()中,在这里创建compaction_job类。该类则是执行compaction的工作类,首先compaction_job.Prepare()计算并准备好每个subcompactoion的边界。然后执行compaction_job.Run()执行compaction。到这里,Flush的触发和调度和Compaction基本一致,函数名也只是Flush和Compaction的区别。

    compaction_job.Prepare();

    NotifyOnCompactionBegin(c->column_family_data(), c.get(), status,
                            compaction_job_stats, job_context->job_id);
    mutex_.Unlock();
    TEST_SYNC_POINT_CALLBACK(
        "DBImpl::BackgroundCompaction:NonTrivial:BeforeRun", nullptr);
    // 调用run()开始执行compaction
    // Should handle erorr?
    compaction_job.Run().PermitUncheckedError();
    TEST_SYNC_POINT("DBImpl::BackgroundCompaction:NonTrivial:AfterRun");
    mutex_.Lock();

        进入Run()中,compaction被切割成若干sub-compaction,创建线程池,主线程运行sub-compaction[0],线程池中的子线程并行运行其余sub-compaction。等待子线程全部结束后,主线程结束。

// Launch a thread for each of subcompactions 1...num_threads-1
  std::vector<port::Thread> thread_pool;
  thread_pool.reserve(num_threads - 1);
  for (size_t i = 1; i < compact_->sub_compact_states.size(); i++) {
    thread_pool.emplace_back(&CompactionJob::ProcessKeyValueCompaction, this,
                             &compact_->sub_compact_states[i]);
  }

  // Always schedule the first subcompaction (whether or not there are also
  // others) in the current thread to be efficient with resources
  ProcessKeyValueCompaction(&compact_->sub_compact_states[0]);

  // Wait for all other threads (if there are any) to finish execution
  for (auto& thread : thread_pool) {
    thread.join();
  }

        到这里的CompactionJob::ProcessKeyValueCompaction()才是实际处理一个sub-compaction,包含了读取数据,归并排序,并写入底层文件中。

2. compaction流程及读写I/O

(1)进入ProcessKeyValueCompaction()函数后,首先生成kv数据的迭代器。通过MakeInputIterator()函数创建InternalIterator。对磁盘中文件的操作也是在该函数中进行的。

 // Although the v2 aggregator is what the level iterator(s) know about,
  // the AddTombstones calls will be propagated down to the v1 aggregator.
  std::unique_ptr<InternalIterator> raw_input(
      versions_->MakeInputIterator(read_options, sub_compact->compaction,
                                   &range_del_agg, file_options_for_read_));
  InternalIterator* input = raw_input.get();

        MakeInputIterator()函数的大致流程是先将文件中的kv数据读取到一个或若干个迭代器中,然后将这些迭代器使用堆排序的方式,合并成最终的一个迭代器。

        compaction读io函数调用路线:ProcessKeyValueCompaction() → VersionSet::MakeInputIterator() → TableCache::NewIterator() → TableCache::FindTable() → TableCache::GetTableReader() → FileSystem::NewRandomAccessFile() → PosixRandomAccessFile::Read()。函数调用栈非常的深,最后的读操作在文件io_posix.cc中。

r = pread(fd_, ptr, left, static_cast<off_t>(offset));

(2)在制作迭代器时,进行merge操作(这里暂未深入了解,有待继续学习)。

(3)在完成merge后,将迭代器中的kv数据依次放入线程绑定的table builder中,然后进行写入。

        compaction写io入口:compaction_job.cc: s = sub_compact->builder->Finish();

        flush写io入口: flush_job.c: FlushJob::WriteLevel0Table() → s = BuildTable(...) → s = builder->Finish();

        写入逻辑: 构建sst使用table_builder类(有多种,默认BlockBasedTableBuilder),写入文件使用WritableFileWriter类。例如compaction中,每个sub_compaction都会绑定一个builder和一个writer。无论是Compaction还是Flush,都是通过table builder构造SST文件,然后调用builder.finish()函数使用Wirter完成写入。看源码,默认的BlockBasedTableBuilder::Finish()如下:

Flush();
  ......
  // Write meta blocks, metaindex block and footer in the following order.
  //    1. [meta block: filter]
  //    2. [meta block: index]
  //    3. [meta block: compression dictionary]
  //    4. [meta block: range deletion tombstone]
  //    5. [meta block: properties]
  //    6. [metaindex block]
  //    7. Footer
  BlockHandle metaindex_block_handle, index_block_handle;
  MetaIndexBuilder meta_index_builder;
  WriteFilterBlock(&meta_index_builder);
  WriteIndexBlock(&meta_index_builder, &index_block_handle);
  WriteCompressionDictBlock(&meta_index_builder);
  WriteRangeDelBlock(&meta_index_builder);
  WritePropertiesBlock(&meta_index_builder);
  if (ok()) {
    // flush the meta index block
    WriteRawBlock(meta_index_builder.Finish(), kNoCompression,
                  &metaindex_block_handle);
  }
  if (ok()) {
    WriteFooter(metaindex_block_handle, index_block_handle);
  }

        可见Finish()函数分别将SST文件的各个部分依次写入,首先Flush()写入data block部分,然后使用若干WriteXxxxx()函数完成各种元数据block的写入。        

        各种WriteXxxxx()函数会触发(BlockBasedTableBuilder::WriteBlock() →)BlockBasedTableBuilder::WriteRawBlock() → WritableFileWriter::Append() (→ WritableFileWriter::Flush() ) → WritableFileWriter::WriteBuffered() → PosixWritableFile::Append() → PosixWrite()。即通过线程绑定的FileWriter类进行数据写入,最终调用PosixWrite()函数来进行文件write操作。

bool PosixWrite(int fd, const char* buf, size_t nbyte) {
  const size_t kLimit1Gb = 1UL << 30;

  const char* src = buf;
  size_t left = nbyte;

  while (left != 0) {
    size_t bytes_to_write = std::min(left, kLimit1Gb);

    ssize_t done = write(fd, src, bytes_to_write);
    // printf("%d: write: src = %p, len = %ld\n", fd, src, bytes_to_write);
    if (done < 0) {
      if (errno == EINTR) {
        continue;
      }
      return false;
    }
    left -= done;
    src += done;
  }
  return true;
}

        

DIRECT_IO

        Rocksdb支持使用direct io,跳过操作系统的page cache,需要将use_direct_reads设置为true,该值默认为false。

        rocksdb具有自实现的block cache,使用direct io时,block cache可以取代操作系统的page cache。

compaction 和 flush 触发机制补充

        默认后台线程数:flush和compaction各为1。官方推荐的基准值分别为4、2。

max_flush_jobs = max_background / 4

max_compaction_jobs = max_background_jobs - max_flush_jobs

(max_background_jobs默认值为2)

        详细了解flush流程可以参阅:

MySQL · RocksDB · Memtable flush分析_oldbalck的博客-CSDN博客

        level大小阈值计算:

        一个level的score大于1时,被视作需要compaction,score由函数void VersionStorageInfo::ComputeCompactionScore()计算。计算逻辑如下:

        L0:score = 未正在进行compaction的文件个数/level0_file_num_compaction_trigger,明显这个值应该是预设的。

score = static_cast<double>(num_sorted_runs) /
                mutable_cf_options.level0_file_num_compaction_trigger;

        Lk(k>1):score = 未正在进行compaction的文件总大小/MaxBytesForLevel。

score = static_cast<double>(level_bytes_no_compacting) /
              MaxBytesForLevel(level);

        这里的MaxBytesForLevel(level),即为每个level的最大大小,使用std::vector<uint64_t> level_max_bytes_存放,其计算方式有两种:若level_compaction_dynamic_level_bytes为false,则直接计算出固定值;若level_compaction_dynamic_level_bytes为true,则会动态计算每层的最大大小,目的是为了LSM树在密集的IO压力下,仍然能保持合理的树形结构,其计算方式为:

  1. 找到当前树形结构数据量最多的一层,作为Target_Size(Ln)

  2. 通过公式Target_Size(Ln-1) = Target_Size(Ln) / max_bytes_for_level_multiplier递推之前的level大小

IO_URING

rocksdb中iouring支持:rocksdb仅在MultiGet() API(用以接受一批key)中实现了iouring支持,集成在PosixRandomAccessFile::MultiRead中。也就是说,普通的读写和compaction是不使用iouring的。

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值