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 = ; 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 = ; 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 + ;
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() < ) ; //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 = ;
for (iter->SeekToFirst(); iter->Valid(); iter->Next()) {
builder->Add(iter->key(), iter->value());
counter++;
}
delete iter;
ArchiveFile(src); //将旧文件移到lost下
if (counter == ) {
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 > && 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的更多相关文章

  1. leveldb源码分析--SSTable之block

    在SSTable中主要存储数据的地方是data block,block_builder就是这个专门进行block的组织的地方,我们来详细看看其中的内容,其主要有Add,Finish和CurrentSi ...

  2. leveldb源码分析--WriteBatch

    从[leveldb源码分析--插入删除流程]和WriteBatch其名我们就很轻易的知道,这个是leveldb内部的一个批量写的结构,在leveldb为了提高插入和删除的效率,在其插入过程中都采用了批 ...

  3. leveldb源码分析--Key结构

    [注]本文参考了sparkliang的专栏的Leveldb源码分析--3并进行了一定的重组和排版 经过上一篇文章的分析我们队leveldb的插入流程有了一定的认识,而该文设计最多的又是Batch的概念 ...

  4. Leveldb源码分析--1

    coming from http://blog.csdn.net/sparkliang/article/details/8567602 [前言:看了一点oceanbase,没有意志力继续坚持下去了,暂 ...

  5. leveldb源码分析--日志

    我们知道在一个数据库系统中为了保证数据的可靠性,我们都会记录对系统的操作日志.日志的功能就是用来在系统down掉的时候对数据进行恢复,所以日志系统对一个要求可靠性的存储系统是极其重要的.接下来我们分析 ...

  6. leveldb源码分析之Slice

    转自:http://luodw.cc/2015/10/15/leveldb-02/ leveldb和redis这样的优秀开源框架都没有使用C++自带的字符串string,redis自己写了个sds,l ...

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

    本打算接下来分析version相关的概念,但是在准备的过程中看到了VersionSet的table_cache_这个变量才想起还有这样一个模块尚未分析,经过权衡觉得leveldb的version相对C ...

  8. leveldb源码分析--SSTable之TableBuilder

    上一篇文章讲述了SSTable的格式以后,本文结合源码解析SSTable是如何生成的. void TableBuilder::Add(const Slice& key, const Slice ...

  9. leveldb源码分析之内存池Arena

    转自:http://luodw.cc/2015/10/15/leveldb-04/ 这篇博客主要讲解下leveldb内存池,内存池很多地方都有用到,像linux内核也有个内存池.内存池的存在主要就是减 ...

随机推荐

  1. sql server 随机读取数据

    --sql server 随机读取数据 * FROM [tablename] ORDER BY NEWID() pk from [tablename] ORDER BY NEWID()) --这两个方 ...

  2. Android --- 斗地主 [牌桌实现源码]

    1.主Activity <span style="font-size:18px;color:#3333ff;">package com.bison; import an ...

  3. 环信SDK与Apple Watch的结合(2)

    这一篇主要是介绍怎么拖apple watch上的相关页面,附源码EMWatchOCDemo. 需要在工程中的“EMWatchOCDemo WatchKit App”中进行操作,该文件夹的结构如图 Wa ...

  4. 面向对象的Javascript(4):重载

    在小项目中对于JavaScript使用,只要写几个function就行了.但在大型项目中,尤其是在开发追求 良好的用户体验的网站中,如SNS,就会 用到大量的JavaScrpt,有时JavaScrip ...

  5. 学习笔记(一)——MVC扩展

    1.视图引擎的作用,总结为两点: 查找视图 渲染视图 ViewEngine即视图引擎, 在ASP.NET MVC中将ViewEngine的作用抽象成了 IViewEngine 接口. 默认情况下,AS ...

  6. 【jQuery基础学习】10 简单了解jQuery Mobile及jQuery各个级别版本的变化

    关于 jQuery Mobile jQuery Mobile是为了填补jQuery在移动设备应用上的一个新项目.它应用了HTML5和CSS3. 主要特性 基于jQuery构建. 采用与jQuery一致 ...

  7. Shiro 整合SpringMVC 并且实现权限管理,登录和注销

    Apache Shiro是Java的一个安全框架.目前,使用Apache Shiro的人越来越多,因为它相当简单,对比Spring Security,可能没有Spring Security做的功能强大 ...

  8. 常用 Git 命令清单(摘录)

    来源:阮一峰的网络日志 网址:http://www.ruanyifeng.com/blog/2015/12/git-cheat-sheet.html 我每天使用 Git ,但是很多命令记不住. 一般来 ...

  9. Html==>>一些经典

    1.CSS overflow 属性 2.<input>标签 <input> 标签用于搜集用户信息. 1 type属性 根据不同的 type 属性值,输入字段拥有很多种形式.可以 ...

  10. 【iOS】通知监听

    下例为:监听文本框  accountField  内容的改变, 当发生改变时, 调用textChange方法(多次).监听结束需要移除通知. - (void)viewDidLoad { [super ...