|
| 1 | +--- |
| 2 | +layout: post |
| 3 | +categories: [LevelDB] |
| 4 | +description: none |
| 5 | +keywords: LevelDB |
| 6 | +--- |
| 7 | +# LevelDB版本控制Version |
| 8 | + |
| 9 | +## 版本控制 |
| 10 | +版本Version记录着LevelDB的元数据信息,LevelDB的元信息是按照版本控制的方式来管理的,一个版本Version便是一套完整的元信息记录。 |
| 11 | + |
| 12 | +LevelDB为何要引入版本控制?正常来说,当版本A升级到版本B后,版本A就是老版本了,这个时候实际上是可以删掉的。但是,在LevelDB中,版本A有可能还在服务读请求,因此,还不能扔掉,那么就需要同时保留这两个版本A、B,因此就形成了A->B这样的链接关系来记录,所有VersionSet就是利用链表来组织的,其中,Version dummy_versions_是双向链表的头指针,Version* current_是当前最新版本。当某一个Version不再服务读请求之后,这个Version就会从双向链表中移除。 |
| 13 | + |
| 14 | + |
| 15 | +下面我们来解释这些跟版本相关的概念:Version、VersionEdit、VersionSet。 |
| 16 | +版本Version |
| 17 | +``` |
| 18 | +class Version { |
| 19 | + VersionSet* vset_; // VersionSet to which this Version belongs |
| 20 | + Version* next_; // Next version in linked list |
| 21 | + Version* prev_; // Previous version in linked list |
| 22 | + int refs_; // Number of live refs to this version |
| 23 | + // List of files per level |
| 24 | + std::vector<FileMetaData*> files_[config::kNumLevels]; |
| 25 | + // Next file to compact based on seek stats. |
| 26 | + FileMetaData* file_to_compact_; |
| 27 | + int file_to_compact_level_; |
| 28 | + // Level that should be compacted next and its compaction score. |
| 29 | + // Score < 1 means compaction is not strictly needed. These fields |
| 30 | + // are initialized by Finalize(). |
| 31 | + double compaction_score_; |
| 32 | + int compaction_level_; |
| 33 | +}; |
| 34 | +``` |
| 35 | +这是一个静态的概念,我们看到Version中的成员变量非常的多,可以分为几个功能: |
| 36 | + |
| 37 | +1)前面4个成员变量与VersionSet相关的Version双向链表:这是将多个Version链接起来的。 |
| 38 | + |
| 39 | +2)每个版本Version都记录了它所包含的各个level中的所有文件,即:files_成员变量std::vector<FileMetaData*> files_[config::kNumLevels],它是一个数组,数组下标就是level,每个level记录的是文件元数据集合,也就是该level所包含的全部文件元数据信息。 |
| 40 | + |
| 41 | +3)与文件压缩相关的信息:LevelDB通过合并操作来生成一个新的版本,因此,需要在当前Version中指定如何合并。当前level的一个文件需要与下一级里面的文件合并生成多个新文件,那就要求当前版中需要记录如何生成下一个版本,Version中就是通过最后4个compaction成员变量来记录的。 |
| 42 | + |
| 43 | +版本增量VersionEdit |
| 44 | +``` |
| 45 | +class VersionEdit { |
| 46 | + private: |
| 47 | + friend class VersionSet; |
| 48 | + typedef std::set<std::pair<int, uint64_t>> DeletedFileSet; |
| 49 | + std::string comparator_; |
| 50 | + uint64_t log_number_; |
| 51 | + uint64_t prev_log_number_; |
| 52 | + uint64_t next_file_number_; |
| 53 | + SequenceNumber last_sequence_; |
| 54 | + bool has_comparator_; |
| 55 | + bool has_log_number_; |
| 56 | + bool has_prev_log_number_; |
| 57 | + bool has_next_file_number_; |
| 58 | + bool has_last_sequence_; |
| 59 | + std::vector<std::pair<int, InternalKey>> compact_pointers_; |
| 60 | + DeletedFileSet deleted_files_; |
| 61 | + std::vector<std::pair<int, FileMetaData>> new_files_; |
| 62 | +}; |
| 63 | +``` |
| 64 | +这是一个动态的概念,delta类型,是相对当前版本的增量信息,同样包括了几个功能: |
| 65 | + |
| 66 | +1)与文件序号相关的记录:Version在合并时,新生成的文件会使用统一的序号,所以新文件的序号也会发生变化。这些文件序号变化也需要做为更改的一部分记录下来,各种has_xxx指示符就是说明这个序号是否发生了变化。而序号的更改,并不是累加到展现到Version上,而是直接作用于VersionSet。 |
| 67 | + |
| 68 | +2)压缩指针compact_pointers_:由于SSTable文件数量可能庞大,合并操作并不是一次性完成的,而且过多的压缩Compaction也会占用机器CPU资源,因此,压缩是阶段式进行的。压缩指针compact_pointers_记录在版本增量VersionEdit中,并不记录在版本Version中,每个Level下一次合并的位置最终是记录在VersionSet中的compact_pointer_。 |
| 69 | + |
| 70 | +每一个Version需要合并的时候,都会经过一定的计算得到每个Level下一次合并的位置,在合并操作Apply()时,直接使用压缩指针compact_pointers_中记录的Level作为索引,因为是版本的Apply,这意味着总是后面的一个版本增量VersionEdit去覆盖前面一个版本增量VersionEdit的合并位置,因此可直接赋值覆盖VersionSet中的compact_pointer_。 |
| 71 | + |
| 72 | +3)与文件变化有关的记录:新增文件信息new_files_和删除文件信息deleted_files_,这两个成员变量都是一个std::pair<>对象,表示哪个level新增和删除的文件信息。 |
| 73 | + |
| 74 | +那么Version和VersionEdit是什么关系呢? |
| 75 | + |
| 76 | +我们知道数学中有表达式: A + B = C,表达的是两数相加得到另外一个数,这样的思想也体现在编程领域:base + delta = new,在LevelDB中,Version和VersionEdit之间也是这种关系,如果要用LevelDB的语言来描述,那么就会变成Version + VersionEdit = new Version。那么加号和等号是怎么体现的呢?那就是VersionSet::Builder,接下来看一下VersionSet::Builder是如何定义的: |
| 77 | +``` |
| 78 | +class VersionSet::Builder { |
| 79 | + private: |
| 80 | + typedef std::set<FileMetaData*, BySmallestKey> FileSet; |
| 81 | + struct LevelState { |
| 82 | + std::set<uint64_t> deleted_files; |
| 83 | + FileSet* added_files; |
| 84 | + }; |
| 85 | + VersionSet* vset_; |
| 86 | + Version* base_; |
| 87 | + LevelState levels_[config::kNumLevels]; |
| 88 | +}; |
| 89 | +``` |
| 90 | +A + B = C用LevelDB的语言来描述,就是如下: |
| 91 | +``` |
| 92 | +Version* v = new Version(this); |
| 93 | +{ |
| 94 | + Builder builder(this, current_); //把current_作为基础项 |
| 95 | + builder.Apply(edit); //累加增量版本edit:current_ + edit |
| 96 | + builder.SaveTo(v); //把结果保存到新版本v:current_ + edit = v |
| 97 | +} |
| 98 | +``` |
| 99 | +将增量版本中记录的删除和新增的文件记录到LevelState levels_[config::kNumLevels]中,然后和当前版本Current中记录的已存在版本,一起生成一个新版本Version。 |
| 100 | + |
| 101 | +具体如何合并(+)?我们来看builder.Apply(edit)的实现。 |
| 102 | + |
| 103 | +1)更新vset_中当前压缩指针:把增量版本中压缩指针的位置记录到vset_中。 |
| 104 | + |
| 105 | +2)把增量版本中删除的文件file记录到Builder.levels_的已删除文件中。 |
| 106 | + |
| 107 | +3)把增量版本中新增的文件file记录到Builder.levels_的已添加文件中。 |
| 108 | + |
| 109 | +代码: |
| 110 | +``` |
| 111 | +// Apply all of the edits in *edit to the current state. |
| 112 | +void Apply(VersionEdit* edit) { |
| 113 | + // Update compaction pointers 把增量版本中压缩指针的位置记录到vset_中 |
| 114 | + |-for (size_t i = 0; i < edit->compact_pointers_.size(); i++) { |
| 115 | + const int level = edit->compact_pointers_[i].first; |
| 116 | + vset_->compact_pointer_[level] = |
| 117 | + edit->compact_pointers_[i].second.Encode().ToString(); |
| 118 | + |-} |
| 119 | + // Delete files 把增量版本中删除的文件file记录到Builder.levels_的已删除文件中 |
| 120 | + |-for (const auto& deleted_file_set_kvp : edit->deleted_files_) { |
| 121 | + const int level = deleted_file_set_kvp.first; |
| 122 | + const uint64_t number = deleted_file_set_kvp.second; |
| 123 | + levels_[level].deleted_files.insert(number); |
| 124 | + |-} |
| 125 | + // Add new files 把增量版本中新增的文件file记录到Builder.levels_的已添加文件中 |
| 126 | + |-for (size_t i = 0; i < edit->new_files_.size(); i++) { |
| 127 | + const int level = edit->new_files_[i].first; |
| 128 | + FileMetaData* f = new FileMetaData(edit->new_files_[i].second); |
| 129 | + f->refs = 1; |
| 130 | + f->allowed_seeks = static_cast<int>((f->file_size / 16384U)); //为了提高读性能,文件大小除以16K可以得到允许的seek次数,超过这个次数,将触发文件被压缩Compaction。 |
| 131 | + if (f->allowed_seeks < 100) f->allowed_seeks = 100; |
| 132 | + levels_[level].deleted_files.erase(f->number); |
| 133 | + levels_[level].added_files->insert(f); |
| 134 | + |-} |
| 135 | +``` |
| 136 | +等号(=)的处理:我们来看builder.SaveTo(v)的实现 |
| 137 | + |
| 138 | +遍历所有的层级level: |
| 139 | + |
| 140 | +1)把base_files和added这两个有序的数组合并到一起。 |
| 141 | + |
| 142 | +2)遍历这些新增文件和base文件:看一下file是否是在要删除的列表里面,如果不在,那么添加到version v相应的level的文件列表中。 |
| 143 | + |
| 144 | +代码: |
| 145 | +``` |
| 146 | +// Save the current state in *v. |
| 147 | +void SaveTo(Version* v) { |
| 148 | + BySmallestKey cmp;//[zll]:这个对象的操作方法operator返回是否较小的key的bool值 |
| 149 | + cmp.internal_comparator = &vset_->icmp_; |
| 150 | + for (int level = 0; level < config::kNumLevels; level++) {//[zll]:遍历所有的层级level |
| 151 | + // Merge the set of added files with the set of pre-existing files. |
| 152 | + // Drop any deleted files. Store the result in *v. |
| 153 | + const std::vector<FileMetaData*>& base_files = base_->files_[level];//[zll]:每一层级中, |
| 154 | +当前版本Current里面的已存在文件。 |
| 155 | + std::vector<FileMetaData*>::const_iterator base_iter = base_files.begin(); |
| 156 | + std::vector<FileMetaData*>::const_iterator base_end = base_files.end(); |
| 157 | + const FileSet* added_files = levels_[level].added_files;//[zll]:Builder里面记录的增量版本 |
| 158 | +的新添加文件状态 |
| 159 | + v->files_[level].reserve(base_files.size() + added_files->size());//[zll]:待保存的版本 |
| 160 | +Version中文件files_扩展为当前版本Current里面已存在的文件+增量版本中新增加的文件。 |
| 161 | + for (const auto& added_file : *added_files) {//[zll]:遍历这些新增文件 |
| 162 | + // Add all smaller files listed in base_ |
| 163 | + for (std::vector<FileMetaData*>::const_iterator bpos = |
| 164 | + std::upper_bound(base_iter, base_end, added_file, cmp);//[zll]:通过二分 |
| 165 | +查找,找到第一个不满足cmp规则的文件,即:那些较小的文件 |
| 166 | + base_iter != bpos; ++base_iter) { |
| 167 | + MaybeAddFile(v, level, *base_iter);//[zll]:把这些较小的文件追加到待保存版本Version中 |
| 168 | + } |
| 169 | + MaybeAddFile(v, level, added_file);//[zll]:把新增版本中的文件追加到待保存版本Version中 |
| 170 | + } |
| 171 | + // Add remaining base files |
| 172 | + for (; base_iter != base_end; ++base_iter) { |
| 173 | + MaybeAddFile(v, level, *base_iter);//[zll]:然后把当前版本CurrentVersion中的文件追加 |
| 174 | +到待保存版本Version中 |
| 175 | + } |
| 176 | + } |
| 177 | + } |
| 178 | +``` |
| 179 | +看一下file是否是在要删除的列表里面,如果不在,那么添加到version v相应的level的文件列表中。 |
| 180 | +``` |
| 181 | + void MaybeAddFile(Version* v, int level, FileMetaData* f) { |
| 182 | + if (levels_[level].deleted_files.count(f->number) > 0) { |
| 183 | + // File is deleted: do nothing |
| 184 | + } else { |
| 185 | + std::vector<FileMetaData*>* files = &v->files_[level]; |
| 186 | + if (level > 0 && !files->empty()) { |
| 187 | + // Must not overlap |
| 188 | + assert(vset_->icmp_.Compare((*files)[files->size() - 1]->largest, |
| 189 | + f->smallest) < 0); |
| 190 | + } |
| 191 | + f->refs++; |
| 192 | + files->push_back(f); |
| 193 | + } |
| 194 | + } |
| 195 | +}; |
| 196 | +``` |
| 197 | + |
| 198 | +文件元数据FileMetaData |
| 199 | +``` |
| 200 | +struct FileMetaData { |
| 201 | + FileMetaData() : refs(0), allowed_seeks(1 << 30), file_size(0) {} |
| 202 | + int refs; |
| 203 | + int allowed_seeks; // Seeks allowed until compaction |
| 204 | + uint64_t number; |
| 205 | + uint64_t file_size; // File size in bytes |
| 206 | + InternalKey smallest; // Smallest internal key served by table |
| 207 | + InternalKey largest; // Largest internal key served by table |
| 208 | +}; |
| 209 | +``` |
| 210 | +文件的元数据信息比较简单,包含文件号、文件大小、最小key、最大key、允许查找的次数、引用计数。对于查找key来说,最重要的是最小key和最大key,在一个文件中查找key,只要判断这个key是否在这个[smallest, largest]区间,就可以很快判定。 |
| 211 | + |
| 212 | +版本集VersionSet |
| 213 | +``` |
| 214 | +class VersionSet { |
| 215 | + Env* const env_; |
| 216 | + const std::string dbname_; |
| 217 | + const Options* const options_; |
| 218 | + TableCache* const table_cache_; |
| 219 | + const InternalKeyComparator icmp_; |
| 220 | + uint64_t next_file_number_; //下一个manifest文件编号,在manifest日志里 |
| 221 | + uint64_t manifest_file_number_; //manifest文件编号,每次重启后递增 |
| 222 | + uint64_t last_sequence_; //sequence号,用于snapshot,每次写入操作都会递增 |
| 223 | + uint64_t log_number_; //WAL日志文件编号 |
| 224 | + uint64_t prev_log_number_; // 0 or backing store for memtable being compacted |
| 225 | + // Opened lazily |
| 226 | + WritableFile* descriptor_file_; |
| 227 | + log::Writer* descriptor_log_; |
| 228 | + Version dummy_versions_; // Head of circular doubly-linked list of versions. |
| 229 | + Version* current_; // == dummy_versions_.prev_ |
| 230 | + // Per-level key at which the next compaction at that level should start. |
| 231 | + // Either an empty string, or a valid InternalKey. |
| 232 | + std::string compact_pointer_[config::kNumLevels]; |
| 233 | +}; |
| 234 | +``` |
| 235 | +用于管理所有的Version,当不断有新版本生成的时候,那么就需要不断地Append到VersionSet里面,当旧的Version不再服务读请求之后,这个Version就会从双向链表中移除,Version* current_用于不会被移出。VersionSet同样包括了几个功能: |
| 236 | + |
| 237 | +1)与Compaction压缩信息相关:每个Level下一次合并的位置最终是记录在VersionSet中的compact_pointer_。 |
| 238 | + |
| 239 | +2)SSTable文件LRU缓存:TableCache* const table_cache_ |
| 240 | + |
| 241 | +3)InternalKey比较器:InternalKeyComparator icmp_ |
| 242 | + |
| 243 | +4)记录版本增量VersionEdit合并之后,新文件序号的最新值。 |
| 244 | + |
| 245 | +5)MANIFEST文件WritableFile* descriptor_file_ |
| 246 | + |
| 247 | +6)WAL日志文件log::Writer* descriptor_log_ |
| 248 | + |
| 249 | +7)版本链表:正常来说,当版本A升级到版本B后,版本A就是老版本了,这个时候实际上是可以删掉的。但是,在LevelDB中,版本A有可能还在服务读请求,因此,还不能扔掉,那么就需要同时保留这两个版本A、B,因此就形成了A->B这样的链接关系来记录,所有VersionSet就是利用链表来组织的,其中,Version dummy_versions_是双向链表的头指针,Version* current_是当前最新版本。当某一个Version不再服务读请求之后,这个Version就会从双向链表中移除。 |
| 250 | + |
| 251 | +Version结构中的链表指针如下: |
| 252 | +``` |
| 253 | +class Version { |
| 254 | + VersionSet* vset_; // VersionSet to which this Version belongs |
| 255 | + Version* next_; // Next version in linked list |
| 256 | + Version* prev_; // Previous version in linked list |
| 257 | + int refs_; // Number of live refs to this version |
| 258 | + … … |
| 259 | +}; |
| 260 | +``` |
| 261 | +注意的是:当前版本current_总是指向双向链表的最后一个元素。 |
| 262 | + |
| 263 | +代码: |
| 264 | +``` |
| 265 | +void VersionSet::AppendVersion(Version* v) { |
| 266 | + // Make "v" current |
| 267 | + assert(v->refs_ == 0); |
| 268 | + assert(v != current_); |
| 269 | + if (current_ != nullptr) { |
| 270 | + current_->Unref(); |
| 271 | + } |
| 272 | + current_ = v;//当前版本current_指向最新加的这个Version |
| 273 | + v->Ref(); |
| 274 | + // Append to linked list 新版本Version追加到链表的末尾 |
| 275 | + v->prev_ = dummy_versions_.prev_; |
| 276 | + v->next_ = &dummy_versions_; |
| 277 | + v->prev_->next_ = v; |
| 278 | + v->next_->prev_ = v; |
| 279 | +} |
| 280 | +``` |
| 281 | +至此,版本Version、版本增量VersionEdit、版本集VersionSet以及VersionSet::Builder之间的密切又复杂的关系梳理完毕,总体感觉LevelDB对版本的处理还是有点乱的。 |
0 commit comments