出于某种目的,你可能不想把levelDB的所有的文件都存到一个目录下:
我们希望的:
level 0 -+
level 1 +--> DIR1 (DISK1)
level 2 -+
------------------------------
level 3 -+
level 4 |
... +--> DIR2 (DISK2)
level N -+
但是 levelDB 不支持类似的选项,只能将文件存到一个目录;而且不幸的是,levelDB由于频繁的compaction操作,带来了频繁的文件创建和删除,且每个level包含多个文件,不易这样改。
我想到的方法有两个:(1) 用基于levelDB改进的RocksDB,它支持多路径。(2)修改levelDB源代码。为了更简单的实现我们想要的分level存到不同目录的功能,我们从levelDB存储的最底层“创建文件”步骤,利用软链接进行修改。
本文将首先介绍levelDB的目录结构,然后分析给出介绍这两种文件更改路径方法。
1. 目录结构
创建一个DB后,和这个DB相关的所有数据都会放在一个文件夹的多个文件中。这些文件包括xxx.ldb
、xxx.log
、LOG
、MANIFEST-xxx
、LOCK
、CURRENT
等。这些解释在[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]中可以看到:
// A list of paths where SST files can be put into, with its target size.
// Newer data is placed into paths specified earlier in the vector while
// older data gradually moves to paths specified later in the vector.
//
// For example, you have a flash device with 10GB allocated for the DB,
// as well as a hard drive of 2TB, you should config it to be:
// [{"/flash_path", 10GB}, {"/hard_drive", 2TB}]
//
// The system will try to guarantee data under each path is close to but
// not larger than the target size. But current and future file sizes used
// by determining where to place a file are based on best-effort estimation,
// which means there is a chance that the actual size under the directory
// is slightly more than target size under some workloads. User should give
// some buffer room for those cases.
//
// If none of the paths has sufficient room to place a file, the file will
// be placed to the last path anyway, despite to the target size.
//
// Placing newer data to earlier paths is also best-efforts. User should
// expect user files to be placed in higher levels in some extreme cases.
//
// If left empty, only one path will be used, which is db_name passed when
// opening the DB.
// Default: empty
std::vector<DbPath> db_paths;
也就是,可以通过配置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()
函数创建一个文件。代码如下:
// step1. (db/db_impl.cc)
Status DBImpl::DoCompactionWork(CompactionState* compact) {
...
status = OpenCompactionOutputFile(compact);
...
}
// step2. (db/db_impl.cc)
Status DBImpl::OpenCompactionOutputFile(CompactionState* compact) {
...
std::string fname = TableFileName(dbname_, file_number); //分配文件名
s = env_->NewWritableFile(fname, &compact->outfile); // 创建文件的封装好的文件操作接口
...
}
// step3. (util/env_posix.cc)
Virtual Status NewWritableFile(const std::string& fname, WritableFile** result) {
...
int fd = open(fname.c_str(), O_WRONLY, 0644);
...
}
Memtable持久化过程中的文件创建: 在BuildTable()
函数(db/builder.cc)中,同样会调用NewWritableFile()
函数,所生成的文件用于level-0数据,即新的从memtable持久化到磁盘的数据。
// step1. (db/builder.cc)
Status BuildTable(const std::string& dbname, Env* env, const Options& options, ...) {
...
s = env->NewWritableFile(fname, &file);
...
}
// step2 与compaction创建文件的step3 一样
...
文件删除过程: 如果不考虑何时会需要进行删除操作,所有删除操作最终都会调用DeleteFile()
函数(util/env_posix.cc),然后DeleteFile()
调用POSIX的unlink()
进行删除。
// step 1. 可能是销毁DB、或者compaction中需要进行删除等
...
// step 2. (util/env_posix.cc)
virtual Status DeleteFile(const std::string& fname) {
...
if (unlink(fname.c_str()) != 0) {
...
}
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
。
// compaction的创建操作的关键修改:(db/db_impl.cc)
Status DBImpl::OpenCompactionOutputFile(CompactionState* compact) {
...
//s = env_->NewWritableFile(fname, &compact->outfile);
std::string level_str = std::to_string(compact->compaction->level());
std::string fname_used_to_create = fname.substr(0,fname.size()-4) + "START" + level_str + "END.ldb";
s = env_->NewWritableFile(fname_used_to_create, &compact->outfile);
...
}
// memtable持久化过程中level-0文件创建操作的关键修改: (db/builder.cc)
Status BuildTable(const std::string& dbname, Env* env, const Options& options, ...) {
...
//s = env->NewWritableFile(fname, &file);
if (!(name_fmt == ldb_fmt)) {
s = env->NewWritableFile(fname, &file);
} else {
std::string fname_used_to_create = fname.substr(0,fname.size()-4) + "START0END.ldb";
s = env->NewWritableFile(fname_used_to_create, &file);
}
...
}
// POSIX封装NewWritableFile函数的关键修改:(util/env_posix.cc)
virtual Status NewWritableFile(const std::string& fname, WritableFile** result) {
...
//fd = open(fname_tmp.c_str(), O_WRONLY, 0644);
if (!(name_fmt == ldb_fmt)) { // if not ldb file, store it in the main directory
fd = open(fname.c_str(), O_TRUNC | O_WRONLY | O_CREAT, 0644);
} else { //.ldb file, redirect by symbolic links !
int split_level = fname.find("START");
int split_level2 = fname.find("END");
std::string level_str = fname.substr(split_level + 5, split_level2);
int level_num = atoi(level_str.c_str());
std::string fname_tmp = fname.substr(0, split_level) + fname.substr(split_level2 + 3, fname.size());
int len = fname_tmp.size();
int split_index = len - 1;
while (split_index >= 0) {
if (fname_tmp[split_index] == '/')
break;
else
split_index--;
}
std::string disk_path;
if (level_num >= 2) { // 即用于第3或者更高层的新文件会被存到DB_PATH/HDD目录
disk_path = "/HDD";
} else { // 0~2层的文件会被存到DB_PATH/SSD目录
disk_path = "/SSD";
}
std::string actual_fname = fname_tmp.substr(0, split_index) + disk_path + fname_tmp.substr(split_index);
// 先创建实际存数据的文件
int tmp_fd = open(actual_fname.c_str(), O_TRUNC | O_WRONLY | O_CREAT, 0644);
close(tmp_fd);
// 再创建软连接直到实际存放数据的文件,这样其它地方的打开、读写等操作就会自动被文件系统重定向到实际的文件。
symlinkat(actual_fname.c_str(), AT_FDCWD, fname_tmp.c_str());
fd = open(fname_tmp.c_str(), O_WRONLY, 0644);
}
...
}
修改删除文件过程: 删除函数DeleteFile()
中,我们只需要把原来的删除改为删除数据实际文件与软链接文件即可,关机修改如下:
// (util/env_posix.cc)
virtual Status DeleteFile(const std::string& fname) {
...
//if (unlink(fname.c_str()) != 0) {
// result = PosixError(fname, errno);
//}
std::string ldb_fmt = "ldb";
std::string name_fmt = fname.substr(fname.size() - 3);
if (!(name_fmt == ldb_fmt)) { // if not ldb file, just delete it from the main directory
if (unlink(fname.c_str()) != 0) {
result = PosixError(fname, errno);
}
} else { //.ldb file, redirect by symbolic links !
char real_name[256];
realpath(fname.c_str(), real_name); // 由软链接获得实际路径
if (unlink(real_name) != 0) { // 删除实际数据文件
result = PosixError(fname, errno);
}
if (unlink(fname.c_str()) != 0) { // 删除DB_PATH下的软链接
result = PosixError(fname, errno);
}
}
...
}
( 具体修改的代码和一些测试结果,可以在我的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