前言
在前面的文章中提到,MemTable 会转换为SSTable,SSTable 又分为多个层。Log中也会记录当前已经成功执行的指令。但是如何有效组织SSTable的层级,如何从崩溃状态恢复到崩溃前的状态呢?这里使用的组件就是Version。Version在LevelDB中的作用就是标识每一个文件的原始信息,如果SSTable 发生了变化,就会创建一个新的Version。
Version VersionEdit 和VersionSet
Version标识的是当前或者历史的某一时刻的文件,VersionEdit 表示是从上一个version到下一个version的中间态。VersionSet则是Version的集合,具体是一个双向链表。关系如下图:
每次文件发生变化都会创建出一个新的VersionEdit,最后变成新的Version,最新的Version是Version链表的尾部节点,会有一个current指向他。在LevelDB中,涉及文件变化的地方有
- MemTable 写入SSTable 包含了正常写入或者Recovery等
- 发生了SSTable的合并
每次变化都涉及到LevelDB的最重要的元数据变化,即文件的归属,MemTable转换为SSTable,所以在LevelDB中会将该操作持久化,即MANIFEST文件。但是如果当前DB一直运行,这个文件肯定会持续变大,最好的方法是定时清理,但是LevelDB并没有做这个操作,而是每次启动的时候会根据配置判断是否新建一个MANIFEST文件,而当前的MANIFEST的名称会被写在CURRENT文件,每次重启的时候会先读取current文件,然后获取里面的MANIFEST文件名称,从而读取里面的文件。因为此类操作其实和WAL日志差不多,所以读写都是复用的Log的写入,只是Record的内容不一样而已。
MANIFEST 文件的读写
在前面的MemTable中提到过,会将整个MemTable中的最大值,最小值记录到一个meta中,这个meta就是Version中的FileMetaData,里面的属性有:
arduino
int refs; // 被version引用的值
int allowed_seeks; // Seeks allowed until compaction 记录被查找的次数,用来做合并处理的
uint64_t number; // 文件号
uint64_t file_size; // File size in bytes 当前文件的大小,BuildTable中的MemTable的大小
InternalKey smallest; // Smallest internal key served by table // 当前文件最小的Key
InternalKey largest; // Largest internal key served by table //当前文件最大的Key
在将MemTable 写入到SSTable的时候,首先就是获取当前的Version
ini
VersionEdit edit;
Version* base = versions_->current();
base->Ref();
Status s = WriteLevel0Table(imm_, &edit, base);
base->Unref();
Ref 方法是用来给Version做引用计数的,这个是用于析构函数的,的那个refs的值小于0 的时候就可以回收掉这个Version了。Unref 则是引用减一。
在调用WriteLevel0Table 之前,此时的edit为初始默认值,Base为current的Version。详细看下WriteLevel0Table的方法:
scss
Status DBImpl::WriteLevel0Table(MemTable* mem, VersionEdit* edit,
Version* base) {
mutex_.AssertHeld(); // 判断是是否拥有锁
const uint64_t start_micros = env_->NowMicros(); // 开始时间
FileMetaData meta; // 新建Meta数据
meta.number = versions_->NewFileNumber();// 从version_set中获取下一个文件号,这个文件号是全局递增的
pending_outputs_.insert(meta.number); //用于记录使用的文件,避免合并过程中将正在使用的文件删除的情况
Iterator* iter = mem->NewIterator();
Log(options_.info_log, "Level-0 table #%llu: started",
(unsigned long long)meta.number);
Status s;
{
mutex_.Unlock(); // 这里释放了锁,也就是说当前可能存在后台线程在删除文件
// 创建Table,生成文件,将file的部分原信息(最大最小key,size)
s = BuildTable(dbname_, env_, options_, table_cache_, iter, &meta);
mutex_.Lock(); // 创建完以后重新获取锁
}
Log(options_.info_log, "Level-0 table #%llu: %lld bytes %s",
(unsigned long long)meta.number, (unsigned long long)meta.file_size,
s.ToString().c_str());
delete iter;
// 从pending_outputs_释放,为什么这里可以释放呢,因为此时已经获取了mutex_锁,所以删除文件的方法就无法获取到这个锁从而删除数据了
pending_outputs_.erase(meta.number);
// Note that if file_size is zero, the file has been deleted and
// should not be added to the manifest.
int level = 0;
if (s.ok() && meta.file_size > 0) {
const Slice min_user_key = meta.smallest.user_key();
const Slice max_user_key = meta.largest.user_key();
if (base != nullptr) {
// 尽可能的将文件往多的层级推
level = base->PickLevelForMemTableOutput(min_user_key, max_user_key);
}
// 将文件写入到version set的newfiles中
edit->AddFile(level, meta.number, meta.file_size, meta.smallest,
meta.largest);
}
CompactionStats stats;
stats.micros = env_->NowMicros() - start_micros;
stats.bytes_written = meta.file_size;
stats_[level].Add(stats);
return s;
}
此时Edit中包含了当前file的Meta和level
ini
void AddFile(int level, uint64_t file, uint64_t file_size,
const InternalKey& smallest, const InternalKey& largest) {
FileMetaData f;
f.number = file;
f.file_size = file_size;
f.smallest = smallest;
f.largest = largest;
new_files_.push_back(std::make_pair(level, f));
}
为什么这里是new_files new,按道理说此时的数据edit是new出来的,岂不是每次应该只有一个edit吗?其实单独从MemTable 写入SStable的方法CompactMemTable 方法来看,确实如此。但是在recovery的时候就不一定的,肯定存在两个MemTable 都可以Compact的情况,但是可以看做一个version,毕竟是在恢复状态。
接下来就进入到了Edit 到version的变化:
scss
// Replace immutable memtable with the generated Table
if (s.ok()) {
edit.SetPrevLogNumber(0);
edit.SetLogNumber(logfile_number_); // Earlier logs no longer needed
s = versions_->LogAndApply(&edit, &mutex_);
}
LogAndApply 方法中包含了version 的使用,一个就是从edit中新建Version
scss
Version* v = new Version(this);
{
Builder builder(this, current_);
builder.Apply(edit);
builder.SaveTo(v);
}
Finalize(v);
然后就是将Version 写入current中:
ini
void VersionSet::AppendVersion(Version* v) {
// Make "v" current
assert(v->refs_ == 0);
assert(v != current_);
if (current_ != nullptr) {
current_->Unref();
}
current_ = v;
v->Ref();
// Append to linked list
v->prev_ = dummy_versions_.prev_;
v->next_ = &dummy_versions_;
v->prev_->next_ = v;
v->next_->prev_ = v;
}
写入到MAINFEST中的文件格式是:
ini
kComparator = 1,
kLogNumber = 2,
kNextFileNumber = 3,
kLastSequence = 4,
kCompactPointer = 5,
kDeletedFile = 6,
kNewFile = 7,
// 8 was used for large value refs
kPrevLogNumber = 9
就是上面的Tag,加上对应的值。最后写入方式和Log的整体写入方式一致,解析方式其实也就是按照Tag将数据还原,这里就不赘述了。
总结
比较潦草的解释了下Version的创建和作用,Version 的主要作用还是为了组织当前的SSTable文件。