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文件就没有机会整理
- leveldb:VersionEdit与MANIFEST文件
- VersionEdit-levelDB源码解析
- leveldb学习:Versionedit和Versionset
- 【转载】leveldb之MANIFEST
- leveldb研究7-Version/VersionSet/VersionEdit,内存中的数据结构Memtable/SkipList
- leveldb代码阅读(10)——levedb的Version、VersionSet、VersionEdit
- manifest 文件
- Manifest 文件
- manifest文件
- Manifest文件
- manifest文件
- Manifest文件
- manifest文件
- levelDB源码分析-SSTable:.sst文件构建与读取
- Manifest与exe.config文件原理与结构的分析
- LevelDB文件结构
- leveldb之log文件
- leveldb之文件
- python cross compile
- mysql 查询 记录最长的一条记录
- [Noi2010] D1T3 海拔 (网络流(迷~) 对偶图 + 最短路)
- 头条号运营技巧,百万爆文运营经验分享
- 调试SI4432要点
- leveldb:VersionEdit与MANIFEST文件
- <PHP 输出九九乘法表 for循环 递归>《正三角》《倒三角》
- oAuth
- Linux学习 处理输出
- 2015年上半年 软件设计师 上午试卷 综合知识-1
- ROS机器人星火计划公开课总结
- 关于Activity和Fragment同时使用startActivityForResult的问题
- 对称加密与非对称加密优缺点详解
- capture pspice 库元件简介