leveldb源碼筆記

<p>最近讀了一下leveldb源碼,leveldb最主要的操做就是get/set,所以從get/set的實現入手,瞭解一下實現機制。</p> <p>以前也看過leveldb相關介紹以及別人的分析blog,已經有了必定了解。leveldb如其名,按照層級來組織數據,數據從內存到磁盤一層一層遷移。在內存中是經過skiplist來管理數據,而磁盤上則是一種名爲SSTable(Sorted Strings Table)的結構來存儲數據的。緩存

###DB::Get實現網絡

  • 這個頭文件include/leveldb/db.h定義了DB抽象類,Get接口也在其中,具體實如今db/db_impl.cc文件中。 下面引用的代碼由於篇幅會刪除一些代碼行,完整代碼參考源文件。

<p>代碼:dom

Status DBImpl::Get(const ReadOptions& options,
	                   const Slice& key,
	                   std::string* value) {
	  Status s;
	  MutexLock l(&mutex_);
	  SequenceNumber snapshot;
	  ...
	
	  // Unlock while reading from files and memtables
	  {
	    mutex_.Unlock();
	    // First look in the memtable, then in the immutable memtable (if any).
	    LookupKey lkey(key, snapshot);
	    if (mem->Get(lkey, value, &s)) {
	      // Done
	    } else if (imm != NULL && imm->Get(lkey, value, &s)) {
	      // Done
	    } else {
	      s = current->Get(options, lkey, value, &stats);
	      have_stat_update = true;
	    }
	    mutex_.Lock();
	  }
	
	  if (have_stat_update && current->UpdateStats(stats)) {
	    MaybeScheduleCompaction();
	  }
	  ...
	  return s;
	}

<p>上面是Get的實現函數,省略了一些代碼。Get主要的查詢過程在中間if-else語句分支中。在查詢以前`mutex_.Unlock();`進行了解鎖,是由於數據是隻追加不刪除的,能夠同時讀寫。數據刪除會轉換成一條標記key-deleted的數據追加到庫中。 <p>`SequenceNumber snapshot`爲數據序號,每一條數據都有序號,後追加的序號比以前的序號要大,相同key的數據,序號大的要排在前面,參見db/dbformat.cc `InternalKeyComparator::Compare`函數。 <p>第一個分支`mem`指向一個MemTable,MemTable只有Add和Get兩個接口來操做數據,底層實現爲skiplist,這個`mem`指向可修改的MemTable。 <p>第二個分支`imm`指向一個不可修改的MemTable,`imm`是`mem`達到必定條件後轉換來的,具體的邏輯在db/db_impl.cc `DBImpl::MakeRoomForWrite`函數中。 <p>前面2個分支都是在內存中進行查詢,沒找到就只能到磁盤上查詢。最後一個分支current指向當前的Version,Version包含數據文件的元信息。 <p>最後根據狀況調用`MaybeScheduleCompaction`函數,在後臺對數據進行Compact,將內存的遷到磁盤,對磁盤上的數據進行合併等。 * `Version::Get`實現。這個函數就是上一節最後一個if分支調用的函數,也是查詢磁盤數據的入口。 <p>代碼:函數

Status Version::Get(const ReadOptions& options,
	                    const LookupKey& k,
	                    std::string* value,
	                    GetStats* stats) {		
	  ...		
	  // We can search level-by-level since entries never hop across
	  // levels.  Therefore we are guaranteed that if we find data
	  // in an smaller level, later levels are irrelevant.
	  std::vector<FileMetaData*> tmp;
	  FileMetaData* tmp2;
	  for (int level = 0; level < config::kNumLevels; level++) {
	    size_t num_files = files_[level].size();
	    if (num_files == 0) continue;
	
	    // Get the list of files to search in this level
	    FileMetaData* const* files = &files_[level][0];
	    if (level == 0) {
	      // Level-0 files may overlap each other.  Find all files that
	      // overlap user_key and process them in order from newest to oldest.
	      tmp.reserve(num_files);
	      for (uint32_t i = 0; i < num_files; i++) {
	        FileMetaData* f = files[i];
	        if (ucmp->Compare(user_key, f->smallest.user_key()) >= 0 &&
	            ucmp->Compare(user_key, f->largest.user_key()) <= 0) {
	          tmp.push_back(f);
	        }
	      }
	      if (tmp.empty()) continue;
	
	      std::sort(tmp.begin(), tmp.end(), NewestFirst);
	      ...
	    } else {
	      // Binary search to find earliest index whose largest key >= ikey.
	      uint32_t index = FindFile(vset_->icmp_, files_[level], ikey);
	      if (index >= num_files) {
	        ...
	      } else {
	        tmp2 = files[index];
	        if (ucmp->Compare(user_key, tmp2->smallest.user_key()) < 0) {
	          // All of "tmp2" is past any data for user_key
	          ...
	        } else {
	          files = &tmp2;
	          num_files = 1;
	        }
	      }
	    }
	
	    for (uint32_t i = 0; i < num_files; ++i) {
	     ...
	
	      FileMetaData* f = files[i];
	      last_file_read = f;
	      last_file_read_level = level;
	
	      Saver saver;
	      saver.state = kNotFound;
	      saver.ucmp = ucmp;
	      saver.user_key = user_key;
	      saver.value = value;
	      s = vset_->table_cache_->Get(options, f->number, f->file_size,
	                                   ikey, &saver, SaveValue);
	     ...
	    }
	  }		
	  return Status::NotFound(Slice());  // Use an empty error message for speed
	}

<p>上面的函數主要是對每一個level上的數據從低到高進行查詢,比較新的數據放在低的level。 <p>主for循環全部level,先根據key查找符合要求的文件,因爲Sstable是排序數據,每一個文件都有key的範圍,因此能夠查找包含了查詢key的文件便可。level-0和其餘的level處理方式不太同樣,level-0是直接遍歷,而其餘level調用`FindFile`進行查詢。 <p>找到符合要求的文件以後,進入後一個for循環,經過`vset_->table_cache_->Get`查找全部的文件。 <p>上面提到的`FindFile`使用internal_key即帶序號的查詢key在一層的文件中進行二分查找,找到離查詢key最近且文件largest-key比查詢key大的文件,若是key存在庫的這一層中,那應該會落在這個文件。 <p> `TableCache::Get`比較簡單,先調用了`FindTable`找到對應的Table對象,而後調用Table對象的`InternalGet`函數。下面說`FindTable`函數和`Table::InternalGet`函數。 * `FindTable`函數 <p>代碼:性能

Status TableCache::FindTable(uint64_t file_number, uint64_t file_size,
	                             Cache::Handle** handle) {
	  Status s;
	  char buf[sizeof(file_number)];
	  EncodeFixed64(buf, file_number);
	  Slice key(buf, sizeof(buf));

	  *handle = cache_->Lookup(key);

	  if (*handle == NULL) {
	    std::string fname = TableFileName(dbname_, file_number);
	    RandomAccessFile* file = NULL;
	    ...
	    if (s.ok()) {
	      s = Table::Open(*options_, file, file_size, &table);
	    }
	
	    if (!s.ok()) {
	      ...
	    } else {
	      TableAndFile* tf = new TableAndFile;
	      tf->file = file;
	      tf->table = table;
	      *handle = cache_->Insert(key, tf, 1, &DeleteEntry);
	    }
	  }
	  return s;
	}

<p>`cache_->Lookup(key)`先在cache中查找,`cache_`指向一個LRU的cache,緩存的內容爲打開的文件對象和Table對象的指針,最後一個else語句塊裏`cache_->Insert`把要緩存的內容插入了緩存。 <p>若緩存中沒有要找的Table則調用`Table::Open`打開文件載入Table對象,而後插入緩存。 <p>`Table::Open`代碼:測試

Status Table::Open(const Options& options,
                   RandomAccessFile* file,
                   uint64_t size,
                   Table** table) {
	  ...
	  char footer_space[Footer::kEncodedLength];
	  Slice footer_input;
	  Status s = file->Read(size - Footer::kEncodedLength, Footer::kEncodedLength,
	                        &footer_input, footer_space);
	  ...
	  if (s.ok()) {
	    ...
	    s = ReadBlock(file, opt, footer.index_handle(), &contents);
	    if (s.ok()) {
	      index_block = new Block(contents);
	    }
	  }
	
	  if (s.ok()) {
	    // We've successfully read the footer and the index block: we're
	    // ready to serve requests.
	    Rep* rep = new Table::Rep;
	    rep->options = options;
	    rep->file = file;
	    rep->metaindex_handle = footer.metaindex_handle();
	    rep->index_block = index_block;
	    rep->cache_id = (options.block_cache ? options.block_cache->NewId() : 0);
	    ...
	    *table = new Table(rep);
	    (*table)->ReadMeta(footer);
	  } else {
	    ...
	  }		
	  return s;
	}

<p>`Table::Open`把index-block的內容讀出來緩存起來,若是有meta數據或filter數據,也會讀出來並緩存。`options.block_cache`這個指針若是指向一個cache對象,後面在讀入新的block的時候也會把block緩存起來。 * `Table::InternalGet` <p>代碼:優化

Status Table::InternalGet(const ReadOptions& options, const Slice& k,
	                          void* arg,
	                          void (*saver)(void*, const Slice&, const Slice&)) {
	  Status s;
	  Iterator* iiter = rep_->index_block->NewIterator(rep_->options.comparator);
	  iiter->Seek(k);
	  if (iiter->Valid()) {
	    Slice handle_value = iiter->value();
	    FilterBlockReader* filter = rep_->filter;
	    BlockHandle handle;
	    if (filter != NULL &&
	        handle.DecodeFrom(&handle_value).ok() &&
	        !filter->KeyMayMatch(handle.offset(), k)) {
	      // Not found
	    } else {
	      Iterator* block_iter = BlockReader(this, options, iiter->value());
	      block_iter->Seek(k);
	      if (block_iter->Valid()) {
	        (*saver)(arg, block_iter->key(), block_iter->value());
	      }
	      s = block_iter->status();
	      delete block_iter;
	    }
	  }
	  ...
	  return s;
	}

<p>`Table::InternalGet`先在index-block中找到距離key最近的block,block也有key範圍,查找過程和查找文件相似也是經過二分查找找到最近的block,可是index-block並非全部block的索引,因此還須要進一步到block附近進行查找。 <p>若是找到key附近的block,就對block進行查找。先結合filter判斷key是否不在,如不在直接返回NotFound。而後讀index對應的block,進行二次查找。`iter->Seek(k)`具體能夠參考table/block.cc `Block::Iter::Seek`函數,函數並無進行相等比較,只能定位範圍。因爲`iter->Seek(k)`只能定位到key附近,因此須要調用`(*saver)(arg, block_iter->key(), block_iter->value())`,saver對應上文提到的db/version_set.cc `SaveValue`函數,代碼:ui

static void SaveValue(void* arg, const Slice& ikey, const Slice& v) {
	  Saver* s = reinterpret_cast<Saver*>(arg);
	  ParsedInternalKey parsed_key;
	  if (!ParseInternalKey(ikey, &parsed_key)) {
	    s->state = kCorrupt;
	  } else {
	    if (s->ucmp->Compare(parsed_key.user_key, s->user_key) == 0) {
	      s->state = (parsed_key.type == kTypeValue) ? kFound : kDeleted;
	      if (s->state == kFound) {
	        s->value->assign(v.data(), v.size());
	      }
	    }
	  }
	}

<p>數據文件的編碼格式比較複雜,就不寫了,能夠參考源文件或網絡。 <p>以上就是Get的過程,流程仍是比較長的。網上的測試結果代表leveldb的寫性能高於讀,跟它的磁盤查找關係很大,對於須要頻繁隨機讀的應用仍是要仔細考慮一下性能問題。打開block-cache可能會提升讀性能,相應的就須要消耗內存,把文件放到ssd也是一個優化方案,以上是根據源碼推測的優化方案,沒有具體的實踐,不知效果如何。 <p>有空再把Set接口的實現看一下。 <p>歡迎指出本文的錯誤,也歡迎分享leveldb具體實踐,謝謝!this

相關文章
相關標籤/搜索