本文的主要目的是(1)了解RocksDB源码中Flush和Compaction的基本流程(2)了解Compaction/FLush过程中是在何处、如何产生I/O的。
目录
RocksDB的compaction 和 flush 的触发机制补充
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压力下,仍然能保持合理的树形结构,其计算方式为:
-
找到当前树形结构数据量最多的一层,作为Target_Size(Ln)
-
通过公式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的。