文章目录
本文将从源码方面分析 Flush 的流程
博客只能分析部分,完整分析见代码注释:LevelDB-annotated
一般而言,分析一个流程要从入口点还是逐层向下分析,但这样带来的后果是主次的详略不当。通常,一种操作的核心函数只有一两个,然后衍生该函数至一整个调用链就构成了操作流程,而该核心函数最直接的描述了思想,重中之重。
因此,本文没有采用从入口分析的方式,而是先分析核心函数 WriteLevel0Table(),然后完善其调用链。
WriteLevel0Table
该函数顾名思义,写 Level0 的 SST,那其实就是 Flush,Flush的真正执行正是该函数。实际上,WriteLevel0Table 并不一定是写 Level0,还有可能是写 Level1 或是 Level2,这取决于顶层的情况。
该函数的工作只有一个:把 imm_ 给 Flush 了。
分为以下三步执行:
- 通过 BuildTable() 来生成 SST
- 通过 PickLevelForMemTableOutput() 来选出存放该 SST 的 Level
- 通过 edit->AddFile() 将新的 SST 加入 VersionEdit 中
先看源码:
Status DBImpl::WriteLevel0Table(MemTable* mem, VersionEdit* edit,
Version* base) {
mutex_.AssertHeld();
const uint64_t start_micros = env_->NowMicros();
FileMetaData meta;
meta.number = versions_->NewFileNumber();
// 加入待生成SST列表
pending_outputs_.insert(meta.number);
Iterator* iter = mem->NewIterator();
Log(options_.info_log, "Level-0 table #%llu: started",
(unsigned long long)meta.number);
Status s;
// 根据im的iter生成SST
{
mutex_.Unlock();
// 注意,BuildTable是包含落盘的
// SST中的一些信息(最大最小key、文件大小)等会记录在meta中
s = BuildTable(dbname_, env_, options_, table_cache_, iter, &meta);
// 至此,SST已经在磁盘中了
mutex_.Lock();
}
Log(options_.info_log, "Level-0 table #%llu: %lld bytes %s",
(unsigned long long)meta.number, (unsigned long long)meta.file_size,
s.ToString().c_str());
// 释放iter
delete iter;
// 从待生成列表中删除
pending_outputs_.erase(meta.number);
// Note that if file_size is zero, the file has been deleted and
// should not be added to the manifest.
int level = 0;
if (s.ok() && meta.file_size > 0) {
const Slice min_user_key = meta.smallest.user_key();
const Slice max_user_key = meta.largest.user_key();
if (base != nullptr) {
// 从当前Version中选出存放该SST的Level
// 一般来说是0,当然也可能不是
// LevelDb的思想:
// 尽可能放在2层,因为level0层经常被访问,内部又允许重叠,如果文件过多,会放大读性能
level = base->PickLevelForMemTableOutput(min_user_key, max_user_key);
}
// 新的SST加入new_files_中
edit->AddFile(level, meta.number, meta.file_size, meta.smallest,
meta.largest);
}
CompactionStats stats;
stats.micros = env_->NowMicros() - start_micros;
stats.bytes_written = meta.file_size;
stats_[level].Add(stats);
return s;
}
逐步的分析这里就不写了,挺简单的,并且注释都写有。有几个需要注意的地方:
BuildTable() 不仅仅是构建 SST 在内存中的结构,而是直接将 SST 落盘,至于怎么构建又怎么落盘的,这需要深入 SST 部分的源码了,大致是通过 builder 来构建一个个 block,然后整合在一起进行刷盘,这里就不深究了,具体去看源码注释。
只需明白:先生成 SST,在决定它放在哪一层。
生成 SST 之后,通过 PickLevelForMemTableOutput() 来决定它放在哪一层。问题来了,Flush 难道不就是放在第一层吗,为什么还要通过函数来决定?还真不是。在 LevelDb 的思想中,Flush 的文件尽可能放在2层,因为0层经常被访问,内部又允许重叠,如果文件过多,会放大读。
但是不是想往下放就能往下放的,要满足以下条件(假设 level 是最终目标层):
- SST 不能和 level+1 有重叠
- SST 可以和 level+2 有重叠,但重叠不能超过阈值
源码如下:
// 从当前Version中选出存放从memtable下刷的SST的Level
// 一般来说,是Level0
// 但是,如果Level和该SST没有重叠,那么就直接放在Level1更好
// 依此类推,如果没有重叠,还可以放到Level2
// 直到出现重叠或达到kMaxMemCompactLevel(2)
int Version::PickLevelForMemTableOutput(const Slice& smallest_user_key,
const Slice& largest_user_key) {
int level = 0;
if (!OverlapInLevel(0, &smallest_user_key, &largest_user_key)) {
// Push to next level if there is no overlap in next level,
// and the #bytes overlapping in the level after that are limited.
InternalKey start(smallest_user_key, kMaxSequenceNumber, kValueTypeForSeek);
InternalKey limit(largest_user_key, 0, static_cast<ValueType>(0));
std::vector<FileMetaData*> overlaps;
while (level < config::kMaxMemCompactLevel) {
// 不能和level+1有重叠
if (OverlapInLevel(level + 1, &smallest_user_key, &largest_user_key)) {
break;
}
// 可以和level+2有重叠,但重叠不能超过阈值
if (level + 2 < config::kNumLevels) {
// Check that file does not overlap too many grandparent bytes.
GetOverlappingInputs(level + 2, &start, &limit, &overlaps);
const int64_t sum = TotalFileSize(overlaps);
if (sum > MaxGrandParentOverlapBytes(vset_->options_)) {
break;
}
}
level++;
}
}
return level;
}
先生成 SST,在决定它放在哪一层,就引入了一个问题:LSM-Tree 中到底如何记录每一层有哪些 SST 呢?
=> 该问题在上一篇博客中回答了:LevelDB层级实现的思考
至此,WriteLevel0Table() 就介绍完毕了,Flush 的核心也就介绍完毕了。
接下来介绍调用链。
MakeRoomForWrite
该函数就是字面意思,为 Write 腾出空间,也就是看情况是否新建 memtable。不需要,则直接退出;需要,则新建 memtable。而 Flush 的入口,就在该函数中。
函数的流程如下:
- 如果允许 delay,而且 L0 的文件个数没有超过 kL0_SlowdownWritesTrigger,那么就等 1s 重来,但是只能等1次。
- 如果不允许 delay,且 memtable 有足够的空间,说明不需要新建了,直接退出。
- 在第 2 步为否的基础上(memtable 满了),如果 imm_ 不为空,则说明 memtable 满了且当前的 imm_ 还没有被 Flush 掉,此时已经没有空间分配出去了,因为 mem_ 和 imm_ 各只能有一个,那么就一直等待,直到Flush 完成后唤醒它,然后重新进入循环
- 在前几步为否的基础上(memtable满了,imm_ 为空),如果 L0 的文件个数超过 kL0_StopWritesTrigger,就说明 L0 文件太多了,那么就等待,直到 L0 被 Compaction 了之后在被唤醒。
- 在前几步为否的基础上(memtable满了,imm_ 为空,L0空间足够),将mem_ 赋值给 imm_ ,然后重建一个新的 mem_ ,即下刷旧的生成新的。随后,调用 MaybeScheduleCompaction() 来将imm_ 给 Flush 掉。
源码如下:
Status DBImpl::MakeRoomForWrite(bool force) {
mutex_.AssertHeld();
assert(!writers_.empty());
bool allow_delay = !force;
Status s;
while (true) {
if (!bg_error_.ok()) {
// Yield previous error
s = bg_error_;
break;
} else if (allow_delay && versions_->NumLevelFiles(0) >=
config::kL0_SlowdownWritesTrigger) {
// We are getting close to hitting a hard limit on the number of
// L0 files. Rather than delaying a single write by several
// seconds when we hit the hard limit, start delaying each
// individual write by 1ms to reduce latency variance. Also,
// this delay hands over some CPU to the compaction thread in
// case it is sharing the same core as the writer.
mutex_.Unlock();
env_->SleepForMicroseconds(1000);
allow_delay = false; // Do not delay a single write more than once
mutex_.Lock();
} else if (!force &&
(mem_->ApproximateMemoryUsage() <= options_.write_buffer_size)) {
// There is room in current memtable
break;
} else if (imm_ != nullptr) {
// We have filled up the current memtable, but the previous
// one is still being compacted, so we wait.
// mem_满了,且imm_仍存在,那么就等待直到imm_被Flush
Log(options_.info_log, "Current memtable full; waiting...\n");
background_work_finished_signal_.Wait();
} else if (versions_->NumLevelFiles(0) >= config::kL0_StopWritesTrigger) {
// There are too many level-0 files.
Log(options_.info_log, "Too many L0 files; waiting...\n");
background_work_finished_signal_.Wait();
} else {
// mem_满了,imm_为空
// 那就把mem_赋值给imm_,然后重建一个新的mem_
// Attempt to switch to a new memtable and trigger compaction of old
assert(versions_->PrevLogNumber() == 0);
uint64_t new_log_number = versions_->NewFileNumber();
WritableFile* lfile = nullptr;
s = env_->NewWritableFile(LogFileName(dbname_, new_log_number), &lfile);
if (!s.ok()) {
// Avoid chewing through file number space in a tight loop.
versions_->ReuseFileNumber(new_log_number);
break;
}
delete log_;
delete logfile_;
logfile_ = lfile;
logfile_number_ = new_log_number;
log_ = new log::Writer(lfile);
imm_ = mem_;
has_imm_.store(true, std::memory_order_release);
mem_ = new MemTable(internal_comparator_);
mem_->Ref();
force = false; // Do not force another compaction if have room
// 将imm_给Flush
MaybeScheduleCompaction();
}
}
return s;
}
当 MakeRoomForWrite() 判断出 imm_ 存在且能够 Flush 后,会调用 MaybeScheduleCompaction() 更进一步。
MaybeScheduleCompaction
该函数的作用就是判断是否需要调度后台 Compaction 任务。注意,LevelDB 的后台任务均为 BGWork,不管是 Flush 还是 Compaction,并且后台线程只能有一个,这是其不同于 RocksDB 的一个主要特征。MaybeScheduleCompaction 仍在主线程中执行,但其结果会新开一个后台线程。代码如下:
// 可能要进行Compaction
void DBImpl::MaybeScheduleCompaction() {
mutex_.AssertHeld();
if (background_compaction_scheduled_) {
// 压缩线程已经在执行了
// Already scheduled
} else if (shutting_down_.load(std::memory_order_acquire)) {
// db正在被关闭
// DB is being deleted; no more background compactions
} else if (!bg_error_.ok()) {
// Already got an error; no more changes
} else if (imm_ == nullptr && manual_compaction_ == nullptr &&
!versions_->NeedsCompaction()) {
// imm_为空,且LSM-Tree不需要Compaction
// No work to be done
} else {
// 三种可能情况:
// 1.imm_不为空,需要Flush
// 2.manual_compaction_不为空,需要手动开启压缩
// 3.LSM-Tree需要压缩
// 调度Compaction
background_compaction_scheduled_ = true;
// Schedule()就是另起一个线程去执行传入函数
env_->Schedule(&DBImpl::BGWork, this);
}
}
通过 background_compaction_scheduled_ 可以看出,后台线程只能有一个。
以下三种情况,Compaction / Flush 会发生:
- imm_不为空,需要Flush
- manual_compaction_不为空,需要手动开启压缩
- LSM-Tree需要压缩
其中第一种就是 FLush。
env_->Schedule(&DBImpl::BGWork, this);
就是另开一个线程执行 BGWork 函数,而BGWork 实际上是调用 BackgroundCall() 函数。
BackgroundCall
注意,此时已经是在新开的后台线程里了。代码如下:
void DBImpl::BackgroundCall() {
// 锁
MutexLock l(&mutex_);
assert(background_compaction_scheduled_);
if (shutting_down_.load(std::memory_order_acquire)) {
// No more background work when shutting down.
} else if (!bg_error_.ok()) {
// No more background work after a background error.
} else {
// 执行Compaction
// 因为此时已经在新线程了,所以BackgroundCompaction()是个串行执行
BackgroundCompaction();
}
// Compaction完毕
background_compaction_scheduled_ = false;
// Previous compaction may have produced too many files in a level,
// so reschedule another compaction if needed.
// 重新判断是否需要Compaction
// 因为前一个Compaction如果过大的话可能产生太多新的SST导致需要另一次Compaction
MaybeScheduleCompaction();
background_work_finished_signal_.SignalAll();
}
可以看出,它就是调用 BackgroundCompaction() 函数,执行完后重新执行一遍 MaybeScheduleCompaction(),因为前一个 Compaction 如果过大的话可能产生太多新的 SST 导致需要另一次Compaction。
BackgroundCompaction
该函数我们只看前面一点:
void DBImpl::BackgroundCompaction() {
mutex_.AssertHeld();
// imm_不为空的话,则执行Flush
// 执行完后返回即可
if (imm_ != nullptr) {
CompactMemTable();
return;
}
// ... 省略很多代码
}
可以看到,如果 imm_ 不为空,那么就执行 CompactMemTable(),也就是 Flush,执行完之后返回即可。
CompactMemTable
直接看代码:
// 把im memtable给flush到L0中
void DBImpl::CompactMemTable() {
mutex_.AssertHeld();
assert(imm_ != nullptr);
// Save the contents of the memtable as a new Table
// 新建VerionEdit对应本次Flush
VersionEdit edit;
// 当前所在的Version
Version* base = versions_->current();
// Version加引用
base->Ref();
// 执行Flush
Status s = WriteLevel0Table(imm_, &edit, base);
// Version解引用
base->Unref();
if (s.ok() && shutting_down_.load(std::memory_order_acquire)) {
s = Status::IOError("Deleting DB during memtable compaction");
}
// Replace immutable memtable with the generated Table
if (s.ok()) {
edit.SetPrevLogNumber(0);
edit.SetLogNumber(logfile_number_); // Earlier logs no longer needed
s = versions_->LogAndApply(&edit, &mutex_);
}
if (s.ok()) {
// Commit to the new state
imm_->Unref();
imm_ = nullptr;
has_imm_.store(false, std::memory_order_release);
RemoveObsoleteFiles();
} else {
RecordBackgroundError(s);
}
}
这就连上了,CompactMemTable() 的工作就是在进行一些 Version 操作之后,调用 WriteLevel0Table() 。
总结
综上,解答如下问题:
- WriteLevel0Table() 何时调用?
- 由谁调用?
- 单线程串行调用还是多线程并行调用?
答:
- imm_ 满时调用。
- 最直接的调用者是CompactMemTable()。继续往前回溯,由 MakeRoomForWrite() 逐步调用。
- 单线程,LevelDB 的后台线程只有一个,不管是 Flush 还是 Compaction。
主要调用链:
MakeRoomForWrite
-> MaybeScheduleCompaction
-> BackgroundCall
->
BackgroundCompaction
-> CompactMemTable
-> WriteLevel0Table