VersionSet 是version组成一个双向循环链表。
class VersionSet{
//. . .
Env* const env_;// 操作系统封装
const std::string dbname_;
const Options* const options_;
TableCache* const table_cache_;
const InternalKeyComparator icmp_;
uint64_t next_file_number_;
uint64_t manifest_file_number_;
uint64_t last_sequence_;
uint64_t log_number_;
WritableFile* descriptor_file_;//manifest文件的写描述符
log::Writer* descriptor_log_;//manifest文件的日志包装形式
Version dummy_versions_; //versions双向链表head
Version* current_; // == dummy_versions_.prev_,当前最新的version
//每层都有一个compact pointer用于指示下次从哪里开始compact,以用于实现循环compact
std::string compact_pointer_[config::kNumLevels];
//. . .
}
VersionSet接口
构造函数
VersionSet::VersionSet(const std::string& dbname,
const Options* options,
TableCache* table_cache,
const InternalKeyComparator* cmp)
VersionSet的构造函数很简单,除了根据参数初始化,还有两个地方值得注意:
- next_file_number_从2开始;
- 创建新的Version并加入到Version链表中,并设置CURRENT=新创建version;
AppendVersion()
void VersionSet::AppendVersion(Version* v)
把v加入到versionset中的双向version链表头部,并设置为current version。并对老的current version执行Uref()。
LogAndApply()
Status VersionSet::LogAndApply(VersionEdit* edit, port::Mutex* mu)
功能:在currentversion上应用指定的VersionEdit,生成新的MANIFEST信息,保存到磁盘上,并用作current version,故为Log And Apply。
参数edit也会被函数修改。
相邻Version之间的不同仅仅是一些文件被删除另一些文件被删除。也就是说将文件变动应用在旧的Version上可以得到新的Version,这也就是Version产生的方式。LevelDB用VersionEdit来表示这种相邻Version的差值。
而LogAndApply()便是记录并应用这些差值的函数。
Status VersionSet::LogAndApply(VersionEdit* edit, port::Mutex* mu) {
if (edit->has_log_number_) {
assert(edit->log_number_ >= log_number_);
assert(edit->log_number_ < next_file_number_);
} else {
edit->SetLogNumber(log_number_);
}
if (!edit->has_prev_log_number_) {
edit->SetPrevLogNumber(prev_log_number_);
}
edit->SetNextFile(next_file_number_);
edit->SetLastSequence(last_sequence_);
//创建一个新的Version v,并把新的edit变动保存到v中。Builder很重要,我们后面进行分析
Version* v = new Version(this);
{
//这边是函数名字apply的含义,应用versionedit生成新版本v
Builder builder(this, current_);
builder.Apply(edit);
builder.SaveTo(v);
}
Finalize(v);//为v计算执行compaction的最佳level
std::string new_manifest_file;
Status s;
if (descriptor_log_ == NULL) {
// 这里不需要unlock *mu因为我们只会在第一次调用(打开数据库时)LogAndApply时
// 才走到这里
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()) {
//生成新manifest文件时必须得记录下当前数据库的快照,不清楚见VersionEdit与MANIFEST文件那一篇
descriptor_log_ = new log::Writer(descriptor_file_);
s = WriteSnapshot(descriptor_log_);
}
}
// Unlock during expensive MANIFEST log write
{
mu->Unlock();
// 把versionedit序列化并记入MANIFEST log,这边是函数名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());
}
}
//如果刚才创建了一个MANIFEST文件,通过写一个指向它的CURRENT文件来记录最新的MANIFEST文件
if (s.ok() && !new_manifest_file.empty()) {
s = SetCurrentFile(env_, dbname_, manifest_file_number_);
}
mu->Lock();
}
// Install the new version
if (s.ok()) {
AppendVersion(v);// 安装这个version
log_number_ = edit->log_number_;
prev_log_number_ = edit->prev_log_number_;
} else {
delete v;
if (!new_manifest_file.empty()) {
delete descriptor_log_;
delete descriptor_file_;
descriptor_log_ = NULL;
descriptor_file_ = NULL;
env_->DeleteFile(new_manifest_file);
}
}
return s;
}
VersionSet::Builder类
Builder是一个内部辅助类,其主要作用是:
1 把一个MANIFEST记录的versionedit应用到版本管理器VersionSet中;
2 把当前的版本状态设置到一个Version对象中。
成员与构造
Builder的vset_与base_都是调用者传入的,此外它还为FileMetaData定义了一个比较类BySmallestKey,首先依照文件的min key,小的在前;如果min key相等则file number小的在前,即越早创建的文件在前
typedefstd::set<FileMetaData*, BySmallestKey> FileSet;
struct LevelState { // 这个是记录添加和删除的文件
std::set<uint64_t>deleted_files;
FileSet* added_files; // 保证添加文件的顺序是有效定义的
};
VersionSet* vset_;
Version* base_;
LevelStatelevels_[config::kNumLevels];
// 其接口有3个:
void Apply(VersionEdit* edit)
void SaveTo(Version* v)
void MaybeAddFile(Version* v, int level, FileMetaData* f)
Apply()
注意除了compaction点直接修改了vset_,其它删除和新加文件的变动只是先存储在Builder自己的成员变量中,在调用SaveTo(v)函数时才施加到v上。
void Apply(VersionEdit* edit) {
//把edit记录的compaction点应用到当前状态
for (size_t i = 0; i < edit->compact_pointers_.size(); i++) {
const int level = edit->compact_pointers_[i].first;
vset_->compact_pointer_[level] =
edit->compact_pointers_[i].second.Encode().ToString();
}
// 把edit记录的已删除文件记录到Builder
const VersionEdit::DeletedFileSet& del = edit->deleted_files_;
for (VersionEdit::DeletedFileSet::const_iterator iter = del.begin();
iter != del.end();
++iter) {
const int level = iter->first;
const uint64_t number = iter->second;
levels_[level].deleted_files.insert(number);
}
//把edit记录的新加文件记录到Builder,这里会初始化文件的allowed_seeks值,以在文件被无谓seek指定次数后自动执行compaction
for (size_t i = 0; i < edit->new_files_.size(); i++) {
const int level = edit->new_files_[i].first;
FileMetaData* f = new FileMetaData(edit->new_files_[i].second);
f->refs = 1;
//如何设置seeks次数呢?文件大小除以16k,不到100算100
f->allowed_seeks = (f->file_size / 16384);
if (f->allowed_seeks < 100) f->allowed_seeks = 100;
levels_[level].deleted_files.erase(f->number);
levels_[level].added_files->insert(f);
}
}
值allowed_seeks事关compaction的优化,其计算依据如下,首先假设:
1 一次seek时间为10ms
2 写入10MB数据的时间为10ms(100MB/s)
3 compact 1MB的数据需要执行25MB的IO
->从本层读取1MB
->从下一层读取10-12MB(文件的key range边界可能是非对齐的)
->向下一层写入10-12MB
这意味这25次seek的代价等同于compact 1MB的数据,也就是一次seek花费的时间大约相当于compact 40KB的数据。基于保守的角度考虑,对于每16KB的数据,我们允许它在触发compaction之前能做一次seek。
SaveTo()
把当前的状态存储到v中返回,函数声明:void SaveTo(Version* v)
void SaveTo(Version* v) {
BySmallestKey cmp;
cmp.internal_comparator = &vset_->icmp_;
for (int level = 0; level < config::kNumLevels; level++) {
const std::vector<FileMetaData*>& base_files = base_->files_[level];
std::vector<FileMetaData*>::const_iterator base_iter = base_files.begin();
std::vector<FileMetaData*>::const_iterator base_end = base_files.end();
const FileSet* added = levels_[level].added_files;
//给v的level层预留空间,最多有base_的level层文件个数加上个level层新增的文件个数的文件
v->files_[level].reserve(base_files.size() + added->size());
//遍历第level层新增的文件个数
for (FileSet::const_iterator added_iter = added->begin();
added_iter != added->end();
++added_iter) {
// //加入base_中小于added_iter的那些文件
for (std::vector<FileMetaData*>::const_iterator bpos
= std::upper_bound(base_iter, base_end, *added_iter, cmp);
base_iter != bpos;
++base_iter) {
MaybeAddFile(v, level, *base_iter);
}
MaybeAddFile(v, level, *added_iter);
}
// 添加base_剩余的那些文件
for (; base_iter != base_end; ++base_iter) {
MaybeAddFile(v, level, *base_iter);
}
}
我很好奇删除的文件呢?怎么没有见应用进base呢?
其实答案就在MaybeAddFile中
MaybeAddFile()
voidMaybeAddFile(Version* v, int level, FileMetaData* f)
函数声明:void MaybeAddFile(Version* v, int level, FileMetaData* f)
该函数尝试将f加入到v的第level层文件中。
要满足两个条件:
1 文件不能被删除,也就是不能在levels_[level].deleted_files集合中;
2 保证文件之间的key是连续的,即基于比较器vset_->icmp_,f的min key要大于levels_[level]集合中最后一个文件的max key;
void MaybeAddFile(Version* v, int level, FileMetaData* f) {
if (levels_[level].deleted_files.count(f->number) > 0) {
// 如果新加入的文件在levels_[level].deleted_files集合中,就不会被加入v的第level层文件中。
//原来version的每一个文件也都需要通过MaybeAddFile加入到新version v中,所以自然就删除了原来
//version中没用的文件了
} else {
std::vector<FileMetaData*>* files = &v->files_[level];
if (level > 0 && !files->empty()) {
assert(vset_->icmp_.Compare((*files)[files->size()-1]->largest,
f->smallest) < 0);
}
f->refs++;
files->push_back(f);
}
}
Status VersionSet::Recover(bool *save_manifest)
对于VersionSet而言,Recover就是根据CURRENT指定的MANIFEST,读取db元信息。
对于该函数的具体分析,我们将放在leveldb的recover专题里分析。
void VersionSet::Finalize(Version* v)
该函数依照规则为下次的compaction计算出最适用的level,对于level 0和>0需要分别对待
void VersionSet::Finalize(Version* v) {
// Precomputed best level for next compaction
int best_level = -1;
double best_score = -1;
for (int level = 0; level < config::kNumLevels-1; level++) {
double score;
if (level == 0) {
// 对于level 0以文件个数计算,kL0_CompactionTrigger默认配置为4,因为如果0层文件多
//了会影响合并效率
score = v->files_[level].size() /
static_cast<double>(config::kL0_CompactionTrigger);
} else {
//对于level>0,根据level内的文件总大小计算
//其中函数MaxBytesForLevel根据level返回其本层文件总大小的预定最大值。
//计算规则为:1048576.0* level^10。
const uint64_t level_bytes = TotalFileSize(v->files_[level]);
score =
static_cast<double>(level_bytes) / MaxBytesForLevel(options_, level);
}
if (score > best_score) {
best_level = level;
best_score = score;
}
}
//遍历了所有层后,将compaction_score值最大的level作为下次合并的level
v->compaction_level_ = best_level;
v->compaction_score_ = best_score;
}
Status VersionSet::WriteSnapshot(log::Writer* log)
把currentversion保存到*log中,信息包括comparator名字、compaction点和各级sstable文件
Status VersionSet::WriteSnapshot(log::Writer* log) {
VersionEdit edit;
edit.SetComparatorName(icmp_.user_comparator()->Name());
// 遍历所有level,根据compact_pointer_[level],保存compaction点
for (int level = 0; level < config::kNumLevels; level++) {
if (!compact_pointer_[level].empty()) {
InternalKey key;
key.DecodeFrom(compact_pointer_[level]);
edit.SetCompactPointer(level, key);
}
}
// 遍历所有level,根据current_->files_,保存当前版本sstable文件集合
for (int level = 0; level < config::kNumLevels; level++) {
const std::vector<FileMetaData*>& files = current_->files_[level];
for (size_t i = 0; i < files.size(); i++) {
const FileMetaData* f = files[i];
edit.AddFile(level, f->number, f->file_size, f->smallest, f->largest);
}
}
//序列化并append到log(MANIFEST文件)中
std::string record;
edit.EncodeTo(&record);
return log->AddRecord(record);
}
其实WriteSnapshot就是把目前数据库的元数据信息,比如当前版本所有sst文件的元数据信息,用log的方式append到MANIFEST文件中,*注意这的versionedit和compact产生的versionedit的不同,这的versionedit记录的是当前版本的全量元数据信息,而compact产生的versionedit记录的仅仅是增量
元数据信息。*
uint64_tApproximateOffsetOf(Version* v, const InternalKey& ikey)
在指定的version中查找指定key的大概位置。
假设version中有n个sstable文件,并且落在了地i个sstable的key空间内,那么返回的位置= sstable1文件大小+sstable2文件大小 + … + sstable (i-1)文件大小
+ key在sstable i中的大概偏移。
可分为两段逻辑。
1 首先直接和sstable的max key作比较,如果key > max key,直接跳过该文件,还记得sstable文件是有序排列的。
对于level >0的文件集合而言,如果如果key < sstable文件的min key,则直接跳出循环,因为后续的sstable的min key肯定大于key。
2 key在sstable i中的大概偏移使用的是Table:: ApproximateOffsetOf(target)接口,前面分析过,它返回的是Table中>= target的key的位置。