【轉載】leveldb之MANIFEST

【轉載】leveldb之MANIFEST

首頁分類標籤留言關於訂閱

2017-01-11 分類 leveldb  標籤 leveldb 

轉自 http://bean-li.github.io/leveldb-manifest/

前言

上一節講了LogAndApply的一部分:

  Version* v = new Version(this);
  {
    Builder builder(this, current_);
    builder.Apply(edit);
    builder.SaveTo(v);
  }
  Finalize(v);

我們結下來講剩下的部分,因爲剩下的部分和MANIFEST文件的內容有關係,所以單拎出來講。

Ceph的mon會記錄一些信息到LevelDB,我們曾經遇到過ceph-mon無法啓動,原因是MANIFEST文件損壞:

如果MANIFEST文件損壞,或者乾脆刪除掉,leveldb能否恢復? 答案是肯定的。

import leveldb
ret = leveldb.RepairDB('/data/mon.iecvq/store.db')

MANIFEST損壞,爲什麼可以修復,修復原理是什麼?

這都是本文要解決的問題。

VersionEdit and MANIFEST

VersionEdit和MANIFEST文件到底是什麼關係?

VersionEdit會保存在MANIFEST文件中。

private:
  friend class VersionSet;

  typedef std::set< std::pair<int, uint64_t> > DeletedFileSet;

  std::string comparator_;
  uint64_t log_number_;
  uint64_t prev_log_number_;
  uint64_t next_file_number_;
  SequenceNumber last_sequence_;
  bool has_comparator_;
  bool has_log_number_;
  bool has_prev_log_number_;
  bool has_next_file_number_;
  bool has_last_sequence_;

  std::vector< std::pair<int, InternalKey> > compact_pointers_;
  DeletedFileSet deleted_files_;
  std::vector< std::pair<int, FileMetaData> > new_files_;

VersionEdit有上面的成員,LevelDB會在合適的時機將VerisonEdit的內容序列化到MANIFEST文件:

enum Tag {
  kComparator           = 1,
  kLogNumber            = 2,
  kNextFileNumber       = 3,
  kLastSequence         = 4,
  kCompactPointer       = 5,
  kDeletedFile          = 6,
  kNewFile              = 7,
  // 8 was used for large value refs
  kPrevLogNumber        = 9
};


每一個VersionEdit,會首先EncodeTo,然後將內容作爲一筆記錄添加到MANIFEST文件。注意MANIFEST文件生成和log文件的生成是一樣的。

在LogAndApply中,當生成新的Version之後,會調用如下內容,將VersionEdit的內容,即兩個版本的差異記錄在MANIFEST文件中:

      std::string record;
      /*先調用VersionEdit的 EncodeTo方法,序列化成字符串*/
      edit->EncodeTo(&record);
      
      /*注意,descriptor_log_是log文件*/
      s = descriptor_log_->AddRecord(record);
      if (s.ok()) {
        s = descriptor_file_->Sync();
      }
      if (!s.ok()) {
        Log(options_->info_log, "MANIFEST write: %s\n", s.ToString().c_str());
      }

注意,本質上說,MANIFEST文件是log類型的文件,即leveldb之log中提到的那種格式。

OK,我們接下來看下VersionEdit如何通過EncodeTo函數序列化:

void VersionEdit::EncodeTo(std::string* dst) const {

  /*將比較器的標識和名稱放入序列化字符串中*/
  if (has_comparator_) {
    PutVarint32(dst, kComparator);
    PutLengthPrefixedSlice(dst, comparator_);
  }
  
  /*將日誌文件編號的標識和名稱放入序列化字符串中*/
  if (has_log_number_) {
    PutVarint32(dst, kLogNumber);
    PutVarint64(dst, log_number_);
  }
  
  /*將前一個日誌的標識和名稱放入序列化字符串中*/
  if (has_prev_log_number_) {
    PutVarint32(dst, kPrevLogNumber);
    PutVarint64(dst, prev_log_number_);
  }
  
  /* 將下一個文件的標識和名稱放入序列化字符串中 */
  if (has_next_file_number_) {
    PutVarint32(dst, kNextFileNumber);
    PutVarint64(dst, next_file_number_);
  }
  
  /*上一個序列號的標識和名稱放入序列化字符串中*/
  if (has_last_sequence_) {
    PutVarint32(dst, kLastSequence);
    PutVarint64(dst, last_sequence_);
  }

  /*將每個壓縮點的標識,層次和InternalKey放入序列化字符串*/
  for (size_t i = 0; i < compact_pointers_.size(); i++) {
    PutVarint32(dst, kCompactPointer);
    PutVarint32(dst, compact_pointers_[i].first);  // level
    PutLengthPrefixedSlice(dst, compact_pointers_[i].second.Encode());
  }

  /*刪除文件部分*/
  for (DeletedFileSet::const_iterator iter = deleted_files_.begin();
       iter != deleted_files_.end();
       ++iter) {
    PutVarint32(dst, kDeletedFile);
    PutVarint32(dst, iter->first);   // level
    PutVarint64(dst, iter->second);  // file number
  }

  /*新增文件部分*/
  for (size_t i = 0; i < new_files_.size(); i++) {
    const FileMetaData& f = new_files_[i].second;
    PutVarint32(dst, kNewFile);
    PutVarint32(dst, new_files_[i].first);  // level
    PutVarint64(dst, f.number);
    PutVarint64(dst, f.file_size);
    PutLengthPrefixedSlice(dst, f.smallest.Encode());
    PutLengthPrefixedSlice(dst, f.largest.Encode());
  }
}

上述流程入下圖所示:

當正常運行期間,每當調用LogAndApply的時候,都會將VersionEdit作爲一筆記錄,追加寫入到MANIFEST文件。

注意,VersionEdit可以序列化,存進MANIFEST文件,同樣道理,MANIFEST中可以將VersionEdit一個一個的重放出來。這個重放的目的,是爲了得到當前的Version 以及VersionSet。

一般來講,當打開的DB的時候,需要獲得這種信息,而這種信息的獲得,靠的就是所有VersionEdit 按照次序一一回放,生成當前的Version。

Status VersionSet::Recover(bool *save_manifest) {
  struct LogReporter : public log::Reader::Reporter {
    Status* status;
    virtual void Corruption(size_t bytes, const Status& s) {
      if (this->status->ok()) *this->status = s;
    }
  };

  // Read "CURRENT" file, which contains a pointer to the current manifest file
  std::string current;
  Status s = ReadFileToString(env_, CurrentFileName(dbname_), &current);
  if (!s.ok()) {
    return s;
  }
  if (current.empty() || current[current.size()-1] != '\n') {
    return Status::Corruption("CURRENT file does not end with newline");
  }
  current.resize(current.size() - 1);

  /*CURRENT文件記錄着MANIFEST的文件名字,名字爲MANIFEST-number */
  std::string dscname = dbname_ + "/" + current;
  SequentialFile* file;
  s = env_->NewSequentialFile(dscname, &file);
  if (!s.ok()) {
    return s;
  }

  bool have_log_number = false;
  bool have_prev_log_number = false;
  bool have_next_file = false;
  bool have_last_sequence = false;
  uint64_t next_file = 0;
  uint64_t last_sequence = 0;
  uint64_t log_number = 0;
  uint64_t prev_log_number = 0;
  Builder builder(this, current_);

  {
    LogReporter reporter;
    reporter.status = &s;
    log::Reader reader(file, &reporter, true/*checksum*/, 0/*initial_offset*/);
    Slice record;
    std::string scratch;
    
    /*文件中一條記錄一條記錄的讀出,並DecodeFrom,生成VersionEdit */
    while (reader.ReadRecord(&record, &scratch) && s.ok()) {
      VersionEdit edit;
      s = edit.DecodeFrom(record);
      if (s.ok()) {
        if (edit.has_comparator_ &&
            edit.comparator_ != icmp_.user_comparator()->Name()) {
          s = Status::InvalidArgument(
              edit.comparator_ + " does not match existing comparator ",
              icmp_.user_comparator()->Name());
        }
      }

      if (s.ok()) {
        /*按照次序,講Verison的變化量層層回放,最重會得到最終版本的Version*/
        builder.Apply(&edit);
      }

      if (edit.has_log_number_) {
        log_number = edit.log_number_;
        have_log_number = true;
      }

      if (edit.has_prev_log_number_) {
        prev_log_number = edit.prev_log_number_;
        have_prev_log_number = true;
      }

      if (edit.has_next_file_number_) {
        next_file = edit.next_file_number_;
        have_next_file = true;
      }

      if (edit.has_last_sequence_) {
        last_sequence = edit.last_sequence_;
        have_last_sequence = true;
      }
    }
  }
  delete file;
  file = NULL;

  if (s.ok()) {
    if (!have_next_file) {
      s = Status::Corruption("no meta-nextfile entry in descriptor");
    } else if (!have_log_number) {
      s = Status::Corruption("no meta-lognumber entry in descriptor");
    } else if (!have_last_sequence) {
      s = Status::Corruption("no last-sequence-number entry in descriptor");
    }

    if (!have_prev_log_number) {
      prev_log_number = 0;
    }

    MarkFileNumberUsed(prev_log_number);
    MarkFileNumberUsed(log_number);
  }

  if (s.ok()) {
    Version* v = new Version(this);
    
    /*通過回放所有的VersionEdit,得到最終版本的Version,存入v*/
    builder.SaveTo(v);
    // Install recovered version
    
    
    Finalize(v);
    
    /* AppendVersion將版本v放入VersionSet集合,同時設置curret_等於v */
    AppendVersion(v);
    manifest_file_number_ = next_file;
    next_file_number_ = next_file + 1;
    last_sequence_ = last_sequence;
    log_number_ = log_number;
    prev_log_number_ = prev_log_number;

    // See if we can reuse the existing MANIFEST file.
    if (ReuseManifest(dscname, current)) {
      // No need to save new manifest
    } else {
      *save_manifest = true;
    }
  }

  return s;
}

首先CURRENT文件中記錄的內容爲對應MANIFEST文件的名字,比如MANIFEST-037896,根據這個名字可以找到正確的MANIFEST文件。因爲MANIFEST文件是log文件,因此可以一條記錄一條記錄的讀出來。

讀出來的內容是VersionEdit 通過EncodeTo序列化過的內容,因此,可以反序列化,得到VersionEdit:

Status VersionEdit::DecodeFrom(const Slice& src) {
  Clear();
  Slice input = src;
  const char* msg = NULL;
  uint32_t tag;

  // Temporary storage for parsing
  int level;
  uint64_t number;
  FileMetaData f;
  Slice str;
  InternalKey key;

  while (msg == NULL && GetVarint32(&input, &tag)) {
    switch (tag) {
      case kComparator:
        if (GetLengthPrefixedSlice(&input, &str)) {
          comparator_ = str.ToString();
          has_comparator_ = true;
        } else {
          msg = "comparator name";
        }
        break;

      case kLogNumber:
        if (GetVarint64(&input, &log_number_)) {
          has_log_number_ = true;
        } else {
          msg = "log number";
        }
        break;

      case kPrevLogNumber:
        if (GetVarint64(&input, &prev_log_number_)) {
          has_prev_log_number_ = true;
        } else {
          msg = "previous log number";
        }
        break;

      case kNextFileNumber:
        if (GetVarint64(&input, &next_file_number_)) {
          has_next_file_number_ = true;
        } else {
          msg = "next file number";
        }
        break;

      case kLastSequence:
        if (GetVarint64(&input, &last_sequence_)) {
          has_last_sequence_ = true;
        } else {
          msg = "last sequence number";
        }
        break;

      case kCompactPointer:
        if (GetLevel(&input, &level) &&
            GetInternalKey(&input, &key)) {
          compact_pointers_.push_back(std::make_pair(level, key));
        } else {
          msg = "compaction pointer";
        }
        break;

      case kDeletedFile:
        if (GetLevel(&input, &level) &&
            GetVarint64(&input, &number)) {
          deleted_files_.insert(std::make_pair(level, number));
        } else {
          msg = "deleted file";
        }
        break;

      case kNewFile:
        if (GetLevel(&input, &level) &&
            GetVarint64(&input, &f.number) &&
            GetVarint64(&input, &f.file_size) &&
            GetInternalKey(&input, &f.smallest) &&
            GetInternalKey(&input, &f.largest)) {
          new_files_.push_back(std::make_pair(level, f));
        } else {
          msg = "new-file entry";
        }
        break;

      default:
        msg = "unknown tag";
        break;
    }
  }

  if (msg == NULL && !input.empty()) {
    msg = "invalid tag";
  }

  Status result;
  if (msg != NULL) {
    result = Status::Corruption("VersionEdit", msg);
  }
  return result;
}

這個DecodeFrom沒啥好說的,就是EncodeTo的反過程。

我們看下MANIFEST Decode之後得到的VersionEdit:

VersionSet::Recover函數中,回放完畢,生成了一個最終版的Verison v,Finalize之後,調用了AppendVersion,這個函數很有意思,事實上,LogAndApply講VersionEdit寫入MANIFEST文件之後,也調用了AppendVersion,如下所示:

// Unlock during expensive MANIFEST log write
  {
    mu->Unlock();

    // Write new record to MANIFEST log
    if (s.ok()) {
      std::string record;
      
      /*將當前VersionEdit Encode,作爲記錄,寫入MANIFEST*/
      edit->EncodeTo(&record); 
      s = descriptor_log_->AddRecord(record);
      if (s.ok()) {
      
        /*調用Sync持久化*/
        s = descriptor_file_->Sync();
      }
      if (!s.ok()) {
        Log(options_->info_log, "MANIFEST write: %s\n", s.ToString().c_str());
      }
    }

    // If we just created a new descriptor file, install it by writing a
    // new CURRENT file that points to it.
    if (s.ok() && !new_manifest_file.empty()) {
      s = SetCurrentFile(env_, dbname_, manifest_file_number_);
    }

    mu->Lock();
  }

  // Install the new version
  if (s.ok()) {
  
    /* 新的版本已經生成(通過current_和VersionEdit), VersionEdit也已經寫入MANIFEST,
     * 此時可以將v設置成current_, 同時將最新的version v鏈入VersionSet的雙向鏈表*/
    AppendVersion(v);
    log_number_ = edit->log_number_;
    prev_log_number_ = edit->prev_log_number_;
  } else {
    delete v;
    if (!new_manifest_file.empty()) {
      delete descriptor_log_;
      delete descriptor_file_;
      descriptor_log_ = NULL;
      descriptor_file_ = NULL;
      env_->DeleteFile(new_manifest_file);
    }
  }

  return s;

AppendVersion的實現如下所示:


void VersionSet::AppendVersion(Version* v) {
  // Make "v" current
  assert(v->refs_ == 0);
  assert(v != current_);
  if (current_ != NULL) {
    current_->Unref();
  }
  current_ = v;
  v->Ref();

  // Append to linked list
  v->prev_ = dummy_versions_.prev_;
  v->next_ = &dummy_versions_;
  v->prev_->next_ = v;
  v->next_->prev_ = v;
}

所以current_版本的更替時機一定要注意到,LogAndApply生成新版本之後,同時將VersionEdit記錄到MANIFEST文件之後。

MANIFEST丟失或者損壞,leveldb如何恢復

如果只有MANIFEST文件損壞,或者乾脆誤刪除,leveldb是可以恢復的。這是結論,事實上這兩種實驗我都已經做過了。

使用python-leveldb,通過如下手段可以修復Leveldb

import leveldb
ret = leveldb.RepairDB('/data/mon.iecvq/store.db')

爲什麼MANIFEST損壞或者丟失之後,依然可以恢復出來?LevelDB如何做到。

對於LevelDB而言,修復過程如下:

  • 首先處理log,這些還未來得及寫入的記錄,寫入新的.sst文件
  • 掃描所有的sst文件,生成元數據信息:包括number filesize, 最小key,最大key
  • 根據這些元數據信息,將生成新的MANIFEST文件。

第三步如何生成新的MANIFEST? 因爲sstable文件是分level的,但是很不幸,我們無法從名字上判斷出來文件屬於哪個level。第三步處理的原則是,既然我分不出來,我就認爲所有的sstale文件都屬於level 0,因爲level 0是允許重疊的,因此並沒有違法基本的準則。

當修復之後,第一次Open LevelDB的時候,很明顯level 0 的文件可能遠遠超過4個文件,因此會Compaction。 又因爲所有的文件都在Level 0 這次Compaction無疑是非常沉重的。它會掃描所有的文件,歸併排序,產生出level 1文件,進而產生出其他level的文件。

從上面的處理流程看,如果只有MANIFEST文件丟失,其他文件沒有損壞,LevelDB是不會丟失數據的,原因是,LevelDB既然已經無法將所有的數據分到不同的Level,但是數據畢竟沒有丟,根據文件的number,完全可以判斷出文件的新舊,從而確定不同sstable文件中的重複數據,which是最新的。經過一次比較耗時的歸併排序,就可以生成最新的levelDB。

上述的方法,從功能的角度看,是正確的,但是效率上不敢恭維。Riak曾經測試過78000個sstable 文件,490G的數據,大家都位於Level 0,歸併排序需要花費6 weeks,6周啊,這個耗時讓人發瘋的。

Riak 1.3 版本做了優化,改變了目錄結構,對於google 最初版本的LevelDB,所有的文件都在一個目錄下,但是Riak 1.3版本引入了子目錄, 將不同level的sst 文件放入不同的子目錄:

sst_0
sst_1
...
sst_6

有了這個,重新生成MANIFEST自然就很簡單了,同樣的78000 sstable文件,Repair過程耗時是分鐘級別的。

MANIFEST 文件的增長和重新生成

存在一個問題不知道大家有沒有意識到,隨着時間的流逝,發生Compact的機會越來越多,Version躍升的次數越多,自然VersionEdit出現的次數越來越多,而每一個VersionEdit都會記錄到MANIFEST,這必然會造成MANIFEST文件不斷變大。

ceph社區早就發現了這個問題, 有一個很bug是這麼描述的:

BUG #5175 leveldb: LOG and MANIFEST file grow without bound (LOG being _text_ log !)

Description

leveldb has two files that seem to grow without bound and are only cleared on db open.

The first is the LOG file which is a textual debug log that leveldb creates. It's opened when the db is opened 

and is never closed or cycled (and leveldb doesn't have any method to make it cycle other than close/reopen).

To make matter worse, if this file is on XFS, it's subject to XFS pre-allocation (so each time the file size 

reaches the currently on-disk allocated size, the allocated size doubles, and so you get used disk space of 2M/

4M/.../256M/512M giving big jumps in used disk space as reported by 'du -sh')

The second file is the MANIFEST file which grows a little at each compaction and is also only 

trimmed on db open. For long running (i.e. weeks/month), those can actually grow quite a bit

 (especially LOG). Note that the same issue exists on OSD as well but they seem to receive a whole lot less 
 
 updates than mon so it shows less.

MANIFEST文件和LOG文件一樣,只要DB不關閉,這個文件一直在增長。我查看了我一個線上環境,MANIFEST文件已經膨脹到了205MB。

試試上,隨着時間的流逝,早期的版本是沒有意義的,我們沒必要還原所有的版本的情況,我們只需要還原還活着的版本的信息。MANIFEST只有一個機會變小,拋棄早期過時的VersionEdit,給當前的VersionSet來個快照,然後從新的起點開始累加VerisonEdit。這個機會就是重新開啓DB。

LevelDB的早期,只要Open DB必然會重新生成MANIFEST,哪怕MANIFEST文件大小比較小,這會給打開DB帶來較大的延遲。

這個commit將Open DB的延遲從80毫秒降低到了0.13ms,效果非常明顯,即優化之後,並不是每一次的Open都會帶來 MANIFEST的重新生成。

在VersionSet::Recover函數中,會判斷是否延用老的MANIFEST文件,判斷邏輯如下:

bool VersionSet::ReuseManifest(const std::string& dscname,
                               const std::string& dscbase) {
  if (!options_->reuse_logs) {
    return false;
  }
  FileType manifest_type;
  uint64_t manifest_number;
  uint64_t manifest_size;
  
  /*如果老的MANIFEST文件太大了,就不在延用,return false
   *延用還是不延用的關鍵在如下語句:
      descriptor_log_ = new log::Writer(descriptor_file_, manifest_size);
   * 如果dscriptor_log_ 爲NULL,當情況有變,發生了版本的躍升,有VersionEdit需要寫入的MANIFEST的時候,
   * 會首先判斷descriptor_log_是否爲NULL,如果爲NULL,表示不要在延用老的MANIFEST了,要另起爐竈
   * 所謂另起爐竈,即起一個空的MANIFEST,先要記下版本的Snapshot,然後將VersionEdit追加寫入
   */
  
  if (!ParseFileName(dscbase, &manifest_number, &manifest_type) ||
      manifest_type != kDescriptorFile ||
      !env_->GetFileSize(dscname, &manifest_size).ok() ||
      // Make new compacted MANIFEST if old one is too big
      manifest_size >= TargetFileSize(options_)) {
    return false;
  }

  assert(descriptor_file_ == NULL);
  assert(descriptor_log_ == NULL);
  Status r = env_->NewAppendableFile(dscname, &descriptor_file_);
  if (!r.ok()) {
    Log(options_->info_log, "Reuse MANIFEST: %s\n", r.ToString().c_str());
    assert(descriptor_file_ == NULL);
    return false;
  }

  Log(options_->info_log, "Reusing MANIFEST %s\n", dscname.c_str());
  descriptor_log_ = new log::Writer(descriptor_file_, manifest_size);
  manifest_file_number_ = manifest_number;
  return true;
}

這部分邏輯要和LogAndApply對照看:延用老的MANIFEST,那麼就會執行如下的語句:

  descriptor_log_ = new log::Writer(descriptor_file_, manifest_size);
  manifest_file_number_ = manifest_number;
  

這個語句的結果是,當version發生變化,出現新的VersionEdit的時候,並不會新創建MANIFEST文件,正相反,會追加寫入VersionEdit。

但是如果MANIFEST文件已經太大了,我們沒必要保留全部的歷史VersionEdit,我們完全可以以當前版本爲基準,打一個SnapShot,後續的變化,以該SnapShot爲基準,不停追加新的VersionEdit。

我們看下LogAndApply中的相關部分:


/* descriptor_log_ == NULL 對應的是不延用老的MANIFEST文件 */
if (descriptor_log_ == NULL) {
    // No reason to unlock *mu here since we only hit this path in the
    // first call to LogAndApply (when opening the database).
    assert(descriptor_file_ == NULL);
    new_manifest_file = DescriptorFileName(dbname_, manifest_file_number_);
    edit->SetNextFile(next_file_number_);
    s = env_->NewWritableFile(new_manifest_file, &descriptor_file_);
    if (s.ok()) {
      descriptor_log_ = new log::Writer(descriptor_file_);
      
      /*當前的版本情況打個快照,作爲新MANIFEST的新起點8/
      s = WriteSnapshot(descriptor_log_);
    }
  }

  // Unlock during expensive MANIFEST log write
  {
    mu->Unlock();

    // Write new record to MANIFEST log
    if (s.ok()) {
      std::string record;
      edit->EncodeTo(&record);
      s = descriptor_log_->AddRecord(record);
      if (s.ok()) {
        s = descriptor_file_->Sync();
      }
      if (!s.ok()) {
        Log(options_->info_log, "MANIFEST write: %s\n", s.ToString().c_str());
      }
    }

    // If we just created a new descriptor file, install it by writing a
    // new CURRENT file that points to it.
    if (s.ok() && !new_manifest_file.empty()) {
      s = SetCurrentFile(env_, dbname_, manifest_file_number_);
    }

    mu->Lock();
  }

如果不延用老的MANIFEST文件,會生成一個空的MANIFEST文件,同時調用WriteSnapShot將當前版本情況作爲起點記錄到MANIFEST文件。

這種情況下,MANIFEST文件的大小會大大減少,就像自我介紹,完全可以自己出生起開始介紹起,完全不必從盤古開天闢地介紹起。

可惜的是,只要DB不關閉,MANIFEST文件就沒有機會整理。因此對於ceph-mon這種daemon進程,MANIFEST文件的大小會一直增長,除非重啓ceph-mon纔有機會整理。

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