文件类型:

1
2
3
4
5
6
7
8
9
enum FileType {
  kLogFile,
  kDBLockFile,
  kTableFile,
  kDescriptorFile,
  kCurrentFile,
  kTempFile,
  kInfoLogFile  // Either the current one, or an old one
};

多版本

版本管理中相关的类

  • Version标识了一个版本,主要信息是这个版本包含的SSTable信息;
  • VersionSet是一个版本集,里面保存了Version的一个双链表,其中有一个Version是当前版本,因为只有一个实例,还保存了其它的一些全局的元数据,Dummy Version是链表头;
  • VersionEdit保存了要对Version做的修改,在一个Version上应用一个VersionEdit,可以生成一个新的Version;
  • Builder是一个帮助类,帮助Version上应用VersionEdit,生成新版本

VersionSet 是一个双链表结构,每个 Version 代表了一个版本

将当前的 版本 和 VersionEdit 合并之后,就得到了一个新的版本 VersionEdit就是一次 Minor Compaction、或者是 Major Compaction的结果

版本管理的工作流程:

  • VersionSet里保存着当前版本,以及被引用的历史版本;
  • 当有Compaction发生时,会将更改的内容写入到一个VersionEdit中;
  • 利用Builder将VersionEdit应用到当前版本上面生成一个新的版本;
  • 将新版本链接到VersionSet的双链表上面;
  • 将新的版本设置为当前版本;
  • 将旧的当前版本Unref,就是引用计数减1。

版本控制中的引用计数:

  • 每个Version包含了一个引用,当前版本的 ref 为 1
  • 当有迭代器需要遍历数据库时,这个版本的 ref ++ 等于 2
  • 其他线程不断写入,触发 compaction,生成新版本
  • 新版本 + 1,老版本 - 1
  • 因为 老版本为 1,所以不会被删除,直到迭代接受,ref = 0
  • 如果 Version 的ref 为0,则可以删除了

实际存储的时候,是存入dbname/MANIFEST-[0-9]+ 这样的文件
CURRENT 指向最新的 manifest 文件
Version 相当于 一个 manifest 文件,VersionSet 则管理多个 manifest 文件
VersionSet 头结点指向最新的 manifest 文件


FileMetaData
sstable的元信息

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
struct FileMetaData {
  FileMetaData() : refs(0), allowed_seeks(1 << 30), file_size(0) {}

  int refs;
  int allowed_seeks;     // Compaction时会介绍这个字段
  uint64_t number;
  uint64_t file_size;    // 文件大小
  InternalKey smallest;  // 文件里最小的internal key
  InternalKey largest;   // 文件里最大的internal key
};

Version
位置:db/version_set.h
读取数据库时,就需要 Version 里面的信息

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Version {
  VersionSet* vset_;  // 版本集的引用
  Version* next_;     // 版本在版本集里的next_指针
  Version* prev_;     // 版本在版本集里的prev_指针
  int refs_;          // 引用计数

  // SSTable的信息,每一项代表相应Level的SSTable信息
  // 除了Level 0外,每个Level里的文件都是按照最小键的顺序排列的,并且没有重叠
  // 通过这个数据项,搜索SSTable时,就可以从Level 0开始搜索
  std::vector<FileMetaData*> files_[config::kNumLevels];
}

VersionEdit

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class VersionEdit {
    typedef std::set<std::pair<int, uint64_t>> DeletedFileSet;

    std::string comparator_;             // 比较器的名称,持久化后,下次打开时需要对比一致
    uint64_t log_number_;                // 日志文件的编号
    uint64_t next_file_number_;          // ldb、log和MANIFEST下一个文件的编号
    SequenceNumber last_sequence_;       // 上一个使用的SequenceNumber
    bool has_comparator_;                // 记录上面4个字段是否存在,存在才会持久化的MANIFEST中
    bool has_log_number_;
    bool has_next_file_number_;
    bool has_last_sequence_

    // 和VersionSet里面的compact_pointers_相同
    std::vector<std::pair<int, InternalKey>> compact_pointers_;
    // 有哪些文件被删除,就是Version里哪些SSTable被删除
    DeletedFileSet deleted_files_;
    // 有哪些文件被增加,pair的第一个参数是Level,第二个参数是文件的元信息
    std::vector<std::pair<int, FileMetaData>> new_files_;
};

VersionEdit 中包含了两个函数

1
2
Status VersionEdit::DecodeFrom(const Slice& src)
void VersionEdit::EncodeTo(std::string* dst) const

这个就是一个 编码,一个解码
将 dst 中的信息,写入到所有变量中,以及从 变量中解码信息到 Slice 中

VersionSet

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
    Env* const env_;                      // 封装部分操作系统调用,包括文件、线程操作等
    const std::string dbname_;            // 数据库名称,Open时传入
    const Options* const options_;        // 数据库选项,Open时传入
    TableCache* const table_cache_;       // 打开的SSTable的缓存,Open时创建
    const InternalKeyComparator icmp_;    // 根据User Key生成的Internal Key的Comparator
    uint64_t next_file_number_;           // ldb、log和MANIFEST生成新文件时都有一个序号单调递增
    uint64_t manifest_file_number_;       // 当前的MANIFEST的编号
    uint64_t last_sequence_;              // 上一个使用的SequenceNumber
    uint64_t log_number_;                 // 当前的日志的编号

    WritableFile* descriptor_file_;       // MANIFEST打开的文件描述符
    log::Writer* descriptor_log_;         // MANIFEST实际存储的格式是WAL日志的格式,所以这里用来写入数据
    Version dummy_versions_;              // Version链表的头结点
    Version* current_;                    // 当前的Version

    // 这是用来记录Compact的进度,Compact总是从某一Level的最小的键开始到某个键结束,
    // 下次再从下一个键开始,所以这个就是下一次这个Level从哪个键开始Compact
    std::string compact_pointer_[config::kNumLevels];

VersionSet::Builder辅助类
主要变量:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  struct BySmallestKey {
    const InternalKeyComparator* internal_comparator;

    bool operator()(FileMetaData* f1, FileMetaData* f2) const {
      int r = internal_comparator->Compare(f1->smallest, f2->smallest);
      if (r != 0) {
        return (r < 0);
      } else {
        // Break ties by file number
        return (f1->number < f2->number);
      }
    }
  };

  typedef std::set<FileMetaData*, BySmallestKey> FileSet;
  struct LevelState {
    std::set<uint64_t> deleted_files;
    FileSet* added_files;
  };

  VersionSet* vset_;
  Version* base_;
  LevelState levels_[config::kNumLevels];

LevelFileNumIterator
主要变量

1
2
3
4
5
6
  const InternalKeyComparator icmp_;
  const std::vector<FileMetaData*>* const flist_;
  uint32_t index_;

  // Backing store for value().  Holds the file number and size.
  mutable char value_buf_[16];

其中 new 出来的 两次迭代器,就使用到了 LevelFileNumIterator

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Iterator* Version::NewConcatenatingIterator(const ReadOptions& options, int level) const {
  return NewTwoLevelIterator(new LevelFileNumIterator(vset_->icmp_, &files_[level]),
	&GetFileIterator, vset_->table_cache_, options);
}

static Iterator* GetFileIterator(void* arg, const ReadOptions& options,
                                 const Slice& file_value) {
	TableCache* cache = reinterpret_cast<TableCache*>(arg);
	return cache->NewIterator(options, DecodeFixed64(file_value.data()),
                              DecodeFixed64(file_value.data() + 8));
}

builder 版本变迁过程

总结一下

  • 整体是 VersionSet 的双链表结构,一个版本由 Version 表示
  • VersionEdit 是一次变更,比如 sstable新增,删除(通过压缩产生),通过 应用 VersionEdit 就可以得到一个新版本
  • builder 会加速变更,可以将多个 VersionEdit 一次性应用
  • 每个 Version 内部关联了一个 FileMetaData,表示一个 sstable的元数据,内部包含了 min、max key方便定位
  • MANIFEST 实际关联了一个 log 文件,还会将变更的内容当做 log 记录下来,等启动恢复的时候,就会读取这些log文件

压缩

压缩,包含两种

  • Minor Compaction,mem-table写满会后刷盘,写入到 level-0 中
  • Major Compaction,某个层的文件太多,需要将部分文件推入更高层

Minor Compaction过程

Minor Compaction 时序图

WriteLevel0Table过程

  • 得到一个新的 FileMetaData 编号,就是 VersionSet->next_file_number_++
  • 调用 BuildTable,其生成的 TableFileName 是根据 FileMetaData的编号得到的
  • 获取 min、max key,然后确定将 ss-table 推入哪个层级,调用 PickLevelForMemTableOutput
  • 将 压缩状态存入 CompactionStats[level] 中,这里的 level 固定为 PickLevelForMemTableOutput 返回的层级

BuildTable 过程:

  • 从 skip-list 的第一个元素开始遍历,一直到结束,将这些 key-value 加入到 TableBuilder
  • 同时记录 FileMetaData 的最大、最小、key数量、文件size
  • 将数据写入到磁盘

PickLevelForMemTableOutput

  • 如果跟 0层的有重叠,则推入 0层
  • 检查方式是,对于 level0,需要遍历所有文件,检查每个文件跟min、max key 是否有重叠
  • 非 level 0的,则使用 二分查找 当前层的所有文件
  • 还会检查 level 0、level 1 在其祖父层级(对应的就是 level 2、level 3中,是否重叠太多
  • 默认的最大单个文件大小为 2M,如果一次重叠的数量 > 20M,PickLevelForMemTableOutput 会继续往更深层级查找
  • 不过限制了最大查找层级为 2,这也是做了一个权衡

Major Compaction 的过程

Compaction类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// db/version_set.h

class Compaction {
    int level_;                      // Compaction文件所在的Level
    uint64_t max_output_file_size_;  // 生成的文件的最大值
    Version* input_version_;         // Compaction发生时的Version
    VersionEdit edit_;               // Compaction结果保存的VersionEdit

  // Each compaction reads inputs from "level_" and "level_+1"
  std::vector<FileMetaData*> inputs_[2];  // The two sets of inputs

  // State used to check for number of overlapping grandparent files
  // (parent == level_ + 1, grandparent == level_ + 2)
  std::vector<FileMetaData*> grandparents_;
  size_t grandparent_index_;  // Index in grandparent_starts_
  bool seen_key_;             // Some output key has been seen
  int64_t overlapped_bytes_;  // Bytes of overlap between current output
                              // and grandparent files

  // State for implementing IsBaseLevelForKey

  // level_ptrs_ holds indices into input_version_->levels_: our state
  // is that we are positioned at one of the file ranges for each
  // higher level than the ones involved in this compaction (i.e. for
  // all L >= level_ + 2).
  size_t level_ptrs_[config::kNumLevels];
}

ManualCompaction 类

1
2
3
4
5
6
7
8
  // Information for a manual compaction
  struct ManualCompaction {
    int level;
    bool done;
    const InternalKey* begin;  // null means beginning of key range
    const InternalKey* end;    // null means end of key range
    InternalKey tmp_storage;   // Used to keep track of compaction progress
  };

Compaction 的触发方式

  • Manual Compaction
  • Size Compaction
  • Seek Compaction

手动压缩的触发函数:

1
void DBImpl::TEST_CompactRange(int level, const Slice* begin, const Slice* end)

Size compation

  • 实现函数为: void VersionSet::Finalize(Version* v)
  • 对于 leve 0,如果 > 4 就会触发
  • 超过 0 的,如果超过了 10^n MB 就会触发
  • 由于 ss-table 的不变性,只有版本变更的时候才会变化
  • 再每次版本变更之后,调用 Finalize,计算出比例最大的 level,下次就从这里开始
  • 由于一次压缩要很久,这里会记录一个 level 的key,std::string compact_pointer_[config::kNumLevels];
  • 下次再压缩这一层的时候,就从这个 key 开始继续压缩

Seek Compaction

  • 为读做的优化,DB::GET、DBIter 时候,如果读取了超过一个 ss-table 则需要优化
  • 对于读取 超过一个 的ss-table,对于第一个 ss-table做一个 减操作,如果等于 0了,就需要做压缩
  • 所以 seek 是精确到某个 ss-table 的
  • 迭代的时候,是每读取 2M数据,就模拟一个key,做对应的 ss-table–
  • 初始值,static_cast((f->file_size / 16384U))
  • 一次Seek花费10ms;
  • 读写1MB的花费10ms (100MB/s);
  • 而Compaction 1MB的数据花费25MB左右的IO

执行过程

  • 执行优先级是 Manual、Size、Seek
  • 前两者是根据 start、end key 选择某一个level的多个文件
  • 后者只有确定的一个文件
  • 根据 level 层的文件,再确定 level + 1层的文件,之后再反向 level 层扩大 level 的输入范围
  • 最后调用多路合并,进行压缩 compaction

扩大输入文件集合

  • 确定起始key、结束key
  • 在level i层中,查找与起始输入文件有key重叠的文件,如图中红线所标注,最终构成level i层的输入文件;
  • 利用level i层的输入文件,在level i+1层找寻有key重叠的文件,结果为绿线标注的文件,构成level i,i+1层的输入文件;
  • 最后利用两层的输入文件,在不扩大level i+1输入文件的前提下,查找level i层的有key重叠的文件,结果为蓝线标准的文件,构成最终的输入文件; 10

归并的过程:

  • 迭代按照Internal Key的顺序进行,多个key则按seq number 降序排列
  • 相同的 key只有第一个有效,因为它的 seq 是最大的
  • 碰到一个删除时,并且它的 seq_number <= 最新快照,会判断更高层是否有这个key
  • 如果有则无法丢弃这个删除操作,因为一旦丢弃,更高层又可见了,不存在可则可以删除
  • 如果ss-table达到 2M,或者和上层文件重叠超过 10个,这两个条件任意一个触发就会 生成一个 ss-table写磁盘 do-compaction

数据删除的处理

  • 如果是覆盖写入,则当做一个删除处理,即按照降序排序,前面的覆盖后面的,后面重复的key可以忽略
  • 如果是删除标记,并且level + 1层没有这个key,则可以删除
  • 如果这个key 的seq_number <= 最老快照的序列号,则可以删除,否则不能删除(因为可能被使用中)

level 0 的压缩过程

  • 选择 key 800 - 2500
  • 因为 level 0有重叠,遍历所有文件,找到 key 重叠的文件,确定 start、end key
  • 选择 level 1 层,根据 start key、end key确定 level 1 层的文件
  • 将 level 0 和 level 1的文件做多路归并

level > 0 的压缩过程

  • 选择 key 800 - 2500
  • 因为是完全有序的,可以根据二分搜索到确定的范围,确定出 level 层的文件
  • 再确定出 level + 1 层的文件

MVCC 数据清理

  • 遍历所有文件
  • 小于 VersionSet 的 log_number_ 的文件
  • 或者不在 遍历列表中的文件,则可以删除

恢复

LevelDB打开需要做以下事情:

  • 如果数据库目录不存在,创建目录
  • 加文件锁,锁住整个数据库;
  • 读取MANIFEST文件,恢复系统关闭时的元数据,也就是版本信息,或者新建MAINFEST文件;
  • 如果上一次关闭时,MemTable里有数据,或者Immutable MemTable写入到SSTable未完成,那么需要做数据恢复,从WAL恢复数据;
  • 创建数据库相关的内存数据结构,如Version、VersionSet等 11

DBImpl::Recover做了以下事情

  • 创建数据库目录;
  • 对这个数据库里面的LOCK文件加文件锁,单进程的
  • 如果数据库不存在,那么调用DBImpl::NewDB创建新的数据库;
  • 调用VersionSet::Recover来读取MANIFEST,恢复版本信息;
  • 根据版本信息,搜索数据库目录,找到关闭时没有写入到SSTable的日志,按日志写入顺序逐个恢复日志数据
  • DBImpl::RecoverLogFile会创建一个MemTable,开始读取日志信息,将日志的数据插入到MemTable
  • 并根据需要调用DBImpl::WriteLevel0Table将MemTable写入到SSTable中

异常恢复

  • 如果元数据丢了,但是数据还在,也可以恢复的
  • 把所有sstable 当做 level 0 层,然后不断做压缩
  • 由于sstable 数量会远大于0,这样会大致大量的compaction
  • 经过很多次compation最后会稳定下来,形成新的 元数据
  • 旧的文件则会删除

参考