之前再看RocksDB Version管理部分代码,查找网上资料发现关于RocksDB Version管理的内容大部分就是对LevelDB RocksDB version管理的摘抄。对于VersionEdit、VersionBuilder两者基本类似,但是RocksDB中由于新增ColumnFamily的概念,导致两者在Version管理中存在些许的差别,而网上资料对于Version管理中ColumnFamily的部分介绍较少,结合之前代码时的记录,将自己的总结分享出来。
Version管理用于维护当前RocksDB中文件列表,包括磁盘中SSTable文件和内存中的Memtable和Immutable Memtable文件。RocksDB中有Column Family的概念,可以将一个Column Family理解为独立的LSM-Tree。Column Family会维护自己的文件列表,内存中MemTable、Immutable Memtable和磁盘中按层排列的SSTable。不同的Column Family之间不共享Memtable和SSTable。每个Column Family的数据信息由ColumnFamilyData维护。RocksDB中可能会存在多个Column Family,多个Column Family由ColumnFamilySet管理。
每个Column Family中随着外部的持续写入,不断发生flush和compaction操作。每次flush和compaction都会导致磁盘SSTable文件的变化,RocksDB通过Version来记录不同时刻的磁盘SSTable文件状态。多个Version以双向链表的方式组织。具体的Version管理示意图如图1所示:
图1 RocksDB Version管理示意图
RocksDB中通过VersionSet进行Version管理。VersionSet中以双向循环链表的方式组织所有ColumnFamilyData。如果单纯以循环双向链表的方式维护所有的Column Family,查找时的时间复杂度为O(N)。为了加快查找速度,ColumnFamilySet中保存两个unordered_map,std::unordered_map<std::string, uint32_t> column_families_; std::unordered_map<uint32_t, ColumnFamilyData*> column_family_data_;第一个保存column family的name到唯一标识uint32_t(每个Column Family有一个唯一的标识)的映射,第二个保存uint32_t到具体ColumnFamilyData*的映射。可以根据Column Family name在O(1)时间内找出对应的ColumnFamilyData。
每个Column Family中的数据包括内存和磁盘的数据。ColumnFamilyData中包含指向Version head(dummy_version_)和最新Version(current_)的指针。SuperVersion用于记录Column Family相关的所有文件,内存中的Memtable、Immutable Memtable(以Vector数组的形式组织),和磁盘中的SSTable。磁盘中的SSTable用Version维护,所有Version按照产生的先后顺序插入到Version双向链表中,最先的位于最前,通过current_指向最新的Version。Version和SSTable都是以引用计数的方式维护。下面的内容是RocksDB如何维护live SSTable的方式,可以概念上大体体会Version管理的内容。
RocksDB除了WAL日志文件外,还会包含文件系统中的SSTable文件列表。每次compaction之后,compaction会生成新的输出文件添加到列表中,同时也会从列表中删除输入文件。需要注意的是,输入文件并不能立即删除,因为此时可能一些Get操作或者外部的迭代器正在使用该文件,需要保存文件至这些操作的完成或者迭代器的释放。
Version用于管理LSM-Tree中的文件列表。在每次compaction结束或者memtable被flush到硬盘,新的version被创建用来记录发生的变化。随着不断的写入、compaction的进行,RocksDB中会存在多个version,但在任何时刻只有一个当前的version版本,即current version。新的Get查询操作或者迭代器在其整个查询过程和迭代器的生命周期内都会使用current version。“过时”(没有被任何Get或者iterator迭代器使用)的version需要清除。被任何version都未使用的文件会被删除。具体的示例如下:
最初开始时version v1有三个文件:
v1={f1, f2, f3} (current)
files on the disk: f1, f2, f3
然后在v1上创建了一个iterator迭代器
v1={f1, f2, f3} (current, used by iterator1)
files on the disk: f1, f2, f3
然后内存中memtable flush,增加了一个文件f4,一个新的version被创建:
V2={f1, f2, f3, f4} (current)
V1={f1, f2, f3} (used by iterator1)
Files on the disk: f1, f2, f3, f4
然后发生了一次compaction,f2, f3和f4合并为一个新的文件f5,新的version被创建:
V3={f1, f5} (current)
V2={f1, f2, f3, f4}
V1={f1, f2, f3} (used by iterator1)
Files on the disk: f1, f2, f3, f4, f5
此时,v2既不是最新的version也没有被任何iterator或者get使用,所以v2可以被删除,f4文件也会被删除(没有任何version引用)。但v1不能被删除,因为v1仍然被iterator1使用。
V3={f1, f5} (current)
V1={f1, f2, f3} (used by iterator1)
Files on the disk: f1, f2, f3, f5
现在,如果iterator1被销毁:
V3={f1, f5}(current)
V1={f1, f2, f3}
Files on the disk: f1, f2, f3, f5
此时,v1既没有被引用也不是最新的,可以被删除,同时文件f2, f3也会被删除:
V3={f1, f5} (current)
Files on the disk: f1, f5
实现的上述操作的方式就是引用计数。SSTable文件和version都有一个reference count。当创建一个version时,该version使用的所有SSTable文件的reference count加一。同理,删除一个version时,该version使用的所有SSTable文件的reference count减一。如果文件的reference count变为0,则该文件可以被删除。
类似的,每个version也都有一个reference count。创建version时,它是最新的版本,因此它的reference count为1。如果该version不是最新的version,则其reference count减一。当该version被get或者iterator使用时,加一;get或者iterator完成时,减一。当version的reference count为0时,该version可以被删除。
RocksDB Version管理部分由Version/VersionEdit/VersionSet完成,Version管理的示例图如图1所示。VersionEdit用于记录两个相邻Version之间的差异,比如新增的文件、删除的文件等等。对Version N施加VersionEdit可以得到Version N+1,如图2所示。、
图2 VersionEdit与Version关系示意图
为了避免进程崩溃或机器宕机导致的数据丢失,RocksDB需要将元数据信息持久化到磁盘,对应的文件就是Manifest,每当有新的Version产生时都需要更新Manifest。新增数据正好对应VersionEdit内容,也就是说Manifest文件记录的是一组VersionEdit值。图3最上面的流程显示了恢复元信息的过程,即一次次应用VersionEdit的过程,这个过程会产生大量的临时Version,过于耗费资源。RocksDB引入VersionBuilder来避免产生中间Version变量,具体方式是先将所有的VersionEdit内容整理到VersionBuilder中,然后一次应用产生最终的Version。
图3 RocksDB VersionBuilder示意图
上述就是RocksDB Version管理的大致内容,清楚了实现思路之后,之后对具体每部分代码进行分析时就会轻松很多。
参考资料: