繼續上一篇遺留問題,本篇介紹對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文件流程介紹完畢,壓縮流程剩下一部分就是跨層進行文件壓縮處理。