[levelDB] Compaction

1、原理分析

前文有述,對於LevelDb來講,寫入記錄操做很簡單,刪除記錄僅僅寫入一個刪除標記就算完事,可是讀取記錄比較複雜,須要在內存以及各個層級文件中依照新鮮程度依次查找,代價很高。爲了加快讀取速度,levelDb採起了compaction的方式來對已有的記錄進行整理壓縮,經過這種方式,來刪除掉一些再也不有效的KV數據,減少數據規模,減小文件數量等。數據庫

     levelDb的compaction機制和過程與Bigtable所講述的是基本一致的,Bigtable中講到三種類型的compaction: minor ,major和full。所謂minor Compaction,就是把memtable中的數據導出到SSTable文件中;major compaction就是合併不一樣層級的SSTable文件,而full compaction就是將全部SSTable進行合併。app

     LevelDb包含其中兩種,minor和major。函數

    咱們將爲你們詳細敘述其機理。源碼分析

    先來看看minor Compaction的過程。Minor compaction 的目的是當內存中的memtable大小到了必定值時,將內容保存到磁盤文件中,圖8.1是其機理示意圖。 大數據

圖8.1 minor compactionui

     從8.1能夠看出,當memtable數量到了必定程度會轉換爲immutable memtable,此時不能往其中寫入記錄,只能從中讀取KV內容。以前介紹過,immutable memtable實際上是一個多層級隊列SkipList,其中的記錄是根據key有序排列的。因此這個minor compaction實現起來也很簡單,就是按照immutable memtable中記錄由小到大遍歷,並依次寫入一個level 0 的新建SSTable文件中,寫完後創建文件的index 數據,這樣就完成了一次minor compaction。從圖中也能夠看出,對於被刪除的記錄,在minor compaction過程當中並不真正刪除這個記錄,緣由也很簡單,這裏只知道要刪掉key記錄,可是這個KV數據在哪裏?那須要複雜的查找,因此在minor compaction的時候並不作刪除,只是將這個key做爲一個記錄寫入文件中,至於真正的刪除操做,在之後更高層級的compaction中會去作。this

     當某個level下的SSTable文件數目超過必定設置值後,levelDb會從這個level的SSTable中選擇一個文件(level>0),將其和高一層級的level+1的SSTable文件合併,這就是major compaction。spa

    咱們知道在大於0的層級中,每一個SSTable文件內的Key都是由小到大有序存儲的,並且不一樣文件之間的key範圍(文件內最小key和最大key之間)不會有任何重疊。Level 0的SSTable文件有些特殊,儘管每一個文件也是根據Key由小到大排列,可是由於level 0的文件是經過minor compaction直接生成的,因此任意兩個level 0下的兩個sstable文件可能再key範圍上有重疊。因此在作major compaction的時候,對於大於level 0的層級,選擇其中一個文件就行,可是對於level 0來講,指定某個文件後,本level中極可能有其餘SSTable文件的key範圍和這個文件有重疊,這種狀況下,要找出全部有重疊的文件和level 1的文件進行合併,即level 0在進行文件選擇的時候,可能會有多個文件參與major compaction。線程

  levelDb在選定某個level進行compaction後,還要選擇是具體哪一個文件要進行compaction,levelDb在這裏有個小技巧, 就是說輪流來,好比此次是文件A進行compaction,那麼下次就是在key range上緊挨着文件A的文件B進行compaction,這樣每一個文件都會有機會輪流和高層的level 文件進行合併。日誌

若是選好了level L的文件A和level L+1層的文件進行合併,那麼問題又來了,應該選擇level L+1哪些文件進行合併?levelDb選擇L+1層中和文件A在key range上有重疊的全部文件來和文件A進行合併。

   也就是說,選定了level L的文件A,以後在level L+1中找到了全部須要合併的文件B,C,D…..等等。剩下的問題就是具體是如何進行major 合併的?就是說給定了一系列文件,每一個文件內部是key有序的,如何對這些文件進行合併,使得新生成的文件仍然Key有序,同時拋掉哪些再也不有價值的KV 數據。

    圖8.2說明了這一過程。

圖8.2 SSTable Compaction

  Major compaction的過程以下:對多個文件採用多路歸併排序的方式,依次找出其中最小的Key記錄,也就是對多個文件中的全部記錄從新進行排序。以後採起必定的標準判斷這個Key是否還須要保存,若是判斷沒有保存價值,那麼直接拋掉,若是以爲還須要繼續保存,那麼就將其寫入level L+1層中新生成的一個SSTable文件中。就這樣對KV數據一一處理,造成了一系列新的L+1層數據文件,以前的L層文件和L+1層參與compaction 的文件數據此時已經沒有意義了,因此所有刪除。這樣就完成了L層和L+1層文件記錄的合併過程。

  那麼在major compaction過程當中,判斷一個KV記錄是否拋棄的標準是什麼呢?其中一個標準是:對於某個key來講,若是在小於L層中存在這個Key,那麼這個KV在major compaction過程當中能夠拋掉。由於咱們前面分析過,對於層級低於L的文件中若是存在同一Key的記錄,那麼說明對於Key來講,有更新鮮的Value存在,那麼過去的Value就等於沒有意義了,因此能夠刪除。

2、源碼分析

代碼流程簡述:

Compaction

在leveldb中compaction主要包括Manual Compaction和Auto Compaction,在Auto Compaction中又包含了MemTable的Compaction和SSTable的Compaction。

Manual Compaction

leveldb中manual compaction是用戶指定須要作compaction的key range,調用接口CompactRange來實現,它的主要流程爲:

  1. 計算和Range有重合的MaxLevel
  2. 從level 0 到 MaxLevel依次在每層對這個Range作Compaction
  3. 作Compaction時會限制選擇作Compaction文件的大小,這樣可能每一個level的CompactRange可能須要作屢次Compaction才能完成
SSTable Compaction
  1. 啓動條件

    • 每一個Level的文件大小或文件數超過了這個Level的限制(L0對比文件個數,其它Level對比文件大小。主要是由於L0文件之間可能重疊,文件過多影響讀訪問,而其它level文件不重疊,限制文件總大小,能夠防止一次compaction IO太重)。
    • 含有被尋道次數超過必定閾值的文件(這個是指讀請求查找可能去讀多個文件,若是最開始讀的那個文件未查找到,那麼這個文件就被認爲尋道一次,當文件的尋道次數達到必定數量時,就認爲這個文件應該去作compaction)
    • 條件1的優先級高於條件2
  2. 觸發條件

    • 任何改變了上面兩個條件的操做,都會觸發Compaction,即調用MaybeScheduleCompaction
    • 涉及到第一個條件改變,就是會改變某層文件的文件數目或大小,而只有Compaction操做以後纔會改變這個條件
    • 涉及到第二個條件的改變,多是讀操做和scan操做(scan操做是每1M數據採樣一次,得到讀最後一個key所尋道的文件,1M數據的cost大約爲一次尋道)
  3. 文件選取

    • 每一個level都會記錄上一次Compaction選取的文件所含Key的最大值,做爲下次compaction選取文件的起點
    • 對於根據啓動條件1所作的Compaction,選取文件就從上次的點開始選取,這樣保證每層每一個文件都會選取到
    • 對於根據啓動條件2所作的Compaction,須要作compaction的文件自己就已經肯定了
    • Level + 1層文件的選取,就是和level層選取的文件有重合的文件

    在leveldb中在L層會選取1個文件,理論上這個文件最多覆蓋的文件數爲12個(leveldb中默認一個文件最大爲2M,每層的最大數據量按照10倍增加。這樣L層的文件在未對齊的狀況下最多覆蓋L+1層的12個文件),這樣能夠控制一次Compaction的最大IO爲(1+12)* 2M讀IO,總的IO不會超過52M

MemTable Compaction

MemTable Compaction最重要的是產出的文件所在層次的選擇,它必須知足以下條件: 假設最終選擇層次L,那麼文件必須和[0, L-1]全部層的文件都沒有重合,且對L+1層文件的覆蓋不能超過必定的閾值(保證Compaction IO可控)

Compaction 文件產出
  1. 何時切換產出文件

    • 文件大小達到必定的閾值
    • 產出文件對Level+2層有交集的全部文件的大小超過必定閾值
  2. key丟棄的兩個條件

    • last_sequence_for_key <= smallest_snapshot (有一個更新的一樣的user_key比最小快照要小)
    • key_type == del && key <= smallest_snapshot && IsBaseLevelForKey(key的類型是刪除,且這個key的版本比最小快照要小,而且在更高Level沒有一樣的user_key)

瞭解了compaction的一些原理和機制之後咱們該回到代碼來看看具體的代碼流程是怎麼樣的,首先回到DBimpl中的MakeRoomForWrite

 1 Status DBImpl::MakeRoomForWrite(bool force) {
 2   bool allow_delay = !force;
 3   Status s;
 4   while (true) {
 5     if (!bg_error_.ok()) {
 6       // Yield previous error
 7       s = bg_error_;
 8       break;
 9     } else if (
10         allow_delay &&
11         versions_->NumLevelFiles(0) >= config::kL0_SlowdownWritesTrigger) {
12           // 當L0的文件數量要達到閾值的時候,咱們每次寫入都延遲1ms,
13            // 這樣能夠爲後臺的compaction騰出必定的cpu(當後臺compaction
14          //和當前線程是使用的一個內核的時候)這樣能夠下降寫入延遲的方差
15           //由於延遲被分攤到多個寫上面,而不是在幾個甚至一個寫的時候
16       env_->SleepForMicroseconds(1000);
17       allow_delay = false; // 每次寫只容許延遲一次
18     } else if (!force &&  //當前mmetable的佔用量未達到閾值
19                (mem_->ApproximateMemoryUsage() <= options_.write_buffer_size)) {
20       break;
21     } else if (imm_ != NULL) {
22       // 上一次memtable的compaction還沒有結束,等待後臺compaction完成
23        // 由於compaction的過程爲 mem ->imm 完成後刪除imm
24       bg_cv_.Wait();
25     } else if (versions_->NumLevelFiles(0) >= config::kL0_StopWritesTrigger) {
26       // level 0的文件數量超過閾值,等待後臺compaction完成
27       bg_cv_.Wait();
28     } else {
29       // memtable達到閾值,新生成日誌和memtable,並將原先的mem轉化爲imm給後臺compact
30       s = env_->NewWritableFile(LogFileName(dbname_, new_log_number), &lfile);
31     
32       delete log_;
33       delete logfile_;
34       logfile_ = lfile;
35       logfile_number_ = new_log_number;
36       log_ = new log::Writer(lfile);
37       imm_ = mem_;
38       has_imm_.Release_Store(imm_);
39       mem_ = new MemTable(internal_comparator_);
40       mem_->Ref();
41       force = false;             // Do not force another compaction if have room
42       MaybeScheduleCompaction(); //觸發後臺compaction
43     }
44   }
45   return s;
46 }

MaybeScheduleCompaction函數只是簡單判斷後臺線程是否已經啓動和一些其餘的錯誤判斷,若是未啓動則啓動後臺compaction線程。這個compaction線程的實如今DBImpl::BackgroundCall,這個函數也只是簡單的調用實現了compaction實際邏輯的函數BackgroundCompaction,咱們這裏就來仔細分析一下這個函數

void DBImpl::BackgroundCompaction() {
  if (imm_ != NULL) { //有轉化的memtable,直接將MemTable寫入SSTable即返回
    CompactMemTable();
    return;
  }
  if (is_manual) { //用戶主動(手動)觸發的compaction
    ManualCompaction* m = manual_compaction_;
   //取得進項compact的輸入文件生成compaction類
    c = versions_->CompactRange(m->level, m->begin, m->end);
    m->done = (c == NULL);
    if (c != NULL) {
     //取得level中最大的一個key
      manual_end = c->input(0, c->num_input_files(0) - 1)->largest;
    }
  } else {
    c = versions_->PickCompaction();
  }
  if (c == NULL) {
  } else if (!is_manual && c->IsTrivialMove()) {
  //若是不是主動觸發的,而且level中的輸入文件與level+1中無重疊,且與level + 2中重疊不大於
  //kMaxGrandParentOverlapBytes = 10 * kTargetFileSize,直接將文件移到level+1中
    c->edit()->DeleteFile(c->level(), f->number);
    c->edit()->AddFile(c->level() + 1, f->number, f->file_size,
                       f->smallest, f->largest);
    status = versions_->LogAndApply(c->edit(), &mutex_);    //寫入version中,稍後分析
  } else {//不然調用DoCompactionWork進行Compact輸入文件
    CompactionState* compact = new CompactionState(c);
    status = DoCompactionWork(compact);
    CleanupCompaction(compact);      //清理compact過程當中的臨時變量
    c->ReleaseInputs();               //清除輸入文件描述符
    DeleteObsoleteFiles();            //刪除無引用的文件
  }
  delete c;
  if (is_manual) {
    ManualCompaction* m = manual_compaction_;
    if (!status.ok()) {//若是compaction出錯,也將手動的compaction標記爲done
      m->done = true;
    }
    if (!m->done) {//若是沒有完成也僅僅記錄基本狀態,感受manual的形式未實現完整邏輯
      m->tmp_storage = manual_end;
      m->begin = &m->tmp_storage;
    }
    manual_compaction_ = NULL;
  }
}

leveldb知足其內部一些閾值條件後觸發的compaction是如何選擇輸入文件的呢?這個邏輯在中,下面咱們來仔細的分析一下

Compaction* VersionSet::PickCompaction() {
 //每次compact完成在VersionSet::Finalize中計算每一個level中TotalFileSize / MaxBytesForLevel
 // 的值,而且將最大的值最爲compaction_score_ ,和compaction_level_
  const bool size_compaction = (current_->compaction_score_ >= 1);  
  //對於每一個SSTable會有一個 容許seek的次數 (f->file_size / 16384)超過這麼屢次會將其設置爲
  const bool seek_compaction = (current_->file_to_compact_ != NULL);
  // 這兩種可能致使的compaction中,咱們優先compact第一種狀況的
  if (size_compaction) {
    level = current_->compaction_level_;
    c = new Compaction(level);
    // 查找第一個包含比上次已經compact的最大key大的key的文件
    for (size_t i = 0; i < current_->files_[level].size(); 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()) {
      // 若是上次已是最大的key,那麼回到第一個文件開始compact
      c->inputs_[0].push_back(current_->files_[level][0]);
    }
  } else if (seek_compaction) {//若是是查找致使的,直接將致使compact的文件加入inputs_[0]
    level = current_->file_to_compact_level_;
    c = new Compaction(level);
    c->inputs_[0].push_back(current_->file_to_compact_);
  } else {
    return NULL;
  }
  c->input_version_ = current_;
  c->input_version_->Ref();
  // 若是是level 0 則還需查找level 0中其餘和輸入文件重疊的文件
  if (level == 0) {
    GetRange(c->inputs_[0], &smallest, &largest);
    current_->GetOverlappingInputs(0, &smallest, &largest, &c->inputs_[0]);
  }
  SetupOtherInputs(c); //嘗試加入level中新的文件,條件爲再也不與level+1中新的文件重疊,這個函數已經分析
  return c;
}

選擇好了須要進行Compaction的的文件之後,就該調用實際的Compaction過程了,咱們來分析其邏輯,過程比較長可是隻要仔細細心的閱讀,其處理的邏輯並不複雜,主要是遍歷全部輸入文件,而後將相同的能夠進行合併,以及刪除一些無用的delete操做等。

  1 Status DBImpl::DoCompactionWork(CompactionState* compact) {
  2     //將snapshot相關的內容記錄到compact信息中
  3   if (snapshots_.empty()) {
  4     compact->smallest_snapshot = versions_->LastSequence();
  5   } else {
  6     compact->smallest_snapshot = snapshots_.oldest()->number_;
  7   }
  8   //遍歷全部inputs文件
  9   Iterator* input = versions_->MakeInputIterator(compact->compaction);
 10   for (; input->Valid() && !shutting_down_.Acquire_Load(); ) {
 11     // 每次都判斷若是有memtable 須要compact,先compact memtable
 12     if (has_imm_.NoBarrier_Load() != NULL) {
 13       if (imm_ != NULL) { 
 14         CompactMemTable();
 15         bg_cv_.SignalAll(); // Wakeup 等待空間的線程
 16       }
 17     }
 18     Slice key = input->key();
 19     if (compact->compaction->ShouldStopBefore(key) &&
 20         compact->builder != NULL) { //當前(level +1)生成的文件和level + 2中有過多的重疊
 21       status = FinishCompactionOutputFile(compact, input); //寫當前文件到磁盤
 22       if (!status.ok()) {
 23         break;
 24       }
 25     }
 26     // Handle key/value, add to state, etc.
 27     bool drop = false;
 28     if (!ParseInternalKey(key, &ikey)) {
 29       // 解碼錯誤,清除以前的狀態
 30       current_user_key.clear();
 31       has_current_user_key = false;
 32       last_sequence_for_key = kMaxSequenceNumber;
 33     } else {
 34       if (!has_current_user_key ||
 35           user_comparator()->Compare(ikey.user_key,
 36                                      Slice(current_user_key)) != 0) {
 37         // 第一次出現的key,將seq設置爲最大標記新key開始
 38         current_user_key.assign(ikey.user_key.data(), ikey.user_key.size());
 39         has_current_user_key = true;
 40         last_sequence_for_key = kMaxSequenceNumber;
 41       }
 42         //由於第一次出現會將last seq設置爲最大,表示上一個key的關於seq的比較結束
 43       if (last_sequence_for_key <= compact->smallest_snapshot) {
 44         // Hidden by an newer entry for same user key
 45         drop = true; // (A)
 46       } else if (ikey.type == kTypeDeletion &&  
 47                  ikey.sequence <= compact->smallest_snapshot &&               //無snapshot引用
 48                  compact->compaction->IsBaseLevelForKey(ikey.user_key)) { //(1)
 49         // For this user key:
 50         // (1) there is no data in higher levels
 51         // 而咱們知道在底層的文件中seq會更大,正在被compact的相同的key會稍後標記這個爲刪除(ruleA)
 52         drop = true;
 53       }
 54       last_sequence_for_key = ikey.sequence; 
 55     }
 56 
 57     if (!drop) {
 58       // 第一次進入compact或者上次文件剛剛寫到磁盤,新建一個文件和table_builder
 59       if (compact->builder == NULL) {
 60         status = OpenCompactionOutputFile(compact);
 61         if (!status.ok()) {
 62           break;
 63         }
 64       }
 65     //新文件,記錄當前key 爲 整個文件的smallest
 66       if (compact->builder->NumEntries() == 0) {
 67         compact->current_output()->smallest.DecodeFrom(key);
 68       }
 69       //每遍歷到一個就將其記錄爲largest
 70       compact->current_output()->largest.DecodeFrom(key);
 71       compact->builder->Add(key, input->value());
 72       // 超過level的閾值大小,將文件寫到磁盤
 73       if (compact->builder->FileSize() >= compact->compaction->MaxOutputFileSize()) {
 74         status = FinishCompactionOutputFile(compact, input);
 75         if (!status.ok()) {
 76           break;
 77         }
 78       }
 79     }
 80     input->Next();
 81   }
 82 //判斷狀態和將未寫到磁盤的數據寫入磁盤
 83   if (status.ok() && shutting_down_.Acquire_Load()) {
 84     status = Status::IOError("Deleting DB during compaction");
 85   }
 86   if (status.ok() && compact->builder != NULL) {
 87     status = FinishCompactionOutputFile(compact, input);
 88   }
 89   if (status.ok()) {
 90     status = input->status();
 91   }
 92   delete input;
 93   input = NULL;
 94   CompactionStats stats;
 95   stats.micros = env_->NowMicros() - start_micros - imm_micros;
 96   for (int which = 0; which < 2; which++) {//計算本次Compaction讀入文件的總大小
 97     for (int i = 0; i < compact->compaction->num_input_files(which); i++) {
 98       stats.bytes_read += compact->compaction->input(which, i)->file_size;
 99     }
100   }
101   for (size_t i = 0; i < compact->outputs.size(); i++) {
102     stats.bytes_written += compact->outputs[i].file_size;
103   }//本次Compaction寫出文件的總大小
104   mutex_.Lock();
105   stats_[compact->compaction->level() + 1].Add(stats);
106   if (status.ok()) {//記錄統計信息以及將Compaction致使的文件變更記錄到versionedit中
107     status = InstallCompactionResults(compact);
108   }
109   return status;
110 }

SSTable的Compaction就分析完了,關於Compaction還剩下MemTable的Compaction,或者也能夠將其說明爲Memtable的dump爲SSTable。再分析完上面的SSTable Compaction後你就發現MemTable的Compaction是如此之簡單了,咱們簡單羅列一下

 1 void DBImpl::CompactMemTable() {
 2   Status s = WriteLevel0Table(imm_, &edit, base);
 3   // Replace immutable memtable with the generated Table
 4   if (s.ok()) {
 5     edit.SetPrevLogNumber(0);
 6     edit.SetLogNumber(logfile_number_); // Earlier logs no longer needed
 7     s = versions_->LogAndApply(&edit, &mutex_);
 8   }
 9   if (s.ok()) {
10     // Commit to the new state
11     imm_->Unref();
12     imm_ = NULL;
13     has_imm_.Release_Store(NULL);
14     DeleteObsoleteFiles();
15   } else {
16     RecordBackgroundError(s);
17   }
18 }

這個邏輯中就一個主要的函數WriteLevel0Table,其流程以下:

 1 Status DBImpl::WriteLevel0Table(MemTable* mem, VersionEdit* edit, Version* base) { 
 2   meta.number = versions_->NewFileNumber();
 3   pending_outputs_.insert(meta.number);
 4   Iterator* iter = mem->NewIterator();
 5   //新生成一個Table_builder負責寫文件
 6     s = BuildTable(dbname_, env_, options_, table_cache_, iter, &meta);
 7 
 8   // Note that if file_size is zero, the file has been deleted and 
 9   // should not be added to the manifest.
10   int level = 0;
11   if (s.ok() && meta.file_size > 0) {
12     const Slice min_user_key = meta.smallest.user_key();
13     const Slice max_user_key = meta.largest.user_key();
14     if (base != NULL) {
15       /* 找到一個當層未overlap 且上冊overlap 不會過多(kMaxGrandParentOverlapBytes)的層返回*/ 
16       level = base->PickLevelForMemTableOutput(min_user_key, max_user_key);
17     }
18     //將文件加到versionedit中
19     edit->AddFile(level, meta.number, meta.file_size,
20                   meta.smallest, meta.largest);
21   }
22   CompactionStats stats;
23   stats.micros = env_->NowMicros() - start_micros;
24   stats.bytes_written = meta.file_size;
25   stats_[level].Add(stats);
26   return s;
27 }

這裏有一個惟一須要注意的是——將Memtable dump到磁盤之後並非如文檔描述的「將新的SSTable加到level 0中.」,而是會用一個函數PickLevelForMemTableOutput選擇一個最高的能夠將這個SSTable放入的level中。通常來講會是level 0,可是仍是存在一些特殊狀況能夠將其放到更高的level中,這樣能夠下降Compaction的頻率。PickLevelForMemTableOutput的邏輯簡單,請讀者自行閱讀。

至此comaction流程相關的函數就分析完了,本節內容比較多,可是隻要靜下心來慢慢品讀理解仍是不難的。至此leveldb中剩下的還有recover,new (新建一個數據庫)、snapshot、get相關的代碼沒有分析了。咱們在compaction的分析過程當中涉及到了不少有關version的類、方法、結構,leveldb的vesion是整個系統極其重要的一環,並且recovery,snapshot,get在必定程度上都會依賴於version的實現,因此接下來的文章準備對version相關的內容進行介紹。

相關文章
相關標籤/搜索