LevelDB的一個重要特性就是數據的分層,由於數據的分層, 越舊的數據處在越大的層級,越新的數據在越小的層級。
在查詢數據的時候, 最先讀取MemTable裏面的數據, 然後是L0的SSTable裏面, 接着是L1, L2直到最大的層級。在分層設計中, 越往上層,數據的容量越大, 大約Ln是Ln-1層數據的10倍。 在各個層級的SSTable文件, 只有L0層的數據是有MemTable直接flush到磁盤上, 其它層的數據是經過compaction過程進行排序整理產生的。這意味着L0層以上的數據, 各個SSTable文件內的數據是有序且不會重疊的。
因此, compaction的過程是產生SSTable的過程, 分爲2中情況:
- 由MemTable到SSTable的flush過程, 也被成爲Minor Compaction;
- 由Ln層的SSTable到Ln+1層的SSTable的數據重排過程, 也被稱爲Major Compaction;
Minor Compaction
LevelDB設定了L0的容量, 以及觸發L0 compaction的條件:
// Level-0 compaction is started when we hit this many files.
static const int kL0_CompactionTrigger = 4;
// Soft limit on number of level-0 files. We slow down writes at this point.
static const int kL0_SlowdownWritesTrigger = 8;
// Maximum number of level-0 files. We stop writes at this point.
static const int kL0_StopWritesTrigger = 12;
默認情況下, L0的SSTable的文件個數是4個, 大於4個就可能開始compaction, 當L0文件的個數大於8個, 應用層的寫入速度會降下來, 以避免L0文件數太多, 當L0文件數大於12個, 前端的寫入停止。
在LevelDB中, 不管是Put, 還是Delete, 都是調用Write來進行寫入, 在寫入之前, 都會調用MakeRoomForWrite來判斷如何進行處理, 是直接寫入還是slowdown, 還是送至前端寫入?
其實現過程如下圖:
從上圖可以看到, Minor Compaction發生在前面4個判斷失敗之後, 它會把當前正在用作寫入的MemTable轉換爲一個只讀的內存數據, 同時產生一個新的MemTable以及與其對應的log文件, 後續的新的寫入都轉移到新產生的MemTable文件內。 同時, 它會產生一個Compact Schedule, 來觸發後臺的線程來將im-memTable
寫入磁盤。
Major Compaction
LevelDB 在數據庫啓動的時候, 會指定Env, 比如LINUX系統下會產生一個PosixEnv對象,它定義了一個schedule函數來構成函數隊列, 所有的後端執行的函數, 會放進該隊列裏面來一個一個地執行:
void PosixEnv::Schedule(void (*function)(void*), void* arg) {
PthreadCall("lock", pthread_mutex_lock(&mu_));
// Start background thread if necessary
if (!started_bgthread_) {
started_bgthread_ = true;
PthreadCall(
"create thread",
pthread_create(&bgthread_, NULL, &PosixEnv::BGThreadWrapper, this));
}
// If the queue is currently empty, the background thread may currently be
// waiting.
if (queue_.empty()) {
PthreadCall("signal", pthread_cond_signal(&bgsignal_));
}
// Add to priority queue
queue_.push_back(BGItem());
queue_.back().function = function;
queue_.back().arg = arg;
PthreadCall("unlock", pthread_mutex_unlock(&mu_));
}
// PosixEnv::BGThreadWrapper包裝了PosixEnv::BGThread() 函數
// 該函數等待加入隊列的函數, 逐個處理
void PosixEnv::BGThread() {
while (true) {
// Wait until there is an item that is ready to run
PthreadCall("lock", pthread_mutex_lock(&mu_));
while (queue_.empty()) {
PthreadCall("wait", pthread_cond_wait(&bgsignal_, &mu_));
}
void (*function)(void*) = queue_.front().function;
void* arg = queue_.front().arg;
queue_.pop_front();
PthreadCall("unlock", pthread_mutex_unlock(&mu_));
(*function)(arg); 、、 根據註冊的函數執行callback函數
}
}
在這裏, compaction指定的處理函數是DBImpl::BGWork(), 經過DBImpl::BackgroundCall(), 最終由DBImpl::BackgroundCompaction()來進行compaction的任務。
void DBImpl::BackgroundCompaction() {
...
if (is_manual) {
}
else {
c = versions_->PickCompaction();
}
CompactionState* compact = new CompactionState(c);
status = DoCompactionWork(compact);
if (!status.ok()) {
RecordBackgroundError(status);
}
CleanupCompaction(compact);
c->ReleaseInputs();
DeleteObsoleteFiles();
}
可以看到, 通過VersionSet::PickCompaction()來蒐集本次compaction的input集合,LevelDB中,Compaction操作有兩種觸發方式:
- 某一level的文件數太多
- 某一文件的查找次數超過允許值;
因此,在VersionSet::PickCompaction()中便有了代碼中的size_compaction和seek_compaction的判斷。在進行合併時,將優先考慮文件數過多的情況。
最後有DBImplement::DoCompactionWork()完成input集合數據的遍歷以及寫入新的SSTable文件裏面。
如下我們給出一個簡化版的compaction過程: