diff --git a/README.md b/README.md index 1c56240..7fcc947 100644 --- a/README.md +++ b/README.md @@ -210,53 +210,17 @@ Status DBImpl::DoCompactionWork(CompactionState* compact) { } ``` -理论上,`Compaction`相关的代码也实现了,但实际上还会存在一些问题。具体问题和解决方法详见 [问题和解决方案](#3.-问题和解决方案)。 - -## 2. 测试用例和测试结果 - -### TestTTL.ReadTTL - -`TestTTL`中第一个测试样例,检测插入和读取数据时,读取过期数据能够正确返回`NotFound`。 - -![ReadTTL](img/test1_succ.png) - -### TestTTL.CompactionTTL - -`TestTTL`中第二个测试样例,检测插入和合并数据,能够在触发合并时清除所有过期数据。 - -![CompactionTTL](img/test2_succ.png) - -然而,当数据库的设定的`level`过小时,仍然会出现错误。 - -![CompactionTTL_fail](img/test2_fail_with_3levels.png) - -具体问题和解决方法见 [问题和解决方案](#3.-问题和解决方案)。 - -### TestTTL.LastLevelCompaction - -在解决了上面遇到的问题之后,本小组添加了一个测试样例,用来检测在含义`SSTable`的最大层能否正确检测文件数量和合并、清理过期数据。 - -![LastLevelCompaction](img/test3_pass.png) - -最后,展示三个测试样例一起运行的结果: - -![All_pass](img/all_pass.jpg) - -## 3. 问题和解决方案 - -### 3.1 无法合并和清理所有数据文件 - -本实验的问题和**合并**`Compaction`有关。 +理论上,`Compaction`已经实现了合并中清除过期数据的功能,但由于原本leveldb是按照没有TTL功能的逻辑设计的,因此其对数据的清除并不完全,也会造成测试不通过。 LevelDB中`Compaction`的逻辑是选中特定层(`level`)合并,假设为`level n`。在`level n`中找目标文件`SSTable A`(假设该次触发的合并从文件A开始),并确定`level n`以及`level n+1`中与`SSTable A`包含的数据(`key/value`)有`key`发生重复(`overlap`)的所有文件,合并后产生新的`SSTable B`放入`level n+1`层中。 -这样引发的问题是:由`DoCompactionWorks`代码可知,不参与合并的文件无法被获取信息,因而即使过期了也无法被清理。在此例子中,若`level n+1`层中有含有过期数据的`SSTable C`,但`SSTable C`与`SSTable A`发生合并时触及的所有文件都没有`overlap`的话,则在该次合并中并没有被触及,因而无法被清理。 +然而由`DoCompactionWorks`代码可知,不参与合并的文件即使过期了也无法被清理。 -再次强调,合并过程必须清理**所有**已经过期的数据(尽管这听起来有些让人困惑,因为正常使用时,没有被合并的过期数据即使未被清理也不影响正常使用),而无法被清理的`SSTable C`明显是个例外,是个错误。 +例如,`level n+1`层中有含有过期数据的`SSTable C`,但由于`level n+1`是最后一个含有文件的层,即使是`CompactRange(nullptr, nullptr)`,对所有数据合并,其只将`level n`的所有文件与`level n+1`中与上层有重叠的文件合并,因此`SSTable C`不会被清理。 -而在LevelDB提供的`CompactRange(nullptr,nullptr)`这个“合并所有数据”的功能中,同样会发生这样的错误,导致有部分数据文件并没有在合并过程中被触及——即使它声称合并了**所有**数据。 +`CompactRange(nullptr, nullptr)`应该能够合并所有数据,也就是可以清除所有过期数据,而无法被清理的`SSTable C`明显是个例外,是个错误。 -从代码层面可以理解导致该错误的原因。原来的代码: +从代码层面看导致该错误的原因。原来的代码: ```c++ void DBImpl::CompactRange(const Slice* begin, const Slice* end) { @@ -291,99 +255,146 @@ void DBImpl::TEST_CompactRange(int level, const Slice* begin, + max_level_with_files:表示含有`SSTable`文件的最高层的编号。注意,这里的`level`编号是从0开始的,因此`max_level_with_files`理论最大值是`config::kNumLevels - 1` -在原来的代码中,`DBImpl::CompactRange`函数中的最后一个循环,选择的`level`无法到达`max_level_with_files`,因此即使“合并所有数据”,也没法触及`max_level_with_files`中的所有文件,而是对`max_level_with_files - 1`层的所有文件做了大合并。 +可以看出与上述例子相符,对最后有文件的那一层不会进行全范围的合并,因此产生漏网之鱼,而最直接想到的解决办法就是让手动合并时也对这最后一层做一次全范围的合并,就可以清除所有的过期数据。 -修改后的代码如下: +这样只需要让循环多走一层: ```c++ -void DBImpl::CompactRange(const Slice* begin, const Slice* end) { - int max_level_with_files = 1; - { - MutexLock l(&mutex_); - Version* base = versions_->current(); - for (int level = 1; level < config::kNumLevels; level++) { - if (base->OverlapInLevel(level, begin, end)) { - max_level_with_files = level; - } - } - } - TEST_CompactMemTable(); // TODO(sanjay): Skip if memtable does not overlap for (int level = 0; level < max_level_with_files + 1; level++) { TEST_CompactRange(level, begin, end); } -} +``` -void DBImpl::TEST_CompactRange(int level, const Slice* begin, - const Slice* end) { - assert(level >= 0); - assert(level < config::kNumLevels); +但是这种方法仍然存在两个问题: - (......) -} -``` +1. 最后有文件的一层(max_level_with_files)可能并没有超过size的限制,也就是说其不应该被合并到下一层中,这也同样符合leveldb原本的逻辑。 + +2. 当存储数据已经堆积到整个leveldb的最底层时(即已达到`kNumLevels`),按照上述代码执行会导致致命错误并使数据库崩溃。 -简单的方法是直接修改了循环的条件和相关函数`TEST_CompactRange`的判断条件,使得`level`可以到达`max_level_with_files`,这样就可以真正地合并所有文件并清理过期数据。 +防止最底层的数据被合并的函数调用是唯一的解决办法,但是这个做法又回到了上一个问题,最底层的过期数据可能始终无法被完全清除。 -### 3.2 最后一层文件堆积且无法清理 +### 1.2.4 合并的最终解决方案 -在上一个问题中的解决方法在遇到最后一层(`MaxLevel`),即`level == config::kNumLevels - 1`时失效,因为这个时候的`Compaction`无法再找到`level + 1`的文件合并了,因为`level + 1`层根本不存在。 +为了完全解决这个问题并让`CompactRange`正常工作,我们提出了一个新的方案: -*在测试样例中,由于存入的文件到达不了`MaxLevel`,并不会遇到这种情况,因此可以顺利通过样例。为了制造出现错误的情况,可以将`config::kNumLevels`设置为较小值(如:3)再运行测试。这种情况放在`git`仓库的`light_ver`分支的修改里了。* +对于最后有文件的一层(即max_level_with_files),我们进行单独的处理,去除掉其中的过期数据,但不将其推向下一层。 -这同时引发了另一个问题的思考:当存储的数据文件(`SSTable`)已经存放在最高层`MaxLevel`中时,无法被合并和清除,因此本小组决定对涉及`MaxLevel`的合并做优化。 +实现上,去除一层(e.g. level n)中的过期数据,实际上等价于将`level n`与一个空的层合并,再将合并的结果输出回`level n`。 -我们选择的优化方式基于以下思考: +因此,我们可以直接利用leveldb中被我们修改过的合并功能(合并中drop过期数据)来实现清除一层中的过期数据。 -+ 通常情况下,`MaxLevel`仅在手动触发的大合并即`Manual Compaction`中使用到,而自动合并`Size Compaction`和`Seek Compaction`几乎不会发生在`MaxLevel`,理由是: - + Seek Compaction的情况:由于`MaxLevel`没有“下一层”,当在该层的`seek`查找不到文件时则返回`NotFound`,不可能触发`Seek Compaction` - + Size Compaction的情况:由于数据库的`SSTable`通常按层顺序存储,若`MaxLevel`的存储文件大小达到上限,说明数据库的存储容量已经满了 -+ 在大合并中,由于针对上个问题的修改方案,当`level`到达`max_level_with_files`时,会和下一层(此时为空)合并,导致合并后的文件会存入下一层,然而这个操作的目的仅仅是清理`max_level_with_files`的过期数据而已,并不需要移动文件,且这样做更早地使用了更高的`level` +> CompactRange函数用于手动合并,对于leveldb的自动合并,我们并不需要强迫其清除其触碰不到的数据,因此只需要修改手动合并的部分 -优化方案:修改`Manual Compaction`相关的函数`CompactRange`、`TEST_CompactRange`和`InstallCompactionResults`,以及`Manual Compaction`类、`Compaction`类,向它们引入布尔类型变量`is_last_level`,判断当前层是否是`max_level_with_files`。 +在实现上,引入参数`is_last_level`表示当前是否在处理`max_level_with_files`,如果是则按照上述逻辑清除该层过期数据,反之按照原本的合并正常进行 -主要修改的函数: +在结构体`ManualCompaction`和类`Compaction`中引入新的成员`is_max_level`: ```c++ -void VersionSet::SetupOtherInputs(Compaction* c) { - const int level = c->level(); - InternalKey smallest, largest; + struct ManualCompaction { + int level; + bool is_last_level; // TTL: Used to check if last level with files + bool done; + const InternalKey* begin; // null means beginning of key range + const InternalKey* end; // null means end of key range + InternalKey tmp_storage; // Used to keep track of compaction progress + }; + + class Compaction { + public: + (...) + bool is_last_level() const {return is_last_level_; } + (...) + + private: + (...) + bool is_last_level_; + (...) + } +``` - AddBoundaryInputs(icmp_, current_->files_[level], &c->inputs_[0]); - GetRange(c->inputs_[0], &smallest, &largest); +给方法`TEST_CompactRange`和`VersionSet::CompactRange`增加一个参数`is_last_level`,用来传递构造结构体`ManualCompaction`和类`Compaction`的相应值。 - // TTL: manual compaction for last level shouldn't have inputs[1] - if (!c->is_last_level()) { +在`CompactRange`的上述循环中调用`TEST_CompactRange`时,会将`is_last_level`传给该方法 + +```c++ + for (int level = 0; level < max_level_with_files + 1; level++) { + TEST_CompactRange(level, begin, end, level == max_level_with_files); + } +``` + +在清除当前层过期数据的操作中,compaction中的inputs[1]应该为空,因此在`VersionSet::SetupOtherInputs`方法中,跳过对inputs[1]的填写: + +```c++ + if (!c->is_last_level()) { current_->GetOverlappingInputs(level + 1, &smallest, &largest, &c->inputs_[1]); AddBoundaryInputs(icmp_, current_->files_[level + 1], &c->inputs_[1]); } - (......) - if (!c->inputs_[1].empty()) { - (......) - } - - (......) -} ``` -若当前合并的`level`是`max_level_with_files`,则`Compaction* c`不需要`inputs_[1]`,相当于和一个空的`level`进行合并。 +接下来我们要做的是让合并的结果输出到当前层`level`而不是`level+1` + +需要修改两处: +1. DBImpl::DoCompactionWork中,stats_应该add在当前level上 +2. DBImpl::InstallCompactionResults中,将结果写入当前level中 + +代码: ```c++ +Status DBImpl::DoCompactionWork(CompactionState* compact) { + (...) + if (!compact->compaction->is_last_level()) { + stats_[compact->compaction->level() + 1].Add(stats); + } else { + // TTL: compaction for last level + stats_[compact->compaction->level()].Add(stats); + } + (...) +} + Status DBImpl::InstallCompactionResults(CompactionState* compact) { - (......) - for (size_t i = 0; i < compact->outputs.size(); i++) { - const CompactionState::Output& out = compact->outputs[i]; - if (!compact->compaction->is_last_level()) { - compact->compaction->edit()->AddFile(level + 1, out.number, out.file_size, out.smallest, out.largest); + (...) + if (!compact->compaction->is_last_level()) { + compact->compaction->edit()->AddFile(level + 1, out.number, out.file_size, + out.smallest, out.largest); } else { // TTL: outputs of last level compaction should be writen to last level itself - compact->compaction->edit()->AddFile(level, out.number, out.file_size, out.smallest, out.largest); + compact->compaction->edit()->AddFile(level, out.number, out.file_size, + out.smallest, out.largest); } - } - return versions_->LogAndApply(compact->compaction->edit(), &mutex_); + (...) } ``` -添加判断条件,若当前层是最高层,则合并后的文件仍然放在本层。 +通过以上修改后即解决了`CompactRange`的问题,使其能够正常工作 -修改后,可以同时解决提到的两个问题,成功清理最高层的过期数据文件。 +## 2. 测试用例和测试结果 + +### TestTTL.ReadTTL + +`TestTTL`中第一个测试样例,检测插入和读取数据时,读取过期数据能够正确返回`NotFound`。 + +![ReadTTL](img/test1_succ.png) + +### TestTTL.CompactionTTL + +`TestTTL`中第二个测试样例,检测插入和合并数据,能够在手动触发所有数据的合并时清除所有过期数据。 + +![CompactionTTL](img/test2_succ.png) + +### TestTTL.LastLevelCompaction + +在解决了上面遇到的问题之后,本小组添加了一个测试样例,用来检测在含义`SSTable`的最底层能否正确合并、清理过期数据。 + +在该样例中将`kNumLevels`设置为`3`可以保证(同时Assert确认正确性)最后一层中存在数据,我们的实现仍然可以通过测试: + +![LastLevelCompaction](img/test3_pass.png) + +而采用上述直接添加一层循环并且跳过最底层避免崩溃的方法则无法通过测试: + + +在该样例中将`kNumLevels`设置为3并 + +最后,展示三个测试样例一起运行的结果: + +![All_pass](img/all_pass.jpg)