继续上一篇内容,来分析一下跨层压缩处理。leveldb不会轻易进行压缩流程(压缩流程影响性能),所以为了提升性能leveldb设计两个简单算法来决定压缩哪一层的哪一个文件
一、压缩预计算
1.1、打分函数
leveldb每次进行文件压缩并没有随意压缩的,而是通过压缩预计算流程(打分函数)计算出一下值,在压缩的时候根据预计算结果选择压缩的层次,那么打分函数是如何实现的呢?
/**
* 预计算下次压缩Level层次
* @param v 新版本信息
*/
void VersionSet::Finalize(Version* v) {
// Precomputed best level for next compaction
int best_level = -1;
double best_score = -1;
// 为什么少一层呢? 因为最后一层不需要压缩
for (int level = 0; level < config::kNumLevels-1; level++) {
double score;
if (level == 0) {
// 对于level 0以文件个数计算,kL0_CompactionTrigger默认配置为4,
// 因为如果0层文件多了会影响合并效率
// We treat level-0 specially by bounding the number of files
// instead of number of bytes for two reasons:
//
// (1) With larger write-buffer sizes, it is nice not to do too
// many level-0 compactions.
//
// (2) The files in level-0 are merged on every read and
// therefore we wish to avoid too many files when the individual
// file size is small (perhaps because of a small write-buffer
// setting, or very high compression ratios, or lots of
// overwrites/deletions).
score = v->files_[level].size() /
static_cast<double>(config::kL0_CompactionTrigger);
} else {
// Compute the ratio of current size to size limit.
//对于level>0,根据level内的文件总大小计算
//其中函数MaxBytesForLevel根据level返回其本层文件总大小的预定最大值。
//计算规则为:1048576.0* level^10。
const uint64_t level_bytes = TotalFileSize(v->files_[level]);
score =
static_cast<double>(level_bytes) / MaxBytesForLevel(options_, level);
}
//始终保存 分数最大以及level层次
if (score > best_score) {
best_level = level;
best_score = score;
}
}
/* 表示下次压缩层次以及计算出的分数 如果分数<1 则不是必须进行压缩 */
v->compaction_level_ = best_level;
v->compaction_score_ = best_score;
}
leveldb通过这样一个简单算法,设定压缩层次以及分数,后面在真正压缩的时候需要用到这两个数据。
1.2、无效Seek次太多
比如有两个文件A[aaa, mmm] ,B[ddd, hhh],如果我们要查询关键字为eee,通过上面可知,这两个文件都符合我们的要求,因此需要先查找A文件,如果A文件不存在eee则对B文件进行查找。很不幸我们需要eee这条记录就在B文件中,那么对于A文件的查找我们称做一次无效的Seek。当我们对于一个文件无效Seek次数过多,说明我们的数据不够和谐,因此就需要对这个文件(A文件)重新整理(即压缩)。leveldb设置的无效Seek次数为2^30。这部分内容在查询流程中在详细说明。
通过上面两种简单的算法,leveldb来确定对哪个层次中的哪个文件进行压缩。我们只需要了解是如何确定出来的即可。
二、BackgroundCompaction压缩入口函数
/**
* 压缩真正入口函数
* 1、imm_ MemTable不空则压缩MemTable 生成ldb(sstable)文件
* 2、对现有level文件进行压缩,避免各个level文件过多以及清理掉标记为删除的数据
*/
void DBImpl::BackgroundCompaction() {
mutex_.AssertHeld();
//压缩Immutable MemTable 不是空 说明存在则进行dump到文件中
if (imm_ != NULL) {
CompactMemTable();
return;
}
Compaction* c;//保存压缩相关数据
bool is_manual = (manual_compaction_ != NULL);
InternalKey manual_end;
if (is_manual) {//手动压缩 一般用于测试debug
ManualCompaction* m = manual_compaction_;
c = versions_->CompactRange(m->level, m->begin, m->end);
m->done = (c == NULL);
if (c != NULL) {
manual_end = c->input(0, c->num_input_files(0) - 1)->largest;
}
Log(options_.info_log,
"Manual compaction at level-%d from %s .. %s; will stop at %s\n",
m->level,
(m->begin ? m->begin->DebugString().c_str() : "(begin)"),
(m->end ? m->end->DebugString().c_str() : "(end)"),
(m->done ? "(end)" : manual_end.DebugString().c_str()));
} else {//选出 最适合压缩的level以及文件元数据 此函数并没有进行压缩
c = versions_->PickCompaction();
}
Status status;
if (c == NULL) {
// Nothing to do
} else if (!is_manual && c->IsTrivialMove()) {//文件不需要合并 只需要修改所属层次
// Move file to next level
assert(c->num_input_files(0) == 1);
FileMetaData* f = c->input(0, 0);
c->edit()->DeleteFile(c->level(), f->number);
c->edit()->AddFile(c->level() + 1, f->number, f->file_size,
f->smallest, f->largest);
status = versions_->LogAndApply(c->edit(), &mutex_);
if (!status.ok()) {
RecordBackgroundError(status);
}
VersionSet::LevelSummaryStorage tmp;
Log(options_.info_log, "Moved #%lld to level-%d %lld bytes %s: %s\n",
static_cast<unsigned long long>(f->number),
c->level() + 1,
static_cast<unsigned long long>(f->file_size),
status.ToString().c_str(),
versions_->LevelSummary(&tmp));
} else {//level和level+1中有key值重合 因此需要合并
CompactionState* compact = new CompactionState(c);
status = DoCompactionWork(compact);//执行压缩
if (!status.ok()) {
RecordBackgroundError(status);
}
CleanupCompaction(compact);
c->ReleaseInputs();
DeleteObsoleteFiles();
}
delete c;
if (status.ok()) {
// Done
} else if (shutting_down_.Acquire_Load()) {
// Ignore compaction errors found during shutting down
} else {
Log(options_.info_log,
"Compaction error: %s", status.ToString().c_str());
}
if (is_manual) {
ManualCompaction* m = manual_compaction_;
if (!status.ok()) {
m->done = true;
}
if (!m->done) {
// We only compacted part of the requested range. Update *m
// to the range that is left to be compacted.
m->tmp_storage = manual_end;
m->begin = &m->tmp_storage;
}
manual_compaction_ = NULL;
}
}
上面压缩函数比较简单,这里简要说明一下:
1) 如果启动压缩流程,优先对MemTable进行压缩
2) 手动压缩一般用于测试环境,比如测试leveldb性能或者是leveldb有没有bug之类,所以对于这个分支可以不用关心
3) 通过调用PickCompaction选择出要进行压缩文件的层次,返回压缩信息
4) 有些时候文件中key与高一层文件没有冲突,这种常见只需要对文件level进行迁移即可,所以通过方法IsTrivialMove进行判断
5) 执行DoCompactionWork对文件进行压缩处理
三、PickCompaction
/**
* 选择出要压缩的level以及文件元数据
*/
Compaction* VersionSet::PickCompaction() {
Compaction* c;
int level;
// We prefer compactions triggered by too much data in a level over
// the compactions triggered by seeks.
const bool size_compaction = (current_->compaction_score_ >= 1);
const bool seek_compaction = (current_->file_to_compact_ != NULL);
/*
* 压缩场景原因:
* 1、一种是某一层级的文件数过多或者文件总大小超过预定门限,
* 2、level n 和level n+1重叠严重,无效seek次数太多。(level n 和level n+1的文件,key的范围可能交叉导致)
*/
if (size_compaction) {//由于文件大小超过门限值
level = current_->compaction_level_;//代表 当前要压缩合并的层次
assert(level >= 0);
assert(level+1 < config::kNumLevels);
c = new Compaction(options_, level);
// Pick the first file that comes after compact_pointer_[level]
for (size_t i = 0; i < current_->files_[level].size(); i++) {
FileMetaData* f = current_->files_[level][i];
//compact_pointer_压缩点 保存上次压缩最大key值 本次largest key大于上次 则进行压缩
if (compact_pointer_[level].empty() ||
icmp_.Compare(f->largest.Encode(), compact_pointer_[level]) > 0) {
c->inputs_[0].push_back(f);//保存文件元数据信息
break;
}
}
if (c->inputs_[0].empty()) {
// Wrap-around to the beginning of the key space
c->inputs_[0].push_back(current_->files_[level][0]); //保存文件元数据信息
}
} else if (seek_compaction) {//无效seek次数太多
level = current_->file_to_compact_level_;//代表 当前要压缩合并的层次
c = new Compaction(options_, level);
c->inputs_[0].push_back(current_->file_to_compact_);
} else {
return NULL;
}
c->input_version_ = current_;
c->input_version_->Ref();
// Files in level 0 may overlap each other, so pick up all overlapping ones
// 如果要压缩文件层次是0 那么key值可能有重复 需要分辨出来
if (level == 0) {
InternalKey smallest, largest;
GetRange(c->inputs_[0], &smallest, &largest);
// Note that the next call will discard the file we placed in
// c->inputs_[0] earlier and replace it with an overlapping set
// which will include the picked file.
// 遍历level-0层所有文件 重新确定输入集合input_[0]
current_->GetOverlappingInputs(0, &smallest, &largest, &c->inputs_[0]);
assert(!c->inputs_[0].empty());
}
/* 当前level要合并的文件统一保存到inputs_[0] 下面这个函数是设置inputs_[1] */
SetupOtherInputs(c);
return c;
}
说明:
1)上面主体if判断是针对两种场景压缩场景判断,通过代码注释可以方便理解。
2)这段代码提出了一个概念压缩点。压缩点实际为当前层次最近一次压缩后文件中最大值key。compact_pointer成员变量保存着每一层压缩点信息。这里涉及到一个概率性问题: 当前层发生压缩流程一定是有新文件加入(无效seek场景不处理压缩点),那么我们期盼新加入的文件比压缩点保存的最大值要大,这样只需要处理新的文件,将新文件与level+1层文件进行合并。那么level层小于压缩点的文件基本不用动(提升性能)。当然这只是一种期望,我们并不能保证每次都有会新的最大值文件出现。当没有大于的压缩点,只能随便选择一个文件,即代码。
if (c->inputs_[0].empty()) {
// Wrap-around to the beginning of the key space
c->inputs_[0].push_back(current_->files_[level][0]); //保存文件元数据信息
}
3)因为level0中的文件比较特殊,该层文件中的key不是有序的,因此需要特殊处理一下,保存整层文件中最小和最大值。注意这里调用了方法GetOverlappingInputs后,inputs_[0]可能保存多个文件。
4)调用方法SetupOtherInputs,实际是设置inputs_[1]。
四、 SetupOtherInputs
/**
* 压缩最主要过程 是剔除掉重复数据
* 他的作用是为了在不影响性能的情况下尽可能多的compaction当前level的文件
*/
void VersionSet::SetupOtherInputs(Compaction* c) {
const int level = c->level();
InternalKey smallest, largest;
GetRange(c->inputs_[0], &smallest, &largest);
//从level+1层中挑出 与 level层key重叠的 文件 inputs_[1]可能保存多个
current_->GetOverlappingInputs(level+1, &smallest, &largest, &c->inputs_[1]);
// Get entire range covered by compaction
// 先合并 在获取最大值、最小值 相当于确定最终区间
InternalKey all_start, all_limit;
GetRange2(c->inputs_[0], c->inputs_[1], &all_start, &all_limit);
// See if we can grow the number of inputs in "level" without
// changing the number of "level+1" files we pick up.
if (!c->inputs_[1].empty()) {//如果进入此分支 表明level 和 level+1有key重复
// 根据新的集合[all_start, all_limit]在level中进行过滤 可能又有符合条件的文件加入到其中
std::vector<FileMetaData*> expanded0;
current_->GetOverlappingInputs(level, &all_start, &all_limit, &expanded0);
// 计算所有文件大小
const int64_t inputs0_size = TotalFileSize(c->inputs_[0]);// level层待合并的文件
const int64_t inputs1_size = TotalFileSize(c->inputs_[1]); // level + 1层待合并文件
const int64_t expanded0_size = TotalFileSize(expanded0);
// expanded0集合 大于 input_[0]表示有新文件加入 因此需要以expanded0为基准
if (expanded0.size() > c->inputs_[0].size() &&
inputs1_size + expanded0_size <
ExpandedCompactionByteSizeLimit(options_)) {
InternalKey new_start, new_limit;
GetRange(expanded0, &new_start, &new_limit);//从扩展0 中挑出最小值和最大值
std::vector<FileMetaData*> expanded1;
current_->GetOverlappingInputs(level+1, &new_start, &new_limit,
&expanded1);//从level+1中挑出 重叠key的文件信息保存到扩展1中
if (expanded1.size() == c->inputs_[1].size()) {//表示level+1集合没有变化,则使用expanded0和expanded1作为input集合
Log(options_->info_log,
"Expanding@%d %d+%d (%ld+%ld bytes) to %d+%d (%ld+%ld bytes)\n",
level,
int(c->inputs_[0].size()),
int(c->inputs_[1].size()),
long(inputs0_size), long(inputs1_size),
int(expanded0.size()),
int(expanded1.size()),
long(expanded0_size), long(inputs1_size));
smallest = new_start;
largest = new_limit;
c->inputs_[0] = expanded0;
c->inputs_[1] = expanded1;
GetRange2(c->inputs_[0], c->inputs_[1], &all_start, &all_limit);
}
}
}
// Compute the set of grandparent files that overlap this compaction
// (parent == level+1; grandparent == level+2)
if (level + 2 < config::kNumLevels) {
current_->GetOverlappingInputs(level + 2, &all_start, &all_limit,
&c->grandparents_);
}
if (false) {
Log(options_->info_log, "Compacting %d '%s' .. '%s'",
level,
smallest.DebugString().c_str(),
largest.DebugString().c_str());
}
// Update the place where we will do the next compaction for this level.
// We update this immediately instead of waiting for the VersionEdit
// to be applied so that if the compaction fails, we will try a different
// key range next time.
compact_pointer_[level] = largest.Encode().ToString();//保存本次压缩 最大值
c->edit_.SetCompactPointer(level, largest);
}
说明:
1)input_[0]中保存文件数目:如果是level0层可能保存多个文件,如果level > 0则保存一个文件。确定input_[0]中最小/大值并且以此来确定input_[1]中符合范围的文件。
2)确定input_[0]和input_[1]文件中的最小/大值,保存在all_start,all_limit
3)如果input_[1]不空,则表明level+1层有冲突文件,这些冲突文件就是要合并的。因为在第2)步骤中确定了最终的all_start和all_limit,为了避免遗漏文件,必须再次对level层中文件进行遍历,查找符合条件的文件,将这些文件定义为expanded0。这里举例详细说明:
假设,level中有两个文件:A0,范围为[fff,kkk],A1,范围为[mmm,ppp],将文件A1作为input_[0]
level+1中有两个文件,F0,范围为[hhh,nnn],F1范围为[ooo,sss],可以得出input_[1] = {F0, F1},集合[hhh,sss]
那么对input_[0] ∪ input_[1] (并集)可得all_start=hhh,all_limit=sss。由于新的集合确定出来,为了避免遗漏文件需要再次遍历level层中的文件,并将新集合称之为expanded0,由上面的例子可知,expanded0包含文件A0和A1。
如果expaned0_size大于input0_size则表示有新的文件加入压缩流程,需要再次进行处理。
4)因为expanded集合大于input_[0],因此需要再次确定level中最小/大值,由上面例子可以范围:new_start=fff,new_limit = ppp,所以需要再次对level+1层文件进行遍历,确定expanded1,如果和expended1集合与input_[1]集合相等,则确定最终集合expanded0作为input_[0],expaneded[1]作为input_[1]。
5)注意input_[1],是有可能为空的,如果input_[1]为空,那么说明level和level+1没有冲突,只需要将level相应文件修改层次即可,不需要进行压缩处理。如下函数处理逻辑:
//是否只需要移动文件进行compaction,不需要merge和split
bool Compaction::IsTrivialMove() const {
const VersionSet* vset = input_version_->vset_;
// Avoid a move if there is lots of overlapping grandparent data.
// Otherwise, the move could create a parent file that will require
// a very expensive merge later on.
return (num_input_files(0) == 1 && num_input_files(1) == 0 &&
TotalFileSize(grandparents_) <=
MaxGrandParentOverlapBytes(vset->options_));
}
五、总结
本篇的重点方法有很多,leveldb处理逻辑比较复杂,好在自认为分析的比较清楚,但是不知道读者是否能看清楚。若有不清楚的可以看一下代码注释,在不清楚可以留言进行讨论。下一篇分析DoCompactionWork流程。