上一篇我们介绍了进行Major Compaction所需要的对象数据实体Compaction。对整个Compact流程的介绍可点此Compact触发流程
本片我们来着重介绍下Compact的Major Compaction流程。
Major Compaction的作用
01节省磁盘空间
我们知道leveldb的存储结构用的是LSM数据结构,增、删、改操作都是顺序追加操作,也就是不会去真正的去修改数据或者删除数据,而是写入操作,只是设置对应的标志。这样的话同一个key可能会存多条分布在不同的SSTable文件中,这明显是冗余的。通过Major Compaction可以删除过期无效的key,减少磁盘空间占用。
02提高读效率
我们知道level-0层的SSTable文件之间是无序的,这样查找一个key时就需要遍历所有的文件,这显然是低效的,如果leveldb的每一层SSTable文件都是无序的,那查找的时候就是灾难了。但是通过major Compaction,leveldb从level-1层开始,每层的SSTable文件之间都是有序的。这样在查找一个key的时候,必要的话只有在level-0层需要遍历所有的SSTable,在level-1及以上层只需通过二分搜索快速定位到某个SSTable文件,然后再去细查此文件即可。
源码分析
之前文章已经讲解了怎么查找用于进行Major Compaction的SSTable文件及数据,不清楚的可以点此Compaction数据产生。主要通过以下两个方法生存:
Compaction* c;
- c = versions_->CompactRange(m->level, m->begin, m->end);
- c = versions_->PickCompaction();
Major Compaction分两种:
- 直接将level层文件移动到level+1层。
- 循环遍历所有待Compact的key,保留有效key,丢弃无效key。
01移动Compact
即简单的移动文件到下一层来达到合并文件的效果。
满足此流程的条件如下:
- 非手动Compact;
- 待Compact的level层只有一个文件,level+1层没文件且grandparents层总的文件大小未超过阈值MaxGrandParentOverlapBytes()。
//通过方法IsTrivialMove()来判断是不是可以简单的移动文件到下一层
//来达到合并文件的效果。但是又要避免因将文件移动到下一层导致与
//下下一层即grandparent层有太多的重叠数据进而导致compact下一层时
//压力太大。
//判断是否可行的方法如下:
//1、inputs_[0]层只有一个文件、inputs_[1]层没文件。
//2、grandparents层总的文件大小未超过阈值MaxGrandParentOverlapBytes()。
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_));
}
<!通过移动文件达到Compact效果的流程>
if (!is_manual && c->IsTrivialMove()) {
// Move file to next level
//1.如果当前压缩文件指出TrivialMove()条件,
//2.则直接在VersionEdit中,记录下level层删除此文件,
// level + 1 添加此新文件。
//3.将此VersionEdit应用出新的Version。
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));
}
02正常Compact
此流程就是遍历所有需要待Compact的SSTable文件中的key,有效的key就写入到新的SSTable文件中,无效的key就丢弃。写完之后删除掉无用的SSTable。详细的说明已在代码中注释。
//Compact工作。
//1.将待Compact的input[0]、input[1]文件创建为迭代器访问
//2.Seek到迭代器的最小key,并开始循环访问这些key并进行Compact。
//3.在循环的过程中,如果有immutable,那就优先处理进行下Compact。
//4.判断下当前准备Compact的文件对应的key是否需要提前停止Compact,并将当前的文件落地为SSTable。
Status DBImpl::DoCompactionWork(CompactionState* compact) {
const uint64_t start_micros = env_->NowMicros();
int64_t imm_micros = 0; // Micros spent doing imm_ compactions
Log(options_.info_log, "Compacting %d@%d + %d@%d files",
compact->compaction->num_input_files(0), compact->compaction->level(),
compact->compaction->num_input_files(1),
compact->compaction->level() + 1);
assert(versions_->NumLevelFiles(compact->compaction->level()) > 0);
assert(compact->builder == nullptr);
assert(compact->outfile == nullptr);
if (snapshots_.empty()) {
compact->smallest_snapshot = versions_->LastSequence();
} else {
//如果某个快照被外部使用(GetSnapShot),这个快照对应的SnapShotImpl对象会被放
//在SnapshotList中(也就是sequence_number被保存下来了),Compaction的时候
//遇到可以清理的数据,还需要判断要清理数据的seq_number不能大于这些快照中的
//sequence_number,否则会影响夸张数据。
compact->smallest_snapshot = snapshots_.oldest()->sequence_number();
}
Iterator* input = versions_->MakeInputIterator(compact->compaction);
// Release mutex while we're actually doing the compaction work
mutex_.Unlock();
//这里input迭代器的循环遍历是每次取迭代器中最小的key,
//key是指InternalKey,key比较器是InternalKeyComparator,
//InternalKey比较方式是对InternalKey中的User_Key按BytewiseComparator来比较。
//在User_Key相同的情况下,按SequenceNumber来比较,SequenceNumber值大的是小于SequenceNumber值小的,
//所以针对abc_456_1、abc_123_2,abc_456_1是小于abc_123_2的。针对我们对同一个user_key的操作,
//最新对此key的操作是小于之前对此key的操作的。
input->SeekToFirst();
Status status;
ParsedInternalKey ikey;
std::string current_user_key;
bool has_current_user_key = false;
SequenceNumber last_sequence_for_key = kMaxSequenceNumber;
while (input->Valid() && !shutting_down_.load(std::memory_order_acquire)) {
// Prioritize immutable compaction work
//检查并优先compact存在的immutable memtable。
if (has_imm_.load(std::memory_order_relaxed)) {
const uint64_t imm_start = env_->NowMicros();
mutex_.Lock();
if (imm_ != nullptr) {
CompactMemTable();
// Wake up MakeRoomForWrite() if necessary.
background_work_finished_signal_.SignalAll();
}
mutex_.Unlock();
imm_micros += (env_->NowMicros() - imm_start);
}
Slice key = input->key();
//如果当前InternalKey与grandparent层产生overlap的size超过阈值,
//那就停止当前SSTable检查,直接落地并停止遍历。
if (compact->compaction->ShouldStopBefore(key) &&
compact->builder != nullptr) {
status = FinishCompactionOutputFile(compact, input);
if (!status.ok()) {
break;
}
}
// Handle key/value, add to state, etc.
//确定当前key是否要丢弃
bool drop = false;
if (!ParseInternalKey(key, &ikey)) {
//解析key失败。
//针对解析失败的key,这里不丢弃,直接存储。
//目的就是不隐藏这种错误,存储到SSTable中,
//便于后续逻辑去处理。
// Do not hide error keys
current_user_key.clear();
has_current_user_key = false;
last_sequence_for_key = kMaxSequenceNumber;
} else {
//解析InternalKey成功。
if (!has_current_user_key ||
user_comparator()->Compare(ikey.user_key, Slice(current_user_key)) !=0) {
//前后检测的两个InternalKey不一样,
//那就记录这个首次出现的key,
//并将last_sequence_for_key设置为最大。
// First occurrence of this user key
current_user_key.assign(ikey.user_key.data(), ikey.user_key.size());
has_current_user_key = true;
last_sequence_for_key = kMaxSequenceNumber;
}
//如果前后两个key不一样,last_sequence_for_key会被赋值为kMaxSequenceNumber,
if (last_sequence_for_key <= compact->smallest_snapshot) {
//进入此逻辑,说明last_sequence_for_key不是最大值kMaxSequenceNumber,
//也就是当前这个key的user_key(是一个比较旧的userkey)和上一个key的user_key是相同的。
//所以这里就直接丢弃。
// Hidden by an newer entry for same user key
drop = true; // (A)
} else if (ikey.type == kTypeDeletion &&
ikey.sequence <= compact->smallest_snapshot &&
compact->compaction->IsBaseLevelForKey(ikey.user_key)) {
//如果这个InternalKey满足一下三个条件,则可以直接丢弃。
//1.是个Deletionkey。
//2.sequence <= small_snaphshot。
//3.当前compact的level是level-n和level-n+1,
// 如果在level-n+1以上的层已经没有此InternalKey对应的user_key了。
//基于以上三种情况可删除。
//为什么要此条件(IsBaseLevelForKey)判断呢?
//举个例子:
//如果在更高层,还有此InternalKey对应的User_key,
//此时你把当前这个InternalKey删除了,那就会出现两个问题:
//问题1:再次读取删除的key时,就会读取到老的过期的key(这个key的type是非deletion),这是有问题的。
//问题2:再次合并时,但这个key(这个key的type是非deletion)首次被读取时last_sequence_for_key会设置为kMaxSequenceNumber,
// 这样就也不会丢弃。
//以上两个问题好像在更高层的也就是旧的此key的所有userkey的type都是是delete的时候好像是没问题的,
//但这毕竟是少数,原则上为了系统正常运行,我们每次丢弃一个标记为kTypeDeletion的key时,
//必须保证数据库中不存在它的过期key,否则就得将它保留,直到后面它和这个过期的key合并为止,合并之后再丢弃
// For this user key:
// (1) there is no data in higher levels
// (2) data in lower levels will have larger sequence numbers
// (3) data in layers that are being compacted here and have
// smaller sequence numbers will be dropped in the next
// few iterations of this loop (by rule (A) above).
// Therefore this deletion marker is obsolete and can be dropped.
drop = true;
}
last_sequence_for_key = ikey.sequence;
}
#if 0
Log(options_.info_log,
" Compact: %s, seq %d, type: %d %d, drop: %d, is_base: %d, "
"%d smallest_snapshot: %d",
ikey.user_key.ToString().c_str(),
(int)ikey.sequence, ikey.type, kTypeValue, drop,
compact->compaction->IsBaseLevelForKey(ikey.user_key),
(int)last_sequence_for_key, (int)compact->smallest_snapshot);
#endif
//写入不丢弃的key
if (!drop) {
//1.打开SSTable文件
// Open output file if necessary
if (compact->builder == nullptr) {
status = OpenCompactionOutputFile(compact);
if (!status.ok()) {
break;
}
}
//对于要写入的key,从input_取出时,
//应该是当前整个input_中最小的key(此处应该就体现了迭代器封装的好处了)。
//也就是说写入到SSTable是升序的。
if (compact->builder->NumEntries() == 0) {
compact->current_output()->smallest.DecodeFrom(key);
}
compact->current_output()->largest.DecodeFrom(key);
compact->builder->Add(key, input->value());
//待生成的SSTable已超过特定阈值,那么就将此SSTable文件落地。
// Close output file if it is big enough
if (compact->builder->FileSize() >=
compact->compaction->MaxOutputFileSize()) {
status = FinishCompactionOutputFile(compact, input);
if (!status.ok()) {
break;
}
}
}
input->Next();
}
if (status.ok() && shutting_down_.load(std::memory_order_acquire)) {
status = Status::IOError("Deleting DB during compaction");
}
if (status.ok() && compact->builder != nullptr) {
status = FinishCompactionOutputFile(compact, input);
}
if (status.ok()) {
status = input->status();
}
delete input;
input = nullptr;
CompactionStats stats;
stats.micros = env_->NowMicros() - start_micros - imm_micros;
for (int which = 0; which < 2; which++) {
for (int i = 0; i < compact->compaction->num_input_files(which); i++) {
stats.bytes_read += compact->compaction->input(which, i)->file_size;
}
}
for (size_t i = 0; i < compact->outputs.size(); i++) {
stats.bytes_written += compact->outputs[i].file_size;
}
mutex_.Lock();
stats_[compact->compaction->level() + 1].Add(stats);
if (status.ok()) {
status = InstallCompactionResults(compact);
}
if (!status.ok()) {
RecordBackgroundError(status);
}
VersionSet::LevelSummaryStorage tmp;
Log(options_.info_log, "compacted to: %s", versions_->LevelSummary(&tmp));
return status;
}
总结
看到一些对major Compaction流程介绍的文章说当待Compact的key与grandparents层的SSTable文件存在overlap的大小太大的时候会结束本次Compact,这里说明下应该是结束对当前SStable的写入,重新创建一个新的SSTable文件来存此key。