[leveldb] 2.open操做介紹

在看源代碼以前, 先了解設計結構是必須的, 這就繞不開著名的LSM Tree了. 我在閱讀了原做者論文BigTable論文以後, 一開始最驚奇的是"僞代碼"呢? 沒有. 其實LSM Tree與其說是某種數據結構/算法, 倒不如說是一種設計思路, 用日誌和批量寫入來替代索引更新, 達到經過犧牲隨機查詢速度換取更迅捷地寫入的效果. 動機是機械硬盤時代, seek操做是昂貴的, 由於須要馬達轉動磁盤. 固態硬盤時代, 狀況極大地好轉了. 但仍然不可避免的是順序寫入一樣快過隨機的.程序員

強烈建議閱讀LSM Tree相關文章, 太基本的我就不重複介紹了, 下面講點思考.算法

全部數據直接寫入memtable並打log, 當memtable足夠大的時候, 變爲immemtable, 開始往硬盤挪, 成爲SSTable. 這就是LSM Tree僅有的所有. 你能夠用任何有道理的數據結構來表示memtable, immemtable和SSTable. Google選擇用跳錶實現memtable和immemtable, 用有序行組來實現SSTable.數據庫

一點也不驚喜吧~ 原論文1996年發表, 過了好多年才被Google工程師發掘. 問題太嚴重了. 首先, 搜索key最差時要發瘋同樣從memtable讀到immemtable, 再到全部SSTable. 其次, SSTable要怎麼有效merge(Google稱之爲"major compaction")呢? 數據庫寫啊寫, 有10G了, 新來了一個immemtable要歸併, 一言不合重寫10個多G? 對此, 原論文描述了一種多組件版本, 下降了瞬時IO壓力, 但總IO卻更高了, 沒解決什麼大問題.緩存

Google打了兩個加強補丁.數據結構

1. 添加BloomFilter, 這樣能夠提高全庫掃描的速度, 確定沒這個key的SSTable直接跳過.多線程

2. leveled compaction, 把SSTable分紅不一樣的等級. 除等級0之外, 其他各等級的SSTable不會有重複的key.app

這能夠說是最重要最有用的改動(否則爲啥叫LevelDB?). 想象一下, 若是永遠只有一個SSTable, 我要把新immemtable歸併進去, 就要重寫這個SSTable. 數據有多大, 這個SSTable也會有多大, 那還怎麼合併?函數

聰明的童鞋能夠說那把SSTable分紅若干份, 每份2MB. 但wrost case同樣悲劇. 好比, 當前這個immemtable剛好永遠有一個key與任意SSTable中至少一個key重複. Ops! 又回到了剛剛重寫全庫的case了.oop

Google的作法則讓每次compaction波及到的範圍是可預期的. 官方文檔摘抄: "The compaction picks a file from level L and all overlapping files from the next level L+1". 這就很是優雅了! 數據庫一個老大難題就是怎麼釋放被刪除記錄的空間? LevelDB這種不當即釋放只按等級延遲合併的方法是很高明的, 沒有任何隨機讀寫操做, 機制上又很簡單, 還不須要bookkeeping.學習

在第一部分的最後糾正下網上不少博文都有錯的點(源代碼證明). compaction不必定會清空全部deletion maker. 這個思考下就明白了. 若是下級還有相同key的數據, 就把deletion maker清了, 應該刪除的數據不是又莫名其妙恢復了麼?

 958-967行,

} else if (ikey.type == kTypeDeletion && ikey.sequence <= compact->smallest_snapshot && compact->compaction->IsBaseLevelForKey(ikey.user_key)) { // For this user key: // (1) there is no data in higher levels // (2) data in lower levels will have larger sequence numbers // (3) data in layers that are being compacted here and have // smaller sequence numbers will be dropped in the next // few iterations of this loop (by rule (A) above). // Therefore this deletion marker is obsolete and can be dropped. 

------

理解了大致設計, 啃代碼的時間到了. 跟我一塊兒看看leveldb::Status status = leveldb::DB::Open(options, "testdb", &db);會觸發什麼模塊吧.

leveldb::DB::Open來自 1490行,

Status DB::Open(const Options& options, const std::string& dbname, DB** dbptr) { // static工廠函數 *dbptr = NULL; DBImpl* impl = new DBImpl(options, dbname); 

源代碼有幾點習慣挺好的, 值得學習.

  • 提供給外部的接口通常都要作成工廠函數, 避免我以爲有點蠢萌的兩步構造.
  • literal type(int, float, void*...)不容許傳引用, int a=1; F(a) vs F(&a), 後者更清晰.
  • 老是考慮下是否是要禁止複製, 是的話寫上private: A(const A&); void operator=(const A&);
  • 單參構造函數加explicit.

接上, new而後跳到117行的構造函數,

 1 DBImpl::DBImpl(const Options& raw_options, const std::string& dbname)
 2     : env_(raw_options.env), // Env* const
 3       internal_comparator_(raw_options.comparator), // const InternalKeyComparator
 4       internal_filter_policy_(raw_options.filter_policy), // const InternalFilterPolicy
 5       options_(SanitizeOptions(dbname, &internal_comparator_, // const Options
 6                                &internal_filter_policy_, raw_options)),
 7       owns_info_log_(options_.info_log != raw_options.info_log), // bool
 8       owns_cache_(options_.block_cache != raw_options.block_cache), // bool
 9       dbname_(dbname), // const std::string
10       db_lock_(NULL), // FileLock*
11       shutting_down_(NULL), // port::AtomicPointer
12       bg_cv_(&mutex_), // port::CondVar
13       mem_(NULL), // MemTable*
14       imm_(NULL), // MemTable*
15       logfile_(NULL), // WritableFile*
16       logfile_number_(0), // uint64_t
17       log_(NULL), // log::Writer*
18       seed_(0), // uint32_t
19       tmp_batch_(new WriteBatch), // WriteBatch*
20       bg_compaction_scheduled_(false), // bool
21       manual_compaction_(NULL) { // ManualCompaction*
22   has_imm_.Release_Store(NULL);
23 
24   // Reserve ten files or so for other uses and give the rest to TableCache.
25   const int table_cache_size = options_.max_open_files - kNumNonTableCacheFiles;
26   table_cache_ = new TableCache(dbname_, &options_, table_cache_size);
27 
28   versions_ = new VersionSet(dbname_, &options_, table_cache_,
29                              &internal_comparator_);
30 }

Google C++ Style雖然禁止函數默認參數, 但容許你扔個Options.

解釋下成員變量的含義,

  • env_, 負責全部IO, 好比創建文件
  • internal_comparator_, 用來比較不一樣key的大小
  • internal_filter_policy_, 可自定義BloomFilter
  • options_, 將調用者傳入的options再用一個函數調整下, 可見Google程序員也不是盡善盡美的... 庫的做者要幫忙去除錯誤參數和優化...
  • db_lock_, 文件鎖
  • shutting_down_, 基於memory barrier的原子指針
  • bg_cv_, 多線程的條件
  • mem_ = memtable, imm = immemtable
  • tmp_batch_, 全部Put都是以batch寫入, 這裏創建個臨時的
  • manual_compaction_, 內部開發者調用時的魔法參數, 能夠不用理會

我決定先搞懂memory barrier的原子指針再繼續分析, 就先到這了.

我之前歷來沒有C++多線程的經驗, 藉着看源碼的機會, 纔有機會了解. 曾今工做時, 我寫Python爬蟲就用thread-safe隊列, 覺得原子性全是靠鎖實現的. 所謂的無鎖就是先修改再檢查要不要反悔的樂觀鎖. 我錯了, X86 CPU的賦值(Store)和讀取(Load)操做自然能夠作到無鎖.

相關問題: C++的6種memory order

那memory barrier這個名詞是哪裏蹦出來的呢? Load是原子性操做, CPU不會Load流程走到一半, 就切換到另外一個線程去了, 也就是Load自己是不會在多線程環境下產生問題的. 真正致使問題的是作這個操做的時機不肯定!

1. 編譯器有可能讓指令亂序, 好比, int a=b; long c=b; 編譯器一旦斷定a和c沒有依賴性, 就有權力讓這兩個取值操做以任意順序執行. 由於有可能有CPU指令能夠一下取4個int, 亂序能夠湊個整.

2. CPU會讓指令亂序, 緣由同上, 但額外還有個緣由是分支預測. AB線程都讀寫一箇中間量c, B在處理c, 你預期B好了, A纔會取. 但萬一A分支預測成功, B在處理的時候, A已經提早Load c進寄存器, 這就沒得玩了...

因此, 必需要有指令告訴CPU和編譯器, 不要改變這個變量的存取順序. 這就是Memory Barrier了. call MemoryBarrier保證先後一行是嚴格按照代碼順序的.

atomic_pointer.h 126-143行, 注意MemoryBarrier()的擺放,

 1 class AtomicPointer {
 2  private:
 3   void* rep_;
 4  public:
 5   AtomicPointer() { }
 6   explicit AtomicPointer(void* p) : rep_(p) {}
 7   inline void* NoBarrier_Load() const { return rep_; }
 8   inline void NoBarrier_Store(void* v) { rep_ = v; }
 9   inline void* Acquire_Load() const {
10     void* result = rep_;
11     MemoryBarrier();
12     return result;
13   }
14   inline void Release_Store(void* v) {
15     MemoryBarrier();
16     rep_ = v;
17   }
18 }; 

大公司的開源項目真的是一個寶庫! 就算用不到, 各類踩了無數坑的庫, 編碼規則和跨平臺代碼都是通常人沒機會完善的.

另外, 有菊苣在問題leveldb中atomic_pointer裏面memory barrier的幾點疑問?提到MemoryBarrier不保證CPU不亂序. 我以爲這個應該不用擔憂. 由於MemoryBarrier的counterpart是std::atomic, 確定嚴格保證語義相同啊. 實在不放心用std::atomic是墜吼的.

------

繼續上次沒讀完的Open部分代碼.

 139-146行,

has_imm_.Release_Store(NULL); // atomic pointer // Reserve ten files or so for other uses and give the rest to TableCache. const int table_cache_size = options_.max_open_files - kNumNonTableCacheFiles; table_cache_ = new TableCache(dbname_, &options_, table_cache_size); versions_ = new VersionSet(dbname_, &options_, table_cache_, &internal_comparator_); 
  • has_imm_, 用於判斷是否有等待或者正在寫入硬盤的immemtable
  • table_cache_, SSTable查詢緩存
  • versions_, 數據庫MVCC

has_imm_就是我上面描述的atomic pointer, 我推測這裏大機率Google程序員僱了一個臨時工(233), 把能夠列表構造的has_imm_放到了函數部分, 由於這裏不存在任何race的可能性. db new完了. 說下一個很重要的原則, 構造函數究竟要作什麼? 阿里和Google共同的觀點: 輕且無反作用(基本就是賦值). 業務有需求的話, 兩步構造或者工廠函數, 二選一.

回到最先的工廠函數, 一個靠譜數據庫的Open操做, 用腳趾頭也能想到要從日誌恢復數據,

 1 DB::Open(const Options& options, const std::string& dbname,
 2                 DB** dbptr) { // 工廠函數
 3   *dbptr = NULL; // 設置結果默認值, 指針傳值
 4 
 5   DBImpl* impl = new DBImpl(options, dbname);
 6   impl->mutex_.Lock(); // 數據恢復時上鎖, 禁止全部可能的後臺任務
 7   VersionEdit edit;
 8   // Recover handles create_if_missing, error_if_exists
 9   bool save_manifest = false;
10   Status s = impl->Recover(&edit, &save_manifest); // 讀log恢復狀態
11   if (s.ok() && impl->mem_ == NULL) {
12     // Create new log and a corresponding memtable. 復位
13     uint64_t new_log_number = impl->versions_->NewFileNumber();
14     WritableFile* lfile;
15     s = options.env->NewWritableFile(LogFileName(dbname, new_log_number),
16                                      &lfile);
17     if (s.ok()) {
18       edit.SetLogNumber(new_log_number);
19       impl->logfile_ = lfile;
20       impl->logfile_number_ = new_log_number;
21       impl->log_ = new log::Writer(lfile);
22       impl->mem_ = new MemTable(impl->internal_comparator_);
23       impl->mem_->Ref();
24     }
25   }
26   if (s.ok() && save_manifest) {
27     edit.SetPrevLogNumber(0);  // No older logs needed after recovery.
28     edit.SetLogNumber(impl->logfile_number_);
29     s = impl->versions_->LogAndApply(&edit, &impl->mutex_); // 同步VersionEdit到MANIFEST文件
30   }
31   if (s.ok()) {
32     impl->DeleteObsoleteFiles(); // 清理無用文件
33     impl->MaybeScheduleCompaction(); // 有寫入就有可能要compact
34   }
35   impl->mutex_.Unlock(); // 初始化完畢
36   if (s.ok()) {
37     assert(impl->mem_ != NULL);
38     *dbptr = impl;
39   } else {
40     delete impl;
41   }
42   return s;
43 }

------

就這樣, Open操做的脈絡大概應該是有了

相關文章
相關標籤/搜索