1. 程式人生 > >[leveldb] 2.open操作介紹

[leveldb] 2.open操作介紹

ted enum 副作用 class 列表 earch 處理 日誌恢復 false

在看源代碼之前, 先了解設計結構是必須的, 這就繞不開著名的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.

這可以說是最重要最有用的改動(不然為啥叫LevelDB?). 想象一下, 如果永遠只有一個SSTable, 我要把新immemtable歸並進去, 就要重寫這個SSTable. 數據有多大, 這個SSTable也會有多大, 那還怎麽合並?

聰明的童鞋可以說那把SSTable分成若幹份, 每份2MB. 但wrost case一樣悲劇. 比如, 當前這個immemtable恰好永遠有一個key與任意SSTable中至少一個key重復. Ops! 又回到了剛剛重寫全庫的case了.

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操作的脈絡大概應該是有了

[leveldb] 2.open操作介紹