01-简介
谷歌曾经发布过三篇大名鼎鼎的论文,《GFS-Google FileSystem》、《BigTable》、《MapReduce》,其中BigTable中描述了分布式数据库的实现,而leveldb正是该论文中所描述的tablet的具体实现。同时,leveldb的作者就是《BigTable》论文的作者。leveldb是一个非常简洁且具有代表意义的基于LSM TREE的存储引擎,堪称经典。掌握了LEVELDB,也就掌握了LSM TREE思想的精髓。 leveldb存储引擎的原理框架如下图所示: 上图简单展示了 LevelDB 的整体架构。LevelDB 的静态结构主要由六个部分组成: MemTable(wTable):内存数据结构,具体实现是 SkipList。 接受用户的读写请求,新的数据修改会首先在这里写入。 Immutable MemTable(rTable):当 MemTable 的大小达到设定的阈值时,会变成 Immutable MemTable,只接受读操作,不再接受写操作,后续由后台线程 Flush 到磁盘上。 SST Files:Sorted String Table Files,磁盘数据存储文件。分为 Level0 到 LevelN 多层,每一层包含多个 SST 文件,文件内数据有序。Level0 直接由 Immutable Memtable Flush 得到,其它每一层的数据由上一层进行 Compaction 得到。 Manifest Files:Manifest 文件中记录 SST 文件在不同 Level 的分布,单个 SST 文件的最大、最小 key,以及其他一些 LevelDB 需要的元信息。由于 LevelDB 支持 snapshot,需要维护多版本,因此可能同时存在多个 Manifest 文件。 Current File:由于 Manifest 文件可能存在多个,Current 记录的是当前的 Manifest 文件名。 Log Files (WAL):用于防止 进程重启MemTable 丢数据的日志文件。
02-leveldb实现
leveldb 的实现在设计思想上类似于单个Bigtable tablet。但是,构成表示的文件的组织有些不同,下面将对此进行解释。 每个数据库都由存储在目录中的一组文件表示。有几种不同类型的文件,如下所述:
01-Log files
日志文件 (*.log) 存储一系列最近的更新。每个更新都附加到当前日志文件中。当日志文件达到预定大小(默认约为 4MB)时,它会转换为排序表(见下文)并创建新的日志文件以供将来更新。 当前日志文件的副本保存在内存结构 ( memtable) 中。每次读取都会查阅此副本,以便读取操作反映所有记录的更新。 该日志文件也称为WAL,即Write ahead log。该日志的作用是当进程由于某种原因挂掉重启时,可以通过WAL日志重放崩溃前的操作。
02-Sorted Tables
排序表 (*.ldb) 存储按键排序的条目序列。每个条目要么是键的值,要么是键的删除标记。(保留删除标记以隐藏旧排序表中存在的过时值)。
这组已排序的表被组织成一系列级别。从日志文件生成的排序表被放置在一个特殊的年轻级别(也称为级别-0)中。当young文件的数量超过某个阈值(目前为4个)时,所有young文件与所有重叠的level-1文件合并在一起,产生一系列新的level-1文件(我们创建一个新的level-1每 2MB 数据的文件。)
年轻级别的文件可能包含重叠的键。但是,其他级别的文件具有不同的非重叠键范围。考虑级别编号 L,其中 L >= 1。当级别 L 中的文件组合大小超过 (10^L) MB(即级别 1 为 10MB,级别 2 为 100MB,…)时,一个文件在level-L,所有在 level-(L+1) 中重叠的文件被合并为 level-(L+1) 的一组新文件。这些合并具有将新更新从年轻级别逐渐迁移到仅使用批量读取和写入的最大级别的效果(即,最大限度地减少昂贵的查找)。
03-MANIFEST
MANIFEST 文件列出了组成每个级别的排序表集、相应的键范围和其他重要的元数据。每当重新打开数据库时,都会创建一个新的 MANIFEST 文件(文件名中嵌入了一个新编号)。MANIFEST 文件按日志的方式格式化,并且对服务状态所做的更改(随着文件的添加或删除)被附加到此日志中。
04-CURRENT
CURRENT 是一个简单的文本文件,其中包含最新的 MANIFEST 文件的名称。
05-Info log
信息性消息打印到名为 LOG 和 LOG.old 的文件中。
06-Others
用于其他目的的其他文件(LOCK、*.dbtmp)。
07-Level 0
当日志文件增长到一定大小(默认为 4MB)时:创建一个全新的内存表和日志文件,并在此处进行未来的更新。 在后台: 将前一个 memtable 的内容写入 sstable。 丢弃内存表。 删除旧的日志文件和旧的 memtable。 将新的 sstable 添加到年轻(0 级)级别。
08-Compactions
当级别 L 的大小超过其限制时,我们在后台线程中对其进行压缩。压缩从级别 L 中选择一个文件,并从下一个级别 L+1 中选择所有重叠的文件。请注意,如果 level-L 文件仅与 level-(L+1) 文件的一部分重叠,则 level-(L+1) 处的整个文件将用作压缩的输入,并将在压缩后丢弃。另外:因为 level-0 是特殊的(其中的文件可能相互重叠),我们特别对待从 level-0 到 level-1 的压缩:一个 level-0 压缩可能会选择多个 level-0 文件,以防其中一些文件相互重叠。
压缩合并挑选的文件的内容以产生一系列级别-(L+1) 文件。在当前输出文件达到目标文件大小(2MB)后,我们切换到生成新的(L+1)级文件。当当前输出文件的键范围增长到足以与十多个级别(L+2)文件重叠时,我们也会切换到新的输出文件。最后一条规则确保稍后对 level-(L+1) 文件的压缩不会从 level-(L+2) 中提取太多数据。
旧文件被丢弃,新文件被添加到服务状态。
特定级别的压缩在密钥空间中旋转。更详细地说,对于每个级别 L,我们会记住级别 L 的最后一次压缩的结束键。级别 L 的下一次压缩将选择在该键之后开始的第一个文件(如果有,则环绕到键空间的开头是没有这样的文件)。
压缩删除覆盖的值。如果没有更高编号的级别包含范围与当前键重叠的文件,它们也会删除删除标记。
09-Timing
0 级压缩将从 0 级读取最多四个 1MB 文件,最坏的情况是所有 1 级文件(10MB)。即,我们将读取 14MB 并写入 14MB。
除了特殊的 level-0 压缩之外,我们将从 level L 中选择一个 2MB 的文件。在最坏的情况下,这将与 level L+1 重叠 ~ 12 个文件(10 因为 level-(L+1) 是大小的十倍L 级的文件范围,另外两个在边界,因为 L 级的文件范围通常不会与 L+1 级的文件范围对齐)。因此,压缩将读取 26MB 并写入 26MB。假设磁盘 IO 速率为 100MB/s(现代驱动器的大致范围),最差的压缩成本将约为 0.5 秒。
如果我们将后台写入限制为较小的值,例如 100MB/s 速度的 10%,则压缩可能需要 5 秒。如果用户以 10MB/s 的速度写入,我们可能会构建大量的 0 级文件(约 50 个来容纳 5*10MB)。由于在每次读取时将更多文件合并在一起的开销,这可能会显着增加读取成本。
解决方案1:为了减少这个问题,我们可能想在0级文件数量很大的情况下增加日志切换阈值。虽然缺点是这个阈值越大,我们需要更多的内存来保存相应的 memtable。
解决方案 2:我们可能希望在 level-0 文件数量增加时人为降低写入速率。
解决方案 3:我们致力于降低非常广泛的合并成本。也许大多数 0 级文件的块在缓存中都是未压缩的,我们只需要担心合并迭代器中的 O(N) 复杂度。
10-文件数
与其总是制作 2MB 的文件,我们可以为更大的级别制作更大的文件以减少总文件数,但代价是更多的突发压缩。或者,我们可以将文件集分片到多个目录中。
2011 年 2 月 4 日在 ext3 文件系统上进行的一项实验显示,在具有不同数量文件的目录中打开 100K 文件的时间如下:
目录中的文件 | 打开文件的微秒 |
---|
1000 | 9 | 10000 | 10 | 100000 | 16 |
那么,在现代文件系统上,甚至可能不需要分片?
11-恢复
阅读 CURRENT 以查找最新提交的清单的名称 读取命名的 MANIFEST 文件 清理陈旧的文件 我们可以在这里打开所有的 sstables,但最好是偷懒… 将日志块转换为新的 0 级 sstable 开始将新写入定向到具有恢复序列的新日志文件
12-文件的垃圾回收
RemoveObsoleteFiles()在每次压缩结束和恢复结束时调用。它查找数据库中所有文件的名称。它删除所有不是当前日志文件的日志文件。它会删除所有未从某个级别引用且不是活动压缩的输出的表文件。
|