You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
王雪飞 7f8f2c0b93 commit the result of test_ttl to complete the README.md 10 months ago
.github/workflows initialize 删除 10 months ago
benchmarks initialize 删除 10 months ago
cmake initialize 删除 10 months ago
db TestTTL.CompactionTTL passed 删除 10 months ago
doc initialize 删除 10 months ago
helpers/memenv initialize 删除 10 months ago
include/leveldb TestTTL.ReadTTL passed 删除 10 months ago
issues initialize 删除 10 months ago
picture commit the result of test_ttl to complete the README.md 删除 10 months ago
port initialize 删除 10 months ago
table initialize 删除 10 months ago
test TestTTL.CompactionTTL passed 删除 10 months ago
third_party initialize 删除 10 months ago
util initialize 删除 10 months ago
.clang-format initialize 10 months ago
.gitignore initialize 10 months ago
.gitmodules initialize 10 months ago
AUTHORS initialize 10 months ago
CMakeLists.txt initialize 10 months ago
CONTRIBUTING.md initialize 10 months ago
LICENSE initialize 10 months ago
NEWS initialize 10 months ago
README.md commit the result of test_ttl to complete the README.md 10 months ago
TODO initialize 10 months ago

README.md

LevelDB TTL 实验报告

小组成员:

王雪飞 10225501435

马也驰 10215501408

1.实验目的

  1. 深入了解LevelDB的内部原理和数据结构。
  2. 掌握TTL(Time To Live,生存时间)功能的设计与实现方法。
  3. 学习如何在开源项目中添加新功能,提升代码阅读和修改能力。

2.实验要求

  1. 在LevelDB中实现键值对的TTL功能,使得过期的数据在读取时自动失效,并在适当的时候被合并清理。
  2. 修改LevelDB的源码,实现对TTL的支持,包括数据的写入、读取和过期数据的清理。
  3. 编写测试用例,验证TTL功能的正确性和稳定性。

3.实验内容

3.1. TTL功能介绍

TTL(Time To Live),即生存时间,是指数据在存储系统中的有效期。设置TTL可以使得过期的数据自动失效,减少存储空间占用,提高系统性能。

为什么需要TTL功能:

  1. 数据自动过期:无需手动删除过期数据,简化数据管理。
  2. 节省存储空间:定期清理无效数据,优化资源利用。
  3. 提高性能:减少无效数据的干扰,提升读写效率。

3.2. 设计方案

在LevelDB中添加TTL功能的方案:

  1. 数据编码方式修改:在键或值中增加过期时间的信息。
  2. 读取时判断过期:在Get操作时,检查数据是否过期,过期则返回NotFound。
  3. Compaction清理:在数据压缩过程中,删除过期的数据。

3.3. 实现步骤

3.3.1. 修改数据结构

在 Put 操作中,将 TTL 与当前时间相加获得 DDL,DDL 为数据失效的时间,将 DDL 与值一起存储,存储格式为<TTL_value>

Status DB::Put(const WriteOptions& opt, const Slice& key,
                   const Slice& value, uint64_t ttl) {
  WriteBatch batch;
  auto now = std::chrono::system_clock::now();
  auto timestamp = std::chrono::duration_cast<std::chrono::microseconds>(now.time_since_epoch()).count();
  auto microsecondsTimestamp = static_cast<uint64_t>(timestamp) + ttl*1000000;
  std::string value_ttl = value.ToString();
  value_ttl += "_" + std::to_string(microsecondsTimestamp);
  Slice new_value(value_ttl.c_str(), value_ttl.size());
  batch.Put(key, new_value);
  return Write(opt, &batch);
}

3.3.2 修改读取流程

在 Get 操作中,获得 value 之后,找到第一个下划线,下划线前面的数据为 DDL,然后用当前时间与 DDL 作比较,判断数据是否过期,过期则返回NotFound。

size_t pos = value->find_last_of('_');
if (pos != std::string::npos) {
std::string substring = value->substr(pos + 1);
auto ddl = static_cast<uint64_t>(std::stoll(substring));
auto now = std::chrono::system_clock::now();
auto timestamp = std::chrono::duration_cast<std::chrono::microseconds>(now.time_since_epoch()).count();
auto microsecondsTimestamp = static_cast<uint64_t>(timestamp);
if (ddl <= microsecondsTimestamp) {
    value->clear();
    Slice msg1("value not found!");
    Slice msg2("value has expired!");
    s = leveldb::Status::NotFound(msg1, msg2);
} else {
    value->resize(pos);
    }
}

3.3.3 修改Compaction流程

在CompactRange函数中选中的最后一层,也就是代码中的max_level_with_files选中进行合并, 确保合并过程选中所有应该被覆盖度文件。

void DBImpl::CompactRange(const Slice* begin, const Slice* end) {
  int max_level_with_files = 1;
  {
    MutexLock l(&mutex_);
    Version* base = versions_->current();
    for (int level = 1; level < config::kNumLevels; level++) {
      if (base->OverlapInLevel(level, begin, end)) {
        max_level_with_files = level;
      }
    }
  }
  TEST_CompactMemTable();  // TODO(sanjay): Skip if memtable does not overlap
  for (int level = 0; level < max_level_with_files; level++) {
    TEST_CompactRange(level, begin, end);
  }
  TEST_CompactRange(max_level_with_files, begin, end);
}

在BackgroundCompaction函数中禁止直接跨层移动文件,确保所有的文件都能通过DoCompactionWork 函数被合并。

} else if (!is_manual && c->IsTrivialMove()) {
//    // Move file to next level
//    assert(c->num_input_files(0) == 1);
//    FileMetaData* f = c->input(0, 0);
//    c->edit()->RemoveFile(c->level(), f->number);
//    c->edit()->AddFile(c->level() + 1, f->number, f->file_size, f->smallest,
//                       f->largest);
//    status = versions_->LogAndApply(c->edit(), &mutex_);
//    if (!status.ok()) {
//      RecordBackgroundError(status);
//    }
//    VersionSet::LevelSummaryStorage tmp;
//    Log(options_.info_log, "Moved #%lld to level-%d %lld bytes %s: %s\n",
//        static_cast<unsigned long long>(f->number), c->level() + 1,
//        static_cast<unsigned long long>(f->file_size),
//        status.ToString().c_str(), versions_->LevelSummary(&tmp));
  }

在DoCompactionWork函数中判断当前遍历的key所对应的value是否过期:如果已经过期, 就将该kv对应的drop标志设置为true,确保在合并是该kv被丢弃。

      Slice value_ddl = input->value();
      std::string value = value_ddl.ToString();
      size_t pos = value.find_last_of('_');
      if (pos != std::string::npos) {
        std::string substring = value.substr(pos + 1);
        auto ddl = static_cast<uint64_t>(std::stoll(substring));
        auto now = std::chrono::system_clock::now();
        auto timestamp = std::chrono::duration_cast<std::chrono::microseconds>(now.time_since_epoch()).count();
        auto microsecondsTimestamp = static_cast<uint64_t>(timestamp);
        if (ddl <= microsecondsTimestamp) {
          drop = true;
        }
      }

4. 实验结果

下面是我们跑仓库提供的test_ttl的结果:

picture

可以看到,两个test都能通过。

实验中遇到的问题

1. TTL存储的位置以及存储方式

我们最初的想法是把TTL跟value存储在一起,形式为<TTL value>,这样Put操作会很简单,仅仅把两个字符串拼接起来即可,但这样的话,在Get操作中时,无法判断从何处分割TTL和value,所以我们决定在TTL和value之间添加一个标志符,存放形式改为<TTL_value>,这样,在Get操作时,只需先找到第一个下划线,下划线前面的为TTL,后面的为value,这样就能把TTL和value区分开来。但还有一个问题,判断条件为:写入数据的时间+ TTL < 读取数据的时间 ,如果仅存放TTL,虽然在调用get时我们可以获得读取数据的时间,并通过解码value获得TTL,但我们没有办法获得写入数据的时间,所以只能通过在Put操作时,把写入数据的时间也写入value中,这样在Get时,就能获得写入数据的时间,从而判断是否过期。所以,我们又把value的形式改为<TTL_写入时间_value>,这样,通过两个下划线把TTL、写入时间和value区分开来,就能实现在get操作时判断是否过期。但我们又想到,既然在get操作解码得到TTL和写入时间之后要加在一块,并且TTL和写入时间都是在get操作时与value进行编码,那么我们为什么不在get操作时就把TTL和写入时间加在一起,再与value编码呢,把写入时间+TTL记为DDL,这样就可以把value编码为<DDL_value>,在get操作时,只需解码得到DDL,然后拿当前时间跟DDL作比较,即可知道数据是否过期。

综上所述,我们的编码格式经过多次迭代:<TTL value>-><TTL_value>-><TTL_写入时间_value>-><DDL_value>,最终得到一个比较满意的编码方式。

2. Compaction流程

一开始我们只在DoCompactionWork函数中添加了一个解码value得到ddl,并于当前时间进行比较判断数据是否过期的过程,但发现测试无法通过。 后来参考助教的提示,我们原本打算修改测试用例,即修改写入数据量的大小,但是没有成功。之后我们决定修改Compact过程。具体来说,我们在CompactRange,BackgroundCompaction中进行修改, 确保每一次Compaction都能合并所有的文件,并且禁止leveldb直接跨层移动文件,最后通过了测试。

3. test_ttl编译以及数据库打开问题

test_ttl一开始无法通过编译,经过检查发现是重复定义了ranges和sizes两个数组。我们将两个数组进行了重命名解决了这个问题。同时,我们还遇到了第二个 测试数据库无法打开的问题,我们通过修改数据库名称的方法解决了这个问题。