继续上一篇遗留问题,本篇介绍对MemTable压缩,在介绍之前先普及一下其他内容。
在存储流程第一篇博客中,有一个方法没有详细说明--MakeRoomForWrite,该函数是保证新插入的数据有足够空间,那么该方法是如何保证的呢?本篇就详细介绍一下该方法。
一、MakeRoomForWrite
/*
* 确保有足够空间可写
* @param force true表示强制立刻写入 false表示延迟写入
* 如果mem_没有可用空间可写,则会重新生成mem_ 旧的mem_转成imm_ 然后启动后台线程
* 进行压缩处理等相关操作
*/
Status DBImpl::MakeRoomForWrite(bool force) {
mutex_.AssertHeld();
assert(!writers_.empty());
bool allow_delay = !force; //是否延迟写入
Status s;
while (true) {
if (!bg_error_.ok()) {//后台线程background 在将imm_写入磁盘(level0时)发生错误
// 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.
// 当文件数目达到8个则进行延迟写入 延迟写入只进行一次
mutex_.Unlock();
env_->SleepForMicroseconds(1000);//直接睡眠1000ms
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
// MemTable的内存是动态扩展,
// 当MemTable已经使用的空间达到了阈值(4M),则不再继续向当前MemTable对象追加数据(需要重新创建)
// 当MemTable已经使用的空间没有达到阈值(4M), 则继续使用
break;
} else if (imm_ != NULL) {/* 表示mem_已满 需要等待imm_持久化到磁盘 */
// We have filled up the current memtable, but the previous
// one is still being compacted, so we wait.
Log(options_.info_log, "Current memtable full; waiting...\n");
bg_cv_.Wait();
} else if (versions_->NumLevelFiles(0) >= config::kL0_StopWritesTrigger) {
// There are too many level-0 files. level-0文件数目太多需要等待压缩
Log(options_.info_log, "Too many L0 files; waiting...\n");
bg_cv_.Wait();
} else {//imm_为空,mem_没有空间可写 else中没有break语句
// 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 = NULL;
s = env_->NewWritableFile(LogFileName(dbname_, new_log_number), &lfile);//创建新的log文件
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_.Release_Store(imm_);
mem_ = new MemTable(internal_comparator_);//创建新的MemTable对象
mem_->Ref();
force = false; // Do not force another compaction if have room
MaybeScheduleCompaction();//启动后台线程 将imm_写到level0
}
}
return s;
}
说明:
1) 该函数主要6个逻辑判断,每个逻辑判断并不是很复杂,在注释中已经明确给出。
2) 最后一个 else语句没有break语句说明,while循环并没有退出,而是进入上面最后一个else if分支然后被阻塞。
3) leveldb中提倡level0层的文件不应该过多,太多影响性能,主要原因是level0层中的文件key是有重叠的,并没有按照顺序存储。所以leveldb默认level0层文件数是8,即config::kL0_SlowdownWritesTrigger。
二、启动压缩流程
2.1、创建独立线程进行压缩
启动压缩流程入口函数为MaybeScheduleCompaction,函数实现如下:
/**
* 尝试调度压缩流程
* 压缩场景原因:
* 一种是某一层级的文件数过多或者文件总大小超过预定门限,
* 另一种是level n 和level n+1重叠严重,无效seek次数太多。(level n 和level n+1的文件,key的范围可能交叉导致)
*/
void DBImpl::MaybeScheduleCompaction() {
mutex_.AssertHeld();
if (bg_compaction_scheduled_) {
// Already scheduled
} else if (shutting_down_.Acquire_Load()) {
// DB is being deleted; no more background compactions
} else if (!bg_error_.ok()) {
// Already got an error; no more changes
} else if (imm_ == NULL &&
manual_compaction_ == NULL &&
!versions_->NeedsCompaction()) {
// No work to be done
} else {
bg_compaction_scheduled_ = true;
env_->Schedule(&DBImpl::BGWork, this); //创建线程 执行压缩
}
}
/**
* 压缩线程线程函数 回调函数
*/
void DBImpl::BGWork(void* db) {
reinterpret_cast<DBImpl*>(db)->BackgroundCall();
}
2.2、执行压缩
通过上一小节可知,真正执行压缩处理的方法是BackgroundCall,下面分析一下该方法内部实现。
/**
* 压缩处理
*/
void DBImpl::BackgroundCall() {
MutexLock l(&mutex_);
assert(bg_compaction_scheduled_);
if (shutting_down_.Acquire_Load()) {
// No more background work when shutting down.
} else if (!bg_error_.ok()) {
// No more background work after a background error.
} else {
BackgroundCompaction();//压缩处理
}
bg_compaction_scheduled_ = false; //表示压缩完成
// Previous compaction may have produced too many files in a level,
// so reschedule another compaction if needed.
MaybeScheduleCompaction(); //再次尝试压缩 因为有可能上一次压缩产生的文件比较多 所以在此进行压缩
bg_cv_.SignalAll();
}
说明:
1) leveldb是支持多线程并发访问的,所以需要判断各种关键状态
2) BackgroundCompaction方法是用于压缩处理
3) 执行完 BackgroundCompaction方法后需要再次调用MaybeScheduleCompaction方法,主要原因是可能由于本次压缩导致某一level层中的文件过多,需要再次压缩。这里是一个递归调用,压缩到最后一层就不在压缩了
2.3、流程图
三、MemTable压缩
3.1、流程图
MemTable压缩实际是将Immutable MemTable写入到ldb文件中,具体流程图如下所示:
3.2、函数实现
/**
* 压缩MemTable
* 将imm_数据压缩到level0中sstable文件里
*/
void DBImpl::CompactMemTable() {
mutex_.AssertHeld();
assert(imm_ != NULL);
// Save the contents of the memtable as a new Table
VersionEdit edit;
Version* base = versions_->current();
base->Ref();
Status s = WriteLevel0Table(imm_, &edit, base); //将imm_写入到文件 将version信息写到MANIFEST中
base->Unref();
if (s.ok() && shutting_down_.Acquire_Load()) {
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_);//更新到Manifest文件中
}
if (s.ok()) {//释放Immutable Table内存
// Commit to the new state
imm_->Unref();
imm_ = NULL;
has_imm_.Release_Store(NULL);
DeleteObsoleteFiles();//删除残余文件
} else {
RecordBackgroundError(s);//后台压缩线程 遇到问题 发起通知
}
}
该函数比较简单,主要实现逻辑在于WriteLevel0Table方法,该方法是将MemTable中的数据写入到文件中,下面我们来重点分析一下该函数具体实现。
四、写入到Level0文件
WriteLeve0Table函数主要将MemTable数据写入到文件中,下面该函数流程图:
/**
* 将MemTable写入到level0文件中
* @param mem MemTable对象
* @param edit 用于写入到MANIFEST文件 版本信息 输出参数
* @param base 当前db version信息
*/
Status DBImpl::WriteLevel0Table(MemTable* mem, VersionEdit* edit,
Version* base) {
mutex_.AssertHeld();
const uint64_t start_micros = env_->NowMicros();
FileMetaData meta;// 保存level0文件 元数据
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中数据写入到ldb文件中 并返回元数据meta
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;//默认放到level0
if (s.ok() && meta.file_size > 0) {
/* 获取用户数据 key 非内部internal_key */
const Slice min_user_key = meta.smallest.user_key();
const Slice max_user_key = meta.largest.user_key();
/**
* 虽然函数名字是将数据写到level0中 但是最终是否写到level0中取决于
* PickLevelForMemTableOutput返回值 该函数会根据key选择合适的层
* 通常会保存在level0,本次存储的最小key和最大key不在level0范围内 就有可能
* 存储到更高层 为什么要这样做呢? 如果可以放到更高层 可以减少压缩频率
*/
if (base != NULL) {
level = base->PickLevelForMemTableOutput(min_user_key, max_user_key);
}
//保存metafiledata 到了这里就确定了文件所属层次
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;
}
4.1、BuildTable
BuildTable方法内部就是创建ldb文件并按照一定格式将数据写入到其中。具体数据格式可参考《leveldb深度剖析-存储结构(2)》。我们了解ldb是按照Block方式组织的,一个Block默认大小是4KB,所以当数据超过4KB就会创建一个Block。这里不在展示相关代码逻辑,只需要了解存储格式就能够比较轻松理解源码。
4.2、PickLevelForMemTableOutput
从MemTable中dump出的文件一定是level0吗?答案是不一定。leveldb中的ldb文件本身没有层次概念,所有的ldb文件都一样,那么如何确定这个文件是在哪一层呢?即由函数PickLevelForMemTableOutput决定。
在上面WriteLevel0Table方法中注释已经很明确给出说明,下面来看一下PickLevelForMemTableOutput内部实现:
/**
* 根据最小key和最大key查找所在层次
* @param smallest_user_key 最小用户数据key
* @param largest_user_key 最大用户数据key
*/
int Version::PickLevelForMemTableOutput(
const Slice& smallest_user_key,
const Slice& largest_user_key) {
int level = 0; //默认存储到level0
/* 如果和level0中key不重叠 则可能保存到更高层中 */
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) {
//进入if分支 表示level层没有冲突 但是level+1层有冲突 则将文件保存在level层
if (OverlapInLevel(level + 1, &smallest_user_key, &largest_user_key)) {
break;
}
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);//overlaps 保存冲突文件元信息
//进入if分支 表示level层和level+1层都没有没有冲突 但是level+2层有冲突
//如果冲突文件大小超过默认值20M则保存在level层中
if (sum > MaxGrandParentOverlapBytes(vset_->options_)) {
break;
}
}
level++;
}
}
return level;
}
说明:
1) 新文件中key与level0中文件key有重叠,则将新文件设置为level0,否则进入2
2) 新文件中key与level1中文件key有重叠,则将新文件设置为level0,否则进入3
3) 新文件中key与level2中文件key有重叠并且重叠文件大小超过20M,则将新文件设置为level0,否则level加1,重新进入2
leveldb为什么要这样设计呢?我的理解如下:
1) level0中文件key可能重叠,文件越多影响性能,所以新文件尽量不放到level0中
2) 为什么压缩MemTable的level最多是2呢?其实kMaxMemCompactLevel有注释,为了提升性能如果层次越大那么open的文件就多比较也就越多
五、总结
至此,MemTable生成ldb文件流程介绍完毕,压缩流程剩下一部分就是跨层进行文件压缩处理。