这篇文章是levelDB官方文档的译文,原文地址:Implementation notes
1. Files
leveldb的实现和一个单点的Bigtable tablet (section 5.3)很相近。然而,文件的组织形式又有些不太一样,下文会解释这一点。
每一个数据库都是存储在一个目录的一系列文件的集合。有以下几种不同类型的文件:
1.1 Log files
(译者注:这里的log files指的是保存key_value对的文件)
log文件(*.log)存储一系列最近的更新。每次更新都是追加在当前的log文件后面。当log文件达到预设的大小(默认是4MB),就转换成一个有序表sorted table,并为后续的更新创建一个新的log文件。
当前log文件的副本保存在内存中(the memtable)。每次读都会检索当前的log文件,因此读操作反应了所有log文件中的更新。
1.2 Sorted tables
排序表(sorted table *.sst)存储一系列的排好序的key-value entry。每个entry要么是一个key-value对,要么是key对应的删除标记。(在老版本的排序表里面,删除标记是一个隐式的过期值)
排序表的集合被组织成一系列级别(level)的形式。从一个log file生成的排序表放置在一个特殊的年轻级别(young level, 也称作level-0),当level-0的文件数量超过阈值(目前是4)时,所有level-0的文件和一些有重合的level-1级别的文件归并成一系列新的level-1级别的文件,level-1级别的文件数据大小是2MB.
level-0级别的不同文件之间存在key的重合。然而,在其他同一级别上的不同文件之间的key区间是唯一的不重合的。例如,level L(L>=1),当Level-L上的文件总和超过 10^L MB,(Level-1: 10MB, Level-2: 100MB, …),一个Level-L的文件和所有在Level-(L+1)有key重合的文件合并成一系列新的Level-(L+1)文件。这些合并通过数据块的读写逐渐将新的数据从level-0迁移到最大level,可以减少一些昂贵的磁盘寻址操作。
译者注:
每层文件的大小和总大小的关系表
level | single file size | level total size |
---|---|---|
level-0 | 1 MB | 4 MB(default 4 files) |
level-1 | 2 MB | 10 MB |
level-2 | 2 MB | 100 MB |
level-N | 2 MB | 10^N MB |
1.3 Manifest
MANIFEST文件列出了每一个level上的排序表(sorted tables)的集合 ,相应的key范围,以及一些其他重要的元数据(metadata)。数据库被重新打开的时候会生成一个新的MANIFEST文件,MANIFEST文件名里面包含一个区别于老的MANIFEST文件的数字。MANIFEST是一个log文件的格式,文件的新增和移除等服务状态的变化会追加到文件的末尾。
1.4 Current
CURRENT是一个保存当前正在使用的MANIFEST文件名的简单txt文件。
1.5 Info logs
一些输出到LOG或者LOG.old文件的有用信息。
1.6 Others
其他各式各样用途的文件,例如LOCK,*.dbtmp等。
2. Level 0
当log文件增长到超过一个大小的时候(默认是1MB):
- 新建一个全新的memtable和log文件,然后后续会更新到新的文件
- 一些后台的处理:
- 把之前替换下来的memtable的内容写到sstable
- 丢弃memtable
- 删除老的log文件和老的memtable
- level-0增加一个新的sstable
3. Compactions
(译者注:compaction不能直接翻译成压缩,因为这个是一个把Level-L文件向Level-(L+1)层合并的过程。)
当Level-L的大小达到限制之后,我们会在一个后台线程中对数据进行压缩合并。合并压缩时会从Level-L中取一个文件A,并把Level-(L+1)层中和A中存在key重合的文件全部取出来。如果A只与Level-(L+1)层的一个文件B的部分key存在重合,那么文件B会作为合并压缩的输入,并会在合并压缩完成之后丢掉。副作用:level-0的所有文件之间可能存在key的重合,所以当level-0向level-1合并的时候会有一些特殊:如果level-0的文件之间存在key重合,那么会取level-0的多个文件。
合并压缩会把输入的文件合并然后生成一系列的Level-(L+1)文件。在合并的时候生成一个新文件的条件:1,当前的输出文件达到2MB。2,如果当前的输出文件和10个以上Level-(L+2)层文件存在key重合。这个确保以后Level-(L+1)层和Level-(L+2)层合并的时候不至于一次性合并太多的文件。
当合并后的新文件合并完成并使用后丢弃旧的文件。
对一个特定层来讲,合并压缩会在所有的key之间进行轮转。例如,对于level-L层来说,我们记下来上次合并压缩的最后一个key,在下次合并压缩的时候,我们从上次记下的key后面的第一个key的那个文件开始,如果key是key space的最后一个key,就从key space的第一个重新开始。
合并压缩会丢弃被覆盖的(overwritten)值。如果更高层中不包含key的值,那么也会丢弃删除标记。
3.1 Timing
Level-0层的合并压缩会从level-0读取4个1MB的文件,最坏的情况下会和level-1的所有文件(10MB)进行合并。也就是读14MB,写14MB。
对于非level-0的合并压缩,从level-L读取一个2MB的文件,在最坏的情况下,会和level-(L+1)的最多12个文件存在key重合,那么合并压缩会读取26MB,写26MB,在100MB/S读写速度的磁盘上,最长的合并压缩将花费0.5秒。
如果我们控制后台写的速度在一个比较小的情况下,假如100MB速度的10%,那么一次合并压缩将花费5秒。如果用户以10MB的速度在写,那么就会生成很多的level-0文件。那么就会因为在每次读数据时的合并操作,显著的增加读的开销。
解决方案:
1. 当level-0文件比较多的时候,我们可以增加生成新的log文件的阈值。副作用就是,阈值越大,生成memtable时的内存开销也越大。
2. 当level-0文件数量上涨的时候,可以人为降低写速度。
3. 减少合并的开销。大多的level-0文件是未经压缩的存放在cache里面,只需要一个O(N)复杂度的合并操作。
3.2 Number of files
取代总是生成2MB的数据文件,我们可以在比较高的层上生成大的文件,降低总的文件数量,当然这会提高合并压缩时的开销。相应的,我们可以用shard的方式把文件放在多个目录中。
An experiment on an ext3
filesystem on Feb 04, 2011 shows the following timings to do 100K file opens in directories with varying number of files:
Files in directory | Microseconds to open a file |
---|---|
1000 | 9 |
10000 | 10 |
100000 | 16 |
So maybe even the sharding is not necessary on modern filesystems?
4. Recovery
- 读取CURRENT文件找到最新的MANIFEST文件
- 读取MANIFEST文件
- 清除无效文件
- 可以打开所有sstables, 但是如果晚一些会更好
- 把log文件转成新的level-0文件
- 把新的更新操作定向到新的log文件
5. Garbage collection of files
在数据合并压缩和恢复的最后阶段都会调用DeleteObsoleteFiles()
。它会遍历数据库中的所有文件,删除所有不在current log file的所有log file,删除在所有层中都没有引用的所有表文件,以及那些失效的压缩输出文件。