leveldb:VersionEdit与MANIFEST文件

来源:互联网 发布:mac应用删不掉 编辑:程序博客网 时间:2024/05/21 07:55

VersionEdit和MANIFEST文件到底是什么关系?

VersionEdit会保存在MANIFEST文件中。VersionEdit就相当于MANIFEST文件中的一条记录。
VersionEdit是version对象的变更记录,用于写入MANIFEST文件。这样通过原始的version加上一系列的versionedit的记录,就可以恢复到最新状态。

VersionEdit分析

下面来看VersionEdit导出的几个重要接口以及成员

// 新添加的sstable文件信息,将添加的sstable文件信息记录在VersionEdit的new_files_void AddFile(int level, uint64_t file, uint64_t file_size,                 constInternalKey& smallest, const InternalKey& largest)  // 从指定的level删除文件,把要删除的文件信息记录在VersionEdit的deleted_files_              void DeleteFile(int level, uint64_t file)   // 将VersionEdit的信息Encode到一个string中,便于写入manifest文件  void EncodeTo(std::string* dst) const // 从Slice中Decode出VersionEdit的信息  Status DecodeFrom(const Slice& src) typedef std::set< std::pair<int, uint64_t> > DeletedFileSet;  std::string comparator_; // key comparator名字  uint64_t log_number_; // 日志编号  uint64_t prev_log_number_; // 前一个日志编号,为了兼容老版本,新版本已弃用,一直为0  uint64_t next_file_number_; // 下一个文件编号  SequenceNumber last_sequence_; // db中最大的seq(序列号),即最后一对kv事务操作的序列号bool has_comparator_;   bool has_log_number_;  bool has_prev_log_number_;  bool has_next_file_number_; bool has_last_sequence_;  //记录每一层下次合并的起始keystd::vector< std::pair<int, InternalKey> >compact_pointers_; DeletedFileSet deleted_files_; // 将要删除的文件集合  std::vector< std::pair<int, FileMetaData> > new_files_; // 新添加的文件集合  

简单来说,VersionEdit记录的就是数据库的变更信息的(如这次将要删除哪些文件,新增哪些文件,以及各层下次合并点的信息)

VersionEdit的持久化

我们知道VersionEdit记录了数据库从一个版本到下一个版本的变更信息,如果只放在内存中,掉电后我们将很难(为什么说很难而不是无法恢复,详见后)快速恢复出数据库的最新版本,所以我们需要将VersionEdit持久化,这边是MANIFEST文件的作用。

MANIFEST文件的格式

首先是使用的coparator名、log编号、前一个log编号、下一个文件编号、上一个序列号。这些都是日志、sstable文件使用到的重要信息,这些字段不一定必然存在。
Leveldb在写入每个字段之前,都会先写入一个varint型数字来标记后面的字段类型。在读取时,先读取此字段,根据类型解析后面的信息。一共有9种类型:
kComparator = 1, kLogNumber = 2, kNextFileNumber = 3, kLastSequence = 4,
kCompactPointer = 5, kDeletedFile = 6, kNewFile = 7, kPrevLogNumber = 9
// 8 was used for large value refs
其中8另有它用。
其次是compact点,可能有多个,写入格式为{kCompactPointer, level, internal key}。
其后是删除文件,可能有多个,格式为{kDeletedFile, level, file number}。
最后是新文件,可能有多个,格式为
{kNewFile, level, file number, file size, min key, max key}。
对于版本间变动它是新加的文件集合,对于MANIFEST快照是该版本包含的所有sstable文件集合。
一张图表示一下,如图
这里写图片描述
其中的数字都是varint存储格式,string都是以varint指明其长度,后面跟实际的字符串内容。
执行序列化和反序列化的是Decode和Encode函数,根据这些代码,我们可以了解Manifest文件的存储格式。

注意,本质上说,MANIFEST文件是log类型的文件,即leveldb之log中提到的那种格式。
OK,我们接下来看下VersionEdit如何通过EncodeTo函数序列化:

void VersionEdit::EncodeTo(std::string* dst) const {  // 记录Comparator名称  if (has_comparator_) {    PutVarint32(dst, kComparator);    PutLengthPrefixedSlice(dst, comparator_);  }  // 记录Log Numer  if (has_log_number_) {    PutVarint32(dst, kLogNumber);    PutVarint64(dst, log_number_);  }  // 记录Prev Log Number,现在已废弃,一般为0  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_);  }  // 记录当前使用的最大的sequence  if (has_last_sequence_) {    PutVarint32(dst, kLastSequence);    PutVarint64(dst, last_sequence_);  }  // 记录每一级Level下次compaction的起始Key    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  }  // 记录每一级新增文件的元数据(sst文件名以及其smallest与largest的key)  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());  }}

从MANIFEST文件读出来的内容是VersionEdit 通过EncodeTo序列化过的内容,因此要反序列化得到VersionEdit,与EncodeTo相反,这里就不重复了。

杂谈

MANIFEST丢失或者损坏,leveldb如何恢复

如果只有MANIFEST文件损坏,或者干脆误删除,leveldb是可以恢复的。
对于LevelDB而言,修复过程如下:
1.首先处理log,这些还未来得及写入的记录,写入新的.sst文件
2.扫描所有的sst文件,生成元数据信息:包括number filesize, 最小key,最大key
根据这些元数据信息,将生成新的MANIFEST文件。
3.第三步如何生成新的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 文件放入不同的子目录,有了这个,重新生成MANIFEST自然就很简单了,同样的78000 sstable文件,Repair过程耗时是分钟级别的。

MANIFEST 文件的增长和重新生成

随着时间的流逝,发生Compact的机会越来越多,Version跃升的次数越多,自然VersionEdit出现的次数越来越多,而每一个VersionEdit都会记录到MANIFEST,这必然会造成MANIFEST文件不断变大。
MANIFEST文件和LOG文件一样,只要DB不关闭,这个文件一直在增长。我查看了我一个线上环境,MANIFEST文件已经膨胀到了205MB。
随着时间的流逝,早期的版本是没有意义的,我们没必要还原所有的版本的情况,我们只需要还原还活着的版本的信息。MANIFEST只有一个机会变小,抛弃早期过时的VersionEdit,给当前的VersionSet来个快照,然后从新的起点开始累加VerisonEdit。这个机会就是重新开启DB。
LevelDB的早期,只要Open DB必然会重新生成MANIFEST,哪怕MANIFEST文件大小比较小,这会给打开DB带来较大的延迟。
优化之后,并不是每一次的Open都会带来 MANIFEST的重新生成。将Open DB的延迟从80毫秒降低到了0.13ms,效果非常明显。
在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的新起点*/      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文件就没有机会整理