leveldb源码分析—Recover和Repair

leveldb作为一个KV存储引擎将数据持久化到磁盘,而对于一个存储引擎来说在存储过程中因为一些其他原因导致程序down掉甚至数据文件被破坏等都会导致程序不能按正常流程再次启动。那么遇到这些状况以后如何使程序最大程度的恢复数据就是非常重要的一项工作,leveldb也提供了这方面的工作。

首先来看recover,这是每一次启动数据库的时候都会呗调用到的流程。其功能是恢复数据库在运行中突然因为某些原因down掉而这个时候leveldb中的丢失的当前状态,以及memtable甚至immtable中还未持久化到SSTable中的数据。我们知道leveldb采用的是WAL的方式进行的,那么对于leveldb中的当前状态存储在manifest文件中,通过读取已经持久化的状态,然后在结合WAL总的信息就可以恢复到最新状态;而memtable和immtable中的数据恢复的主要就是将未持久化到SSTable的数据从Log中读取出来重做到memtable或者SSTable即可。整个恢复的流程如下:

Status DBImpl::Recover(VersionEdit* edit) {
    env_->CreateDir(dbname_);
    Status s = env_->LockFile(LockFileName(dbname_), &db_lock_);
    if (!env_->FileExists(CurrentFileName(dbname_))) {
        if (options_.create_if_missing) {
            s = NewDB(); //生成全新的manifest和current文件
            return s;
        }
    }

    s = versions_->Recover();                   // 恢复当前version信息
    s = env_->GetChildren(dbname_, &filenames); // 获取文件列表
    versions_->AddLiveFiles(&expected);
    for (size_t i = 0; i < filenames.size(); i++) {
        if (ParseFileName(filenames[i], &number, &type)) {
            expected.erase(number); //删除存在的文件
            if (type == kLogFile && ((number >= min_log) || (number == prev_log)))
                logs.push_back(number); //存储当前已有的日志文件
        }
    }
    if (!expected.empty()) { //如果文件缺失
        return Status::Corruption(buf);
    }
    std::sort(logs.begin(), logs.end()); //排序日志文件
    for (size_t i = 0; i < logs.size(); i++) {
        s = RecoverLogFile(logs[i], edit, &max_sequence); //重做日志操作
        versions_->MarkFileNumberUsed(logs[i]);
    }
    if (s.ok()) {
        if (versions_->LastSequence() < max_sequence) {
            versions_->SetLastSequence(max_sequence);
        }
    }
}

下面我们首先来详细看看从manifest文件中恢复version信息的流程

Status VersionSet::Recover() {
    // Read "CURRENT" file, which contains a pointer to the current manifest file
    Status s = ReadFileToString(env_, CurrentFileName(dbname_), &current);
    // 生成当前manifest的文件名
    std::string dscname = dbname_ + "/" + current;
    SequentialFile* file;
    s = env_->NewSequentialFile(dscname, &file);
    if (!s.ok()) {
        return s;
    }
    // 各种初始化
    {
        // 逐个读取每个versionedit,并重建、记录响应的log_num等
        while (reader.ReadRecord(&record, &scratch) && s.ok()) {
            s = edit.DecodeFrom(record);
            if (s.ok()) {
                if (edit.has_comparator_ &&
                        edit.comparator_ != icmp_.user_comparator()->Name()) {
                }
            }
            if (s.ok()) {
                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()) {
        //恢复时获取到的num,seq等的判断和处理
        MarkFileNumberUsed(prev_log_number);
        MarkFileNumberUsed(log_number);
    }
    if (s.ok()) {
        Version* v = new Version(this);
        builder.SaveTo(v); // 存储一个全量的version
        // 计算Compaction相关的数据
        Finalize(v);
        AppendVersion(v); // 将version加入version链表中,然后根据恢复得到的信息维护当前version的信息
        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;
    }
    return s;
}

恢复了当前version的基本信息以后进行日志操作就可以恢复内存中的数据了,重做日志操作即是将日志中记录的操作读取出来,然后再将读取到的操作重新写入到leveldb中

Status DBImpl::RecoverLogFile(uint64_t log_number,
        VersionEdit* edit,
        SequenceNumber* max_sequence) {
    while (reader.ReadRecord(&record, &scratch) &&
            status.ok()) {
        if (record.size() < 12) ; //size不对
        WriteBatchInternal::SetContents(&batch, record);
        status = WriteBatchInternal::InsertInto(&batch, mem);//插入

        if (last_seq > *max_sequence) {
            *max_sequence = last_seq;
        }
        if (mem->ApproximateMemoryUsage() > options_.write_buffer_size) {
            status = WriteLevel0Table(mem, edit, NULL); //写成SSTable
        }
    }
    if (status.ok() && mem != NULL) {
        status = WriteLevel0Table(mem, edit, NULL);
        //写成SSTable
    }
    return status;
}

recover是leveldb中较为轻量级的恢复,主要是恢复leveldb未持久化到磁盘的数据和状态,每次启动时都会被调用到。那么如果我们的数据库受到了更大程度的伤害呢?比如SSTable文件损坏甚至丢失,manifest文件丢失。那么这个时候就必须使用下面的repair了,这个是当leveldb不能正常启动的时候需要手动进行的。

repair主要包含了如下几个流程来完成数据的修复:

FindFiles();               // 遍历数据目录下的文件解析文件名至manifest,日志,SSTable
   ConvertLogFilesToTables(); // 解析日志文件生成为SSTble,主要用到了ConvertLogToTable,
    ExtractMetaData();        /* 逐个遍历解析扫描到的SSTable并重新manifest的filemeta信息以供后面使用,
                            如果解析过程中有不能解析的数据,丢弃不能解析的数据并生成新的SSTable     */
    WriteDescriptor();       // 重置manifest文件号为1并生成最新的manifest,将其记录到current文件

ConvertLogFilesToTables主要用了一个和RecoverLogFile函数类似的ConvertLogToTable函数,他们的主要区别在RecoverLogFile在转化日志操作的时候使用的是leveldb的全局环境memtable和immtable,所以recover的数据有可能会持久化为SSTable,而有一部分则会留在内存中的memtable中;而ConvertLogToTable则是自己新建了一个局部的memtable来进行操作,然后将数据持久化为SSTable,这个过程从log恢复的数据一定会持久化为SSTable。

然后我们着重看下ExtractMetaData函数对扫描到的SSTable文件的逐个进行ScanTable的流程:

void ScanTable(uint64_t number) {
    std::string fname = TableFileName(dbname_, number); //生成ldb文件名
    Status status = env_->GetFileSize(fname, &t.meta.file_size); //获取文件大小,否则尝试sst文件后缀名
    if (!status.ok()) {
        // 略,和上面类似
    }
    if (!status.ok()) { // 如果都不能获取到文件大小,这个文件恢复失败,将其放入lost下
        ArchiveFile(TableFileName(dbname_, number));
        ArchiveFile(SSTTableFileName(dbname_, number));
        return;
    }
    // 遍历文件获得metadata信息.
    Iterator* iter = NewTableIterator(t.meta);
    for (iter->SeekToFirst(); iter->Valid(); iter->Next()) {
        Slice key = iter->key();
        if (!ParseInternalKey(key, &parsed)) {
            continue;
        }
        counter++;
        if (empty) { // 第一个key记录问文件最小key
            empty = false;
            t.meta.smallest.DecodeFrom(key);
        } //否则替换上一次设置的最大key,这样遍历到最后一个时就保留的是最后的一个
        t.meta.largest.DecodeFrom(key);
        if (parsed.sequence > t.max_sequence) {
            t.max_sequence = parsed.sequence;
        }
    }
    // 如果遍历过程中解析key无失败,直接存储metadata,否则遍历SSTable生成新的SSTable,并删除旧的
    if (status.ok()) {
        tables_.push_back(t);
    } else {
        RepairTable(fname, t); // RepairTable archives input file.
    }
}

如果数据有损坏,那么就必须重建SSTable,其过程如下:

void RepairTable(const std::string& src, TableInfo t) {
    // 遍历SSTable生成新的SSTable,并删除旧的
    // 生成新文件
    std::string copy = TableFileName(dbname_, next_file_number_++);
    WritableFile* file;
    Status s = env_->NewWritableFile(copy, &file);
    if (!s.ok()) {
        return;
    }
    TableBuilder* builder = new TableBuilder(options_, file);
    // Copy data.
    Iterator* iter = NewTableIterator(t.meta);
    int counter = 0;
    for (iter->SeekToFirst(); iter->Valid(); iter->Next()) {
        builder->Add(iter->key(), iter->value());
        counter++;
    }
    delete iter;
    ArchiveFile(src); //将旧文件移到lost下
    if (counter == 0) {
        builder->Abandon(); // Nothing to save
    } else {
        s = builder->Finish();
        if (s.ok()) {
            t.meta.file_size = builder->FileSize();
        }
    }
    delete builder;
    builder = NULL;
    if (s.ok()) {
        s = file->Close();
    }
    delete file;
    file = NULL;
    if (counter > 0 && s.ok()) {
        std::string orig = TableFileName(dbname_, t.meta.number);
        s = env_->RenameFile(copy, orig);
        if (s.ok()) {
            tables_.push_back(t);
        }
    }
    if (!s.ok()) {
        env_->DeleteFile(copy);
    }
}

在recover和repair的过程中我们发现了version的维护和管理十分重要,原本不打算再对这部分进行分析,但是现在看来对其进行一下细致的梳理还是很有必要的,预计下一篇专门介绍这部分内容。

leveldb源码分析—Recover和Repair

时间: 2024-10-12 00:33:33

leveldb源码分析—Recover和Repair的相关文章

leveldb源码分析--SSTable之Compaction

对于compaction是leveldb中体量最大的一部分,也应该是最为复杂的部分,为了便于理解我们首先从一些基本的概念开始.下面是一些从doc/impl.html中翻译和整理的内容: Level 0 当日志文件超过一定大小的阈值是 (默认为 1MB): 建立一个新的memtable和日志文件,以后的操作都是用新的memtable和日志文件 后台进行如下操作: 将旧的 memtable写到SSTable中(过程为先转为immtable_table,然后遍历写入) 废弃旧的 memtable 删除

LevelDB源码分析--Iterator

我们先来参考来至使用Iterator简化代码2-TwoLevelIterator的例子,略微修改希望能帮助更加容易立即,如果有不理解请各位看客阅读原文. 下面我们再来看一个例子,我们为一个书店写程序,书店里有许多书Book,每个书架(BookShelf)上有多本书. 类结构如下所示 class Book { private: string book_name_; }; class Shelf { private: vector<Book> books_; }; 如何遍历书架上所有的书呢?一种实

leveldb源码分析--SSTable之逻辑结构

SSTable是leveldb 的核心模块,这也是其称为leveldb的原因,leveldb正是通过将数据分为不同level的数据分为对应的不同的数据文件存储到磁盘之中的.为了理解其机制,我们首先看看SSTable中的基本概念. 首先看看数据的整体存储结构: 可以从图中看到了几个概念:Datablock,Metablock, MetaIndex block, Indexblock, Footer.具体他们的含义可以大致解释如下: 1. Datablock,我们知道文件中的k/v对是有序存储的,他

leveldb源码分析--Memtable

本节讲述内存中LevelDB的数据结构Memtable,Memtable义如其名即为内存中的KV Table,即LSM-Tree中的C0 Tree.我们知道在LSM-Tree中刚插入的的KV数据都是存储在内存中,当内存中存储的数据超过一定量以后再写到磁盘中.而对于leveldb来说这个过程演变为内存中的数据都是插入到MemTable中,当MemTable中的数据超过一定量(Options.write_buffer_size)以后MemTable就转化为Immutable Memtable等待du

leveldb源码分析--SSTable之TableBuilder

上一篇文章讲述了SSTable的格式以后,本文结合源码解析SSTable是如何生成的. void TableBuilder::Add(const Slice& key, const Slice& value) { //如果已经插入过数据,那么要保证当前插入的key > 之前最后一次插入的key, // SSTable必须是有序的插入数据 if (r->num_entries > 0) { assert(r->options.comparator->Compar

LevelDB源码分析--Cache及Get查找流程

本打算接下来分析version相关的概念,但是在准备的过程中看到了VersionSet的table_cache_这个变量才想起还有这样一个模块尚未分析,经过权衡觉得leveldb的version相对Cache来说相对复杂,而且version虽然对整个leveldb来说实现上跟其他功能十分紧密,但是从概念上来说却相对弱很多,有点感觉是附加的功能的感觉.所以从介绍系统首先应该注意的是整个系统概念的完整性的角度说还是先分析Cache相关的功能. 我们先来看Cache的基本框架结构数据: struct

leveldb源码分析--BloomFilter

bloomfilter是leveldb中的一大性能利器,所以为了文章的表现完整性这里新启这么一篇文章.leveldb中的bloomfilter的实现在bloom.cc中,是一个较为简单的实现,所以就不再具体进行分析.本文列出两个参考地址: 那些优雅的数据结构(1) : BloomFilter——大规模数据处理利器 是一个简单的bloomfilter的介绍乐和实现 Bloom Filter  则是一位专业的bloomfilter的研究人士的博客,内容及其的牛逼,对此感兴趣的阅读以下其文章应该有不小

leveldb源码分析--SSTable之block

在SSTable中主要存储数据的地方是data block,block_builder就是这个专门进行block的组织的地方,我们来详细看看其中的内容,其主要有Add,Finish和CurrentSizeEstimate三个函数.Finish的逻辑十分简单就是简单的将restart点信息和restart点个数分别以PutFixed32的格式写入数据最后:CurrentSizeEstimate则是简单的计算当前块需要的存储大小 = 已插入的KV对的大小 + 重启点个数 * 4 + 1 * 4(重启

leveldb源码分析--日志

我们知道在一个数据库系统中为了保证数据的可靠性,我们都会记录对系统的操作日志.日志的功能就是用来在系统down掉的时候对数据进行恢复,所以日志系统对一个要求可靠性的存储系统是极其重要的.接下来我们分析leveldb的日志,首先在leveldb源码目录中有doc/log_format.txt,这个文件详细的描述了leveldb的日志格式: record := checksum: uint32 // crc32c of type and data[] ; little-endian length: