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.

146 line
8.3 KiB

3 週之前
3 週之前
3 週之前
3 週之前
  1. # LevelDB TTL 实验报告
  2. 小组成员:
  3. 王雪飞 10225501435
  4. 马也驰 10215501408
  5. ## 1.实验目的
  6. 1. 深入了解LevelDB的内部原理和数据结构。
  7. 2. 掌握TTL(Time To Live,生存时间)功能的设计与实现方法。
  8. 3. 学习如何在开源项目中添加新功能,提升代码阅读和修改能力。
  9. ## 2.实验要求
  10. 1. 在LevelDB中实现键值对的TTL功能,使得过期的数据在读取时自动失效,并在适当的时候被合并清理。
  11. 2. 修改LevelDB的源码,实现对TTL的支持,包括数据的写入、读取和过期数据的清理。
  12. 3. 编写测试用例,验证TTL功能的正确性和稳定性。
  13. ## 3.实验内容
  14. ### 3.1. TTL功能介绍
  15. TTL(Time To Live),即生存时间,是指数据在存储系统中的有效期。设置TTL可以使得过期的数据自动失效,减少存储空间占用,提高系统性能。
  16. 为什么需要TTL功能:
  17. 1. 数据自动过期:无需手动删除过期数据,简化数据管理。
  18. 2. 节省存储空间:定期清理无效数据,优化资源利用。
  19. 3. 提高性能:减少无效数据的干扰,提升读写效率。
  20. ### 3.2. 设计方案
  21. 在LevelDB中添加TTL功能的方案:
  22. 1. 数据编码方式修改:在键或值中增加过期时间的信息。
  23. 2. 读取时判断过期:在Get操作时,检查数据是否过期,过期则返回NotFound。
  24. 3. Compaction清理:在数据压缩过程中,删除过期的数据。
  25. ## 3.3. 实现步骤
  26. #### 3.3.1. 修改数据结构
  27. 在 Put 操作中,将 TTL 与当前时间相加获得 DDL,DDL 为数据失效的时间,将 DDL 与值一起存储,存储格式为`<TTL_value>`。
  28. ```
  29. Status DB::Put(const WriteOptions& opt, const Slice& key,
  30. const Slice& value, uint64_t ttl) {
  31. WriteBatch batch;
  32. auto now = std::chrono::system_clock::now();
  33. auto timestamp = std::chrono::duration_cast<std::chrono::microseconds>(now.time_since_epoch()).count();
  34. auto microsecondsTimestamp = static_cast<uint64_t>(timestamp) + ttl*1000000;
  35. std::string value_ttl = value.ToString();
  36. value_ttl += "_" + std::to_string(microsecondsTimestamp);
  37. Slice new_value(value_ttl.c_str(), value_ttl.size());
  38. batch.Put(key, new_value);
  39. return Write(opt, &batch);
  40. }
  41. ```
  42. ### 3.3.2 修改读取流程
  43. 在 Get 操作中,获得 value 之后,找到第一个下划线,下划线前面的数据为 DDL,然后用当前时间与 DDL 作比较,判断数据是否过期,过期则返回NotFound。
  44. ```
  45. size_t pos = value->find_last_of('_');
  46. if (pos != std::string::npos) {
  47. std::string substring = value->substr(pos + 1);
  48. auto ddl = static_cast<uint64_t>(std::stoll(substring));
  49. auto now = std::chrono::system_clock::now();
  50. auto timestamp = std::chrono::duration_cast<std::chrono::microseconds>(now.time_since_epoch()).count();
  51. auto microsecondsTimestamp = static_cast<uint64_t>(timestamp);
  52. if (ddl <= microsecondsTimestamp) {
  53. value->clear();
  54. Slice msg1("value not found!");
  55. Slice msg2("value has expired!");
  56. s = leveldb::Status::NotFound(msg1, msg2);
  57. } else {
  58. value->resize(pos);
  59. }
  60. }
  61. ```
  62. ### 3.3.3 修改Compaction流程
  63. 在CompactRange函数中选中的最后一层,也就是代码中的max_level_with_files选中进行合并,
  64. 确保合并过程选中所有应该被覆盖度文件。
  65. ```
  66. void DBImpl::CompactRange(const Slice* begin, const Slice* end) {
  67. int max_level_with_files = 1;
  68. {
  69. MutexLock l(&mutex_);
  70. Version* base = versions_->current();
  71. for (int level = 1; level < config::kNumLevels; level++) {
  72. if (base->OverlapInLevel(level, begin, end)) {
  73. max_level_with_files = level;
  74. }
  75. }
  76. }
  77. TEST_CompactMemTable(); // TODO(sanjay): Skip if memtable does not overlap
  78. for (int level = 0; level < max_level_with_files; level++) {
  79. TEST_CompactRange(level, begin, end);
  80. }
  81. TEST_CompactRange(max_level_with_files, begin, end);
  82. }
  83. ```
  84. 在BackgroundCompaction函数中禁止直接跨层移动文件,确保所有的文件都能通过DoCompactionWork
  85. 函数被合并。
  86. ```
  87. } else if (!is_manual && c->IsTrivialMove()) {
  88. // // Move file to next level
  89. // assert(c->num_input_files(0) == 1);
  90. // FileMetaData* f = c->input(0, 0);
  91. // c->edit()->RemoveFile(c->level(), f->number);
  92. // c->edit()->AddFile(c->level() + 1, f->number, f->file_size, f->smallest,
  93. // f->largest);
  94. // status = versions_->LogAndApply(c->edit(), &mutex_);
  95. // if (!status.ok()) {
  96. // RecordBackgroundError(status);
  97. // }
  98. // VersionSet::LevelSummaryStorage tmp;
  99. // Log(options_.info_log, "Moved #%lld to level-%d %lld bytes %s: %s\n",
  100. // static_cast<unsigned long long>(f->number), c->level() + 1,
  101. // static_cast<unsigned long long>(f->file_size),
  102. // status.ToString().c_str(), versions_->LevelSummary(&tmp));
  103. }
  104. ```
  105. 在DoCompactionWork函数中判断当前遍历的key所对应的value是否过期:如果已经过期,
  106. 就将该kv对应的drop标志设置为true,确保在合并是该kv被丢弃。
  107. ```
  108. Slice value_ddl = input->value();
  109. std::string value = value_ddl.ToString();
  110. size_t pos = value.find_last_of('_');
  111. if (pos != std::string::npos) {
  112. std::string substring = value.substr(pos + 1);
  113. auto ddl = static_cast<uint64_t>(std::stoll(substring));
  114. auto now = std::chrono::system_clock::now();
  115. auto timestamp = std::chrono::duration_cast<std::chrono::microseconds>(now.time_since_epoch()).count();
  116. auto microsecondsTimestamp = static_cast<uint64_t>(timestamp);
  117. if (ddl <= microsecondsTimestamp) {
  118. drop = true;
  119. }
  120. }
  121. ```
  122. ## 4. 实验结果
  123. 下面是我们跑仓库提供的test_ttl的结果:
  124. ![picture](picture/leveldb_ttl实验结果.png)
  125. 可以看到,两个test都能通过。
  126. ## 实验中遇到的问题
  127. ### 1. TTL存储的位置以及存储方式
  128. 我们最初的想法是把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作比较,即可知道数据是否过期。
  129. 综上所述,我们的编码格式经过多次迭代:`<TTL value>`->`<TTL_value>`->`<TTL_写入时间_value>`->`<DDL_value>`,最终得到一个比较满意的编码方式。
  130. ### 2. Compaction流程
  131. 一开始我们只在DoCompactionWork函数中添加了一个解码value得到ddl,并于当前时间进行比较判断数据是否过期的过程,但发现测试无法通过。
  132. 后来参考助教的提示,我们原本打算修改测试用例,即修改写入数据量的大小,但是没有成功。之后我们决定修改Compact过程。具体来说,我们在CompactRange,BackgroundCompaction中进行修改,
  133. 确保每一次Compaction都能合并所有的文件,并且禁止leveldb直接跨层移动文件,最后通过了测试。
  134. ### 3. test_ttl编译以及数据库打开问题
  135. test_ttl一开始无法通过编译,经过检查发现是重复定义了ranges和sizes两个数组。我们将两个数组进行了重命名解决了这个问题。同时,我们还遇到了第二个
  136. 测试数据库无法打开的问题,我们通过修改数据库名称的方法解决了这个问题。