LevelDB compact 過程

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過程:
在這裏插入圖片描述

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章