LevelDB中数据文件的分目录存储

出于某种目的,你可能不想把levelDB的所有的文件都存到一个目录下:

但是 levelDB 不支持类似的选项,只能将文件存到一个目录;而且不幸的是,levelDB由于频繁的compaction操作,带来了频繁的文件创建和删除,且每个level包含多个文件,不易这样改。

我想到的方法有两个:(1) 用基于levelDB改进的RocksDB,它支持多路径。(2)修改levelDB源代码。为了更简单的实现我们想要的分level存到不同目录的功能,我们从levelDB存储的最底层“创建文件”步骤,利用软链接进行修改。

本文将首先介绍levelDB的目录结构,然后分析给出介绍这两种文件更改路径方法。

1. 目录结构

创建一个DB后,和这个DB相关的所有数据都会放在一个文件夹的多个文件中。这些文件包括xxx.ldbxxx.logLOGMANIFEST-xxxLOCKCURRENT等。这些解释在[1]中说的都很清楚,我可以用中文再解释一遍。

日志文件 xxx.log
xxx.log文件包括是最近存储的数据序列,所以是大小是乱序的,当xxx.log文件达到一定的大小(默认4MB),就会被转换成Sorted tables有序表文件。这个日志文件对应内存中当前的memtable,当这个memtable满了后,会被写到level-0,对应的xxx.log文件会被删除,新的xxx.log会被生成,对应于新的memtable。

有序表文件(SSTable) xxx.ldb
SST是Sorted Strings Table的缩写,levelDB中,对应的文件格式是ldb。xxx.ldb文件中的KV记录是按key的大小排好序的。随着数据量增加,ldb文件有很多。但是,它们文件名前缀数字、文件大小都与所属的level无联系(文件名即文件大小都不包含level语义),所以我们无法从文件名和文件大小判断出某个文件中数据所处的level。不过,一个文件只可能属于一个level。

除level-0外,各个level的文件总大小是预先设定的,level-1 10MB,level-2 100MB, level-3 1000MB……;而level-0较特殊,其由文件个数限制,默认达到4个level-0 ldb文件就会merge到level-1中;而且,level-0中的数据可能有重叠存在。

清单文件 MANIFEST
我们从xxx.ldb文件名和大小无法判断其所属level,那么就要有一个额外的文件存储这些“元信息”。MANIFEST-xxx文件就负责存储哪个文件属于哪个level,每次打开这个DB,都会新建一个清单文件并标一个特殊的后缀作为标记,其中的内容也是以log-structured形式追加存储的。

当前文件 CURRENT
如果MANIFEST-xxx是各个xxx.ldb文件的元数据文件,那么CURRENT文件就是MANIFEST-xxx文件的元数据文件,它只存了一个编号,这个编号就是某个MANIFEST文件的后缀,用来指明当前所用的是哪个MANIFEST文件。

信息日志 LOG
LOG文件是给人看的日志,也就是DB运行过程中打印出的各种日志信息。

锁文件 LOCK
顾名思义,一个用文件实现的锁,一个DB同时只能被一个进程上锁。

2. 方法1: 改用RocksDB

在RocksDB的源码include/RocksDB/options.h[2]中可以看到:

也就是,可以通过配置db_paths选项,来将数据存到不同的路径,并可以指定各个目录的配额大小。但是注释中只说了系统会尽力将更新的数据放到前边的目录中,没有做保证,如果想详细了解机制,可能还要再深入代码中。

3. 方法2:修改levelDB的源码

3.1 文件创建和删除流程

这样修改是基于一些观察的,首先,levelDB有如下机制:(1) 新文件的创建只发生在compaction过程中(level-x –> level-x+1)或memtable持久化到磁盘(memtable –> level-0)时发生;(2)旧文件的删除只发生在compaction过程中或DB销毁时发生;(3) 文件的管理实现了统一的接口封装,比如创建一个文件的接口有NewWritableFile()等。具体步骤如下:

Compaction过程中的文件创建: DoCompactionWork()函数(db/db_impl.cc)在需要创建新文件的函数会调用OpenCompactionOutputFile()函数(db/db_impl.cc)。OpenCompactionOutputFile()这个函数会给新文件分配一个名字,然后调用NewWritableFile()函数(util/env_posix.cc)。NewWritableFile()会直接最终调用open()函数创建一个文件。代码如下:

Memtable持久化过程中的文件创建:BuildTable()函数(db/builder.cc)中,同样会调用NewWritableFile()函数,所生成的文件用于level-0数据,即新的从memtable持久化到磁盘的数据。

文件删除过程: 如果不考虑何时会需要进行删除操作,所有删除操作最终都会调用DeleteFile()函数(util/env_posix.cc),然后DeleteFile()调用POSIX的unlink()进行删除。

3.2 思路和实现

基于以上levelDB的具体实现,我们可以用软链接(符号链接)的方法来进行修改,在原目录下只存ldb文件的链接,然后将实际文件创建在其他目录,这样就基本避免了了在POSIX文件操作这层(util/env_posix.cc)之外的过多修改。

修改创建文件过程: 在最后的step 3中NewWritableFile()创建文件时,我们要将文件区分到不同的level,还需要一个额外信息 — 文件所属的level。根据前面将文件创建分为两种:(1)在compaction需要创建文件时,step 2中调用NewWritableFile()OpenCompactionOutputFile()函数的CompactionState* compact结构包含有level信息,即compact->compaction->level(),它代表要被compact的level,那么新创建文件的level就应该是compact->compaction->level() + 1。(2)在memtable到level0的过程中需要创建文件时,新创建的文件一定是level-0文件。

我们只需将这些level信息从OpenCompactionOutputFile()BuildTable()函数传递给NewWritableFile()即可,我实现的方法是将level数想办法嵌入到ldb格式文件的fname这个string参数中传到NewWritableFile()中,参数修改方法为xxx.ldb --> xxxSTART[level_num]END.ldb

修改删除文件过程: 删除函数DeleteFile()中,我们只需要把原来的删除改为删除数据实际文件与软链接文件即可,关机修改如下:

( 具体修改的代码和一些测试结果,可以在我的github找到:https://github.com/zhangjaycee/levelDB-multipath )

4. 结论

至此,我们可以将一个DB下不同level的文件分别存到不同目录,只要我们将不同存储设备挂到这些不同的目录下,我们也就实现了将不同level的文件分别存到不同盘下。我只用db_bench测试了修改源码版的levelDB,这种修改下SSD+HDD混合的性能在只用SSD和只用HDD之间,符合预期。


[1] https://github.com/google/leveldb/blob/master/doc/impl.md

[2] https://github.com/facebook/rocksdb/blob/master/include/rocksdb/options.h

发表评论

电子邮件地址不会被公开。 必填项已用*标注