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

出于某种目的,你可能不想把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.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]中可以看到:

  // 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

Leave a Reply

Your email address will not be published. Required fields are marked *