RocksDB Compaction(二)源码分析

本文详细探讨了RocksDB中Flush和Compaction的触发及执行流程,包括线程调度、I/O操作和Direct_IO支持。通过源码分析,揭示了数据读写的具体步骤,如迭代器生成、归并排序以及BlockBasedTableBuilder的Finish()函数在数据写入中的作用。同时,文章还介绍了RocksDB如何利用Direct_IO绕过pagecache以及Compaction和Flush的触发条件和线程调度策略。

        本文的主要目的是(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 完整实现过程 概览_天行健,地势坤-优快云博客

线程调度过程&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的博客-优快云博客

        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的。

<think>嗯,用户想比较RocksDB在完整压缩过程中读流量和写流量的大小。这是个很专业的存储引擎问题,用户可能是数据库开发或运维工程师。 用户提到的"完整压缩"应该是指手动触发的全量Compaction操作,比如执行rocksdb::DB::CompactRange()。这个需求很合理,因为生产环境中需要评估Compaction对I/O的影响。 从技术原理看,Compaction本质是数据重写过程。核心步骤包括:从多层挑选SST文件,合并重复/过期数据,写入新层级。因此必然涉及大量磁盘读写。 关键点在于:读取流量主要来自旧文件的扫描,写入流量则是新文件的生成。根据RocksDB官方文档和源码分析(引用2和3),Leveled Compaction策略下,写放大系数通常在10-30倍,但读取流量可能更高——因为除了当前层文件,还要读取下层文件进行归并排序。 用户可能还隐含了其他需求:比如想优化I/O瓶颈,或者预算带宽消耗。需要提醒写放大对SSD寿命的影响,这点对使用NVMe存储的用户特别重要。 考虑到用户的技术水平,回复需要包含具体计算示例。用100GB数据量演示最直观,同时补充理论依据:"读取流量 > 写入流量"的结论来自三个机制:1) 多层级文件重复扫描 2) Bloom filter假阳性带来的无效读 3) 快照等特性导致的临时数据保留。 最后生成的问题要覆盖延伸场景:如何监控实际流量(IOStats)、降低读放大的技巧(调整max_subcompactions)、以及企业级应用案例(TiDB的实践)。这样既能解决当前问题,又提供扩展知识。</think>在RocksDB的**完整压缩(Full Compaction)**过程中,**读取流量显著大于写入流量**,这是由其Compaction机制的本质决定的。以下是详细分析: --- ### **读写流量对比原理** 1. **读取阶段** - Full Compaction需扫描**所有层级(Level 0至最高层)的SST文件**,读取其中全部键值对(包括已删除或过期的数据)。 - 若启用布隆过滤器(Bloom Filter),仍需读取数据块进行校验,尤其当数据分布密集时。 - **读取流量公式**近似为: $$ \text{Read Size} \approx \sum_{i=0}^{N} \text{Size}(\text{Level}_i) $$ 其中 \( N \) 为最高层级。 2. **写入阶段** - 仅将**有效键值对**写入新的SST文件,并丢弃已删除或覆盖的数据。 - 写入量取决于**实际有效数据量**,通常远小于读取总量。 - **写入流量公式**为: $$ \text{Write Size} = \text{Size}(\text{Live Data}) $$ --- ### **关键影响因素** | 因素 | 对读流量的影响 | 对写流量的影响 | |---------------------|-----------------------------------|-----------------------------------| | **数据重叠度** | 层级间重叠越多,重复读取越严重 | 无直接影响 | | **删除/更新比例** | 需读取所有旧版本数据 | 仅写入最新版本,流量降低 | | **压缩算法** | 解压原始数据增加读开销 | 压缩后写入减少写开销 | | **SST文件大小** | 大文件需更多I/O扫描 | 大文件写入减少元数据开销 | --- ### **量化示例** 假设数据库总数据量 **100 GB**,其中: - **有效数据占比**:60%(即60 GB) - **删除/过期数据**:40%(40 GB) 则Full Compaction过程: 1. **读取流量** ≈ 100 GB(所有文件) 2. **写入流量** = 60 GB(仅有效数据) **读写比 ≈ 1.67 : 1**(读流量是写流量的1.67倍) > 📌 **注**:实际场景中因**元数据、索引、布隆过滤器假阳性**等因素,读取流量可能更高。 --- ### **优化读写流量的方法** 1. **降低读放大** - 启用 `optimize_filters_for_hits`:减少布隆过滤器访问[^2]。 - 调整 `max_subcompactions`:并行化减少总I/O时间[^3]。 2. **降低写放大** - 选择 `level_compaction_dynamic_level_bytes`:动态调整层级大小平衡写入[^1]。 - 使用 `Tiered Compaction`:对写友好但读性能下降。 --- ### **监控指标** 通过RocksDB Statistics 可观察: ```bash rocksdb.compact.read.bytes # 压缩总读取字节 rocksdb.compact.write.bytes # 压缩总写入字节 rocksdb.num.keys.written # 写入的有效键数 ``` 对比两者即可验证读写比例[^3]。 --- ### **总结** - **Full Compaction必然伴随高读取流量**,因其需扫描全量数据。 - **写入流量由有效数据量决定**,通常为总数据量的50%-80%。 - **读流量普遍高于写流量**,比例由数据更新/删除频率决定,典型范围在 **1.5x~3x** 之间[^1][^3]。 ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值