文章目录
Compaction过程
在LevelDB中,Compaction主要由两种:
- minor compaction:这个就是
immutable
持久化到Level 0层的过程。这里最重要的要求就是高性能,因为其一旦阻塞就会导致memtable
无法写入但是有没有办法转换成immutable
。 - major compaction:负责将磁盘上的SSTable和并,每合并一次,
sstable
中的数据就落到更底一层,数据慢慢被合并到底层的Level
而通过Compaction可以达到以下的几个效果:
- 内存中的数据持久化到磁盘
- 清理荣誉数据
- 通过
compaction
使Level 0层以下的文件层中的数据保持有序,这样便可以通过二分进行数据查找,同时也可以减少待查找的文件数量,提升读效率
minor compaction
刚刚也稍微介绍了以下minor compaction
,为了提升数据持久化的速度,在对immutable
进行持久化时不会考虑不同文件之间的重复和顺序问题,这也使得这个过程变得稍微简单了一些。
1)触发minor compaction的时间
当内存中的memtable size
小于配置的阈值时,数据都会直接更新到memtable。超过大小后,memtable
会转换成 Immutable
,这时会由一个后台线程负责将immutable持久化到磁盘成文 Level 0的SSTable
文件。
这个过程在之前介绍的DBImpl::MakeRoomForWrite
得到实现:
Status DBImpl::MakeRoomForWrite(bool force) {
...
//memtable 转换成immutable
imm_ = mem_;
has_imm_.store(true, std::memory_order_release);
//申请新的memtable
mem_ = new MemTable(internal_comparator_);
mem_->Ref();
force = false; // Do not force another compaction if have room
//触发合并操作
MaybeScheduleCompaction();
}
}
return s;
}
这里我们可以看到调用了函数 DBImpl::MaybeScheduleCompaction
,这个函数会把DBImpl::BGWork
加入后台线程的执行日程之中。而这个DBImpl::BGWork
则会调用DBImpl::BackgroundCall
,而这个DBImpl::BackgroundCall
则会调用函数DBImpl::BackgroundCompaction
,在这个函数里,才会真正的调用合并函数DBImpl::CompactMemTable
。
2)将immutable memtable落盘成SSTable
DBImpl::CompactMemTable
会调用函数WriteLevel0Table
,在这函数里会落盘immutable
:
void DBImpl::CompactMemTable() {
mutex_.AssertHeld();
assert(imm_ != nullptr);
// Save the contents of the memtable as a new Table
VersionEdit edit;
Version* base = versions_->current();
base->Ref();
Status s = WriteLevel0Table(imm_, &edit, base);
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
//记录edit信息
s = versions_->LogAndApply(&edit, &mutex_);
}
if (s.ok()) {
// Commit to the new state
//释放imm_空间
imm_->Unref();
imm_ = nullptr;
has_imm_.store(false, std::memory_order_release);
//清理无效文件
RemoveObsoleteFiles();
} else {
RecordBackgroundError(s);
}
}
而WriteLevel0Table
在落盘immutable
的同时会将文件信息记录到edit
之中,edit里面保存了文件的元信息。这里要提一点的是,假如说新生成的SSTable文件实际上并不总是放到Level 0 层,如果这个新生成的SSTable的key与当前Level 1层的所有文件都没有重叠,则会直接将文件放到Level 1层。
Status DBImpl::WriteLevel0Table(MemTable* mem, VersionEdit* edit,
Version* base) {
mutex_.AssertHeld();
const uint64_t start_micros = env_->NowMicros();
//生成sstable编号,用于构建文件名
FileMetaData meta;
meta.number = versions_->NewFileNumber();
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;
{
mutex_.Unlock();
//更新memtable中的所有数据到xxx.ldb文件追踪
//meta 记录key的范围,file_size等元信息
s = BuildTable(dbname_, env_, options_, table_cache_, iter, &meta);
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());
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) {
//为新生成的sstable选择合适的level,其实这里就是比较最大key和最小key的过程
level = base->PickLevelForMemTableOutput(min_user_key, max_user_key);
}
//level及file meta记录到edit,实际上就是记录到一个vector<[int]level,[FileMetaData]file>
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;
}
3)将edit信息记录到version
WriteLevel0Table
执行完成之后,会将新生成的edit
信息记录到version(version是整个LevelDB的元信息),当前的version作为数据库的一个最新状态,后续的操作都会基于该状态。
void DBImpl::CompactMemTable() {
...
// Replace immutable memtable with the generated Table
if (s.ok()) {
edit.SetPrevLogNumber(0);
edit.SetLogNumber(logfile_number_); // Earlier logs no longer needed
//记录edit信息
s = versions_->LogAndApply(&edit, &mutex_);
}
if (s.ok()) {
// Commit to the new state
//释放imm_空间
imm_->Unref();
imm_ = nullptr;
has_imm_.store(false, std::memory_order_release);
//清理无效文件
RemoveObsoleteFiles();
} else {
RecordBackgroundError(s);
}
}
major compaction
而Major Compaction主要有三种分别为:
-
Manual Compaction:是人工触发的Compaction,由外部接口调用产生,例如在ceph调用的Compaction都是Manual Compaction,实际其内部触发调用的接口
DBImpl::CompactRange
-
Size Compaction:是根据每个level的总文件大小来触发,注意Size Compation的优先级高于Seek Compaction。
-
Seek Compaction:每个文件的 seek miss 次数都有一个阈值,如果超过了这个阈值,那么认为这个文件需要Compact。
其中这些 Compaction 的优先级不一样(详细可以参见 BackgroundCompaction 函数),具体优先级的大小为:
Minor > Manual > Size > Seek
LevelDB 是在 MayBeScheduleCompaction
的 Compation 调度函数中完成各种 Compaction 的调度的,第一个判断的就是 immu_ (也就是 immutable memtable)是不是为 NULL,如果不为 NULL,那么说明有 immutable memtable 存在,那就需要优先将其转化为 level 0 的 sst 文件,否则再看是不是 Manual,否则再是PickCompaction() 函数——它的内部会优先判断是不是有 Size Compaction,如果有就优先处理。
major compation
的设计带来一个明显的好处就是可以清理冗余数据,节省磁盘空间,因为之前被标记删除的数据可以在major compaction
的过程中被清理掉。
Level 0中的数据文件之间是无序的,但是被归并到Level 1之后,数据变得有序,这使读操作需要查询的文件数就会变少。因此,major compaction
带来的另外一个好处就是可以提升读效率。
1)触发major compaction的时机
- Level 0:SSTable文件个数超过指定个数
- Level i:第i层的sstable size总大小超过10iMB。level越大,说明数据越冷,读取的几率越小。
- 对于sstable文件还有seek限制,如果文件多次seek但是一直没有查找到数据,那么就应该被合并了。
上面的这个逻辑在函数VersionSet::Finalize
得到体现,
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) {
//如果是第0层,就直接判断第0层文件是否大于4个,kL0_CompactionTrigger=4
score = v->files_[level].size() /
static_cast<double>(config::kL0_CompactionTrigger);
} else {
// TotalFileSize得到这一层的总文件大小
const uint64_t level_bytes = TotalFileSize(v->files_[level]);
// 其他层就计算这一层的文件占用空间是否大于限制
score =
static_cast<double>(level_bytes) / MaxBytesForLevel(options_, level);
}
if (score > best_score) {
best_level = level;
best_score = score;
}
}
//这里记录当前层数和score,score大于1就表示要合并
v->compaction_level_ = best_level;
v->compaction_score_ = best_score;
}
2) compaction流程
- 选择合适的level及sstable用于合并,这个过程发生在
VersionSet::PickCompaction
筛选文件会根据size_compaction
规则,或者seek_compaction
规则计算应该合并的文件。
首先就是根据刚刚计算得到的score,如果这个score大于1就表示要合并了。我们把size_compaction
置为true
。之后,就是把文件元信息指针FileMetaData
放入inputs[0]
之中了。
const bool size_compaction = (current_->compaction_score_ >= 1);
const bool seek_compaction = (current_->file_to_compact_ != nullptr);
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];
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) {
level = current_->file_to_compact_level_;
c = new Compaction(options_, level);
c->inputs_[0].push_back(current_->file_to_compact_);
} else {
return nullptr;
}
对于seek_compaction
,则是会为每个新的SSTable文件维护一个allowed_seek的初始阈值,表示最多容忍多少次seek miss,当allowed_seeks
递减到小于0的时候,就会将对应文件标记为需要compact
。
bool Version::UpdateStats(const GetStats& stats) {
FileMetaData* f = stats.seek_file;
if (f != nullptr) {
f->allowed_seeks--;
if (f->allowed_seeks <= 0 && file_to_compact_ == nullptr) {
file_to_compact_ = f;
file_to_compact_level_ = stats.seek_file_level;
return true;
}
}
return false;
}
- 根据key重叠的情况扩大输入文件集合
根据 key 重叠情况扩大输入文件集合的基本思想是:所有有重叠的 level+1 层文件都要参与 compact,得到这些文件后,反过来看下,在不增加 level+1 层文件的前提下,能否继续增加 level 层的文件。具体步骤如下:
这个过程发生在函数Version::GetOverlappingInputs
以及函数VersionSet::SetupOtherInputs
之中。其中Version::GetOverlappingInputs
负责扩大Level 0层的范围,VersionSet::SetupOtherInputs
扩大其他层的范围。函数均在VersionSet::PickCompaction
被调用:
void Version::GetOverlappingInputs(int level, const InternalKey* begin,
const InternalKey* end,
std::vector<FileMetaData*>* inputs) {
assert(level >= 0);
assert(level < config::kNumLevels);
inputs->clear();
Slice user_begin, user_end;
if (begin != nullptr) {
user_begin = begin->user_key();
}
if (end != nullptr) {
user_end = end->user_key();
}
const Comparator* user_cmp = vset_->icmp_.user_comparator();
for (size_t i = 0; i < files_[level].size();) {
FileMetaData* f = files_[level][i++];
const Slice file_start = f->smallest.user_key();
const Slice file_limit = f->largest.user_key();
if (begin != nullptr && user_cmp->Compare(file_limit, user_begin) < 0) {
// "f" is completely before specified range; skip it
} else if (end != nullptr && user_cmp->Compare(file_start, user_end) > 0) {
// "f" is completely after specified range; skip it
} else {
inputs->push_back(f);
if (level == 0) {
// Level-0 files may overlap each other. So check if the newly
// added file has expanded the range. If so, restart search.
if (begin != nullptr && user_cmp->Compare(file_start, user_begin) < 0) {
user_begin = file_start;
inputs->clear();
i = 0;
} else if (end != nullptr &&
user_cmp->Compare(file_limit, user_end) > 0) {
user_end = file_limit;
inputs->clear();
i = 0;
}
}
}
}
}
level与level+1层归并压缩之后,最后的文件是要放到level+1层的,在方法SetupOtherInputs
中获取到压缩之后的key范围[all_start,all_limit],并查询出level+2层与level+1层overlap的SSTable,存放与grandparents_中。
- 多路合并
多路合并会将上一步骤选出来的待合并 sstable 中的数据按序整理。在按序整理之后,就会进入函数DBImpl::DoCompactionWork
(调用点在DBImpl::BackgroundCompaction
之中)。这个函数会调用 VersionSet::MakeInputIterator
函数返回了一个迭代器对象,通过遍历该迭代器对象,则可以得到全部有序的 key 集合。
Iterator* VersionSet::MakeInputIterator(Compaction* c) {
ReadOptions options;
options.verify_checksums = options_->paranoid_checks;
options.fill_cache = false;
// Level-0 files have to be merged together. For other levels,
// we will make a concatenating iterator per level.
// TODO(opt): use concatenating iterator for level-0 if there is no overlap
const int space = (c->level() == 0 ? c->inputs_[0].size() + 1 : 2);
//list 存储所有Iterator
Iterator** list = new Iterator*[space];
int num = 0;
for (int which = 0; which < 2; which++) {
if (!c->inputs_[which].empty()) {
//第0层
if (c->level() + which == 0) {
const std::vector<FileMetaData*>& files = c->inputs_[which];
for (size_t i = 0; i < files.size(); i++) {
list[num++] = table_cache_->NewIterator(options, files[i]->number,
files[i]->file_size);
}
} else {
// Create concatenating iterator for the files from this level
list[num++] = NewTwoLevelIterator(
new Version::LevelFileNumIterator(icmp_, &c->inputs_[which]),
&GetFileIterator, table_cache_, options);
}
}
}
assert(num <= space);
Iterator* result = NewMergingIterator(&icmp_, list, num);
delete[] list;
return result;
}
下面的流程就是遍历所有需要待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;
}
参考文献
[1] LevelDB 原理解析:数据的读写与合并是怎样发生的?(在原文基础上增添内容)
[2] 【leveldb】Compact(二 十 三):Major Compaction