作者: 韩晨旭 10225101440 李畅 10225102463
Non puoi selezionare più di 25 argomenti Gli argomenti devono iniziare con una lettera o un numero, possono includere trattini ('-') e possono essere lunghi fino a 35 caratteri.

307 righe
13 KiB

3 settimane fa
  1. <center><font size=7>在LevelDB中实现TTL功能</font></center>
  2. <center><font size=4>10225102463 李畅 10225101___ 韩晨旭</font></center>
  3. # 实验要求
  4. + 在LevelDB中实现键值对的`TTL(Time-To-Live)`功能,使得过期的数据在**读取**时自动失效,并在适当的时候被**合并**清理。
  5. + 修改LevelDB的源码,实现对`TTL`的支持,包括数据的写入、读取和过期数据的清理。
  6. + 编写测试用例,验证`TTL`功能的正确性和稳定性。*(Optional)*
  7. # 1. 设计思路和实现过程
  8. ## 1.1 设计思路
  9. ### Phase 0
  10. 在LevelDB中实现`TTL`功能主要涉及数据的**读、写、合并**。
  11. 在**写入**数据时,`TTL`功能是个可选项。从代码层面来说,LevelDB数据的写入调用了`Put`函数接口:
  12. ```
  13. // 假设增加一个新的Put接口,包含TTL参数, 单位(秒)
  14. Status DB::Put(const WriteOptions& opt, const Slice& key,
  15. const Slice& value, uint64_t ttl);
  16. // 如果调用的是原本的Put接口,那么就不会失效
  17. Status DB::Put(const WriteOptions& opt, const Slice& key,
  18. const Slice& value);
  19. ```
  20. 这段代码中,如果存入`TTL`参数,则调用本实验中新实现的`Put`函数接口;否则直接调用原有的`Put`接口。
  21. ### Phase 1
  22. 本小组的思路很简明:
  23. + 直接将`TTL`信息在**写入**阶段添加到原有的数据结构中
  24. + 在**读取**和**合并**时从得到的数据中 **解读** 出其存储的`TTL`信息,判断是否过期
  25. 因此,接下来需要思考的是如何将`TTL`信息巧妙地存入LevelDB的数据结构中。
  26. ### Phase 2
  27. 由于插入数据时调用的`Put`接口只有三个参数,其中`opt`是写入时的系统配置,实际插入的数据只有`key/value`,因此存储`ttl`信息时,最简单的方法就是存入`key`或`value`中。
  28. 在这样的方法中,可以调用原有的`Put`函数,将含义`ttl`信息的`key/value`数据存入数据库。
  29. 经过讨论后,本小组选择将`ttl`信息存入`value`中。
  30. 这样做的优缺点是:
  31. + **优点**:由于LevelDB在合并数据的过程中,需要根据`SSTable`的`key`对数据进行有序化处理,将`ttl`信息存储在`value`中不会影响`key`的信息,因此对已有的合并过程不产生影响
  32. + **缺点**:在获取到`SSTable`的`key`时,无法直接判断该文件是否因为`ttl`而成为过期数据,仍然需要读取对应的`value`才能判断,多了一步读取开销。***但是***,实际上在读取数据以及合并数据的过程中,其代码实际上都读取了对应`SSTable`中存储的`value`信息,因此获取`ttl`信息时必要的读取`value`过程并不是多余的,实际***几乎不***造成额外的读取开销影响
  33. ### Phase 3
  34. 在确定插入数据时选用的方法后,读取和合并的操作只需要在获取数据文件的`value`后解读其包含的`ttl`信息,并判断是否过期就可以了。
  35. ## 1.2 实现过程
  36. ### 1.2.1 写入 Put
  37. 首先需要实现的是将`ttl`信息存入`value`的方法。`Put`中获取的`ttl`参数是该数据文件的**生存时间(单位:秒)**。本小组对此进行处理方法是通过`ttl`计算过期时间的**时间戳**,转码为字符串类型后存入`value`的最前部,并用`|`符号与原来的值分隔开。
  38. 对于不使用`ttl`的数据文件,存入`0`作为`ttl`,在读取数据时若读到`0`则表示不使用`TTL`功能。
  39. 将此功能封装为**编码**和**解码**文件过期时间(DeadLine)的函数,存储在`/util/coding.h`文件中:
  40. ```
  41. inline std::string EncodeDeadLine(uint64_t ddl, const Slice& value) { // 存储ttl信息
  42. return std::to_string(ddl) + "|" + value.ToString();
  43. }
  44. inline void DecodeDeadLineValue(std::string* value, uint64_t& ddl) { // 解读ttl信息
  45. auto separator = value->find_first_of("|");
  46. std::string ddl_str = value->substr(0, separator);
  47. ddl = std::atoll(ddl_str.c_str());
  48. *value = value->substr(separator + 1);
  49. }
  50. ```
  51. 在写入数据调用`Put`接口时,分别启用`EncodeDeadLine`函数存储`ttl`信息:
  52. ```
  53. // Default implementations of convenience methods that subclasses of DB
  54. // can call if they wish
  55. // TTL: Update TTL Encode
  56. Status DB::Put(const WriteOptions& opt, const Slice& key, const Slice& value) { // 不使用TTL
  57. WriteBatch batch;
  58. batch.Put(key, EncodeDeadLine(0, value));
  59. return Write(opt, &batch);
  60. }
  61. // TTL: Put methods for ttl
  62. Status DB::Put(const WriteOptions& options, const Slice& key,
  63. const Slice& value, uint64_t ttl) { // 使用TTL
  64. WriteBatch batch;
  65. auto dead_line = std::time(nullptr) + ttl; // 计算过期时间的时间戳
  66. batch.Put(key, EncodeDeadLine(dead_line, value));
  67. return Write(options, &batch);
  68. }
  69. ```
  70. ### 1.2.2 读取 Get
  71. LevelDB在读取数据时,调用`Get`接口,获取文件的`key/value`,因此只需要在`Get`函数中加入使用`TTL`功能的相关代码:
  72. ```
  73. Status DBImpl::Get(const ReadOptions& options, const Slice& key,
  74. std::string* value) {
  75. (......)
  76. // Unlock while reading from files and memtables
  77. {
  78. mutex_.Unlock();
  79. // First look in the memtable, then in the immutable memtable (if any).
  80. LookupKey lkey(key, snapshot);
  81. if (mem->Get(lkey, value, &s)) {
  82. // Done
  83. } else if (imm != nullptr && imm->Get(lkey, value, &s)) {
  84. // Done
  85. } else {
  86. s = current->Get(options, lkey, value, &stats);
  87. have_stat_update = true;
  88. }
  89. // TTL: Get the true value and make sure the data is still living
  90. if(!value->empty()) {
  91. uint64_t dead_line;
  92. DecodeDeadLineValue(value, dead_line);
  93. if (dead_line != 0) {
  94. // use TTL
  95. if (std::time(nullptr) >= dead_line) {
  96. // data expired
  97. *value = "";
  98. s = Status::NotFound("Data expired");
  99. }
  100. } else {
  101. // TTL not set
  102. }
  103. }
  104. mutex_.Lock();
  105. }
  106. (......)
  107. }
  108. ```
  109. 若使用了`TTL`功能,则当文件过期时,返回`NotFound("Data expired")`的信息,即“数据已清除”。
  110. **注意**:由于LevelDB对于数据的读取是只读`ReadOnly`的,因此只能返回`NotFound`的信息,而无法真正清理过期数据。
  111. ### 1.2.3 合并 Compaction
  112. 在合并的过程中,需要做到清理掉过期的数据,释放空间。
  113. 在大合并的过程中,需要调用`DoCompactionWorks`函数实现合并的操作,也是在这个过程中,LevelDB得以真正完成清理旧版本数据、已删除数据并释放空间的过程。
  114. 其实现逻辑是在该函数的过程中引入一个布尔变量`drop`,对于需要清理的数据设置`drop`为`True`,而需要保留的数据则是`drop`为`False`,最后根据`drop`的值清理过期数据,并将需要保留的数据合并写入新的`SSTable`。
  115. 因此,我们只需要在判断`drop`为`True`的条件中加入对过期时间(DeadLine)的判断就可以实现`TTL`功能的清理过期数据了:
  116. ```
  117. Status DBImpl::DoCompactionWork(CompactionState* compact) {
  118. (......)
  119. while (input->Valid() && !shutting_down_.load(std::memory_order_acquire)) {
  120. (......)
  121. // Handle key/value, add to state, etc.
  122. bool drop = false;
  123. if (!ParseInternalKey(key, &ikey)) {
  124. // Do not hide error keys
  125. current_user_key.clear();
  126. has_current_user_key = false;
  127. last_sequence_for_key = kMaxSequenceNumber;
  128. } else {
  129. if (!has_current_user_key ||
  130. user_comparator()->Compare(ikey.user_key, Slice(current_user_key)) !=
  131. 0) {
  132. // First occurrence of this user key
  133. current_user_key.assign(ikey.user_key.data(), ikey.user_key.size());
  134. has_current_user_key = true;
  135. last_sequence_for_key = kMaxSequenceNumber;
  136. }
  137. std::string value = input->value().ToString();
  138. uint64_t ddl;
  139. DecodeDeadLineValue(&value, ddl);
  140. if (last_sequence_for_key <= compact->smallest_snapshot) {
  141. // Hidden by an newer entry for same user key
  142. drop = true; // (A)
  143. } else if (ikey.type == kTypeDeletion &&
  144. ikey.sequence <= compact->smallest_snapshot &&
  145. compact->compaction->IsBaseLevelForKey(ikey.user_key)) {
  146. // For this user key:
  147. // (1) there is no data in higher levels
  148. // (2) data in lower levels will have larger sequence numbers
  149. // (3) data in layers that are being compacted here and have
  150. // smaller sequence numbers will be dropped in the next
  151. // few iterations of this loop (by rule (A) above).
  152. // Therefore this deletion marker is obsolete and can be dropped.
  153. drop = true;
  154. } else if (ddl <= std::time(nullptr)) { // 根据ttl判断是否为过期数据
  155. // TTL: data expired
  156. drop = true;
  157. }
  158. last_sequence_for_key = ikey.sequence;
  159. }
  160. (......)
  161. input->Next();
  162. }
  163. (......)
  164. }
  165. ```
  166. 理论上,`Compaction`相关的代码也实现了,但实际上还会存在一些问题。具体问题和解决方法详见 [问题和解决方案](#3.-问题和解决方案)。
  167. ## 2. 测试用例和测试结果
  168. ## 3. 问题和解决方案
  169. 本实验的问题和**合并**`Compaction`有关。
  170. LevelDB中`Compaction`的逻辑是选中特定层(`level`)合并,假设为`level n`。在`level n`中找目标文件`SSTable A`(假设该次触发的合并从文件A开始),并确定`level n`以及`level n+1`中与`SSTable A`包含的数据(`key/value`)有`key`发生重复(`overlap`)的所有文件,合并后产生新的`SSTable B`放入`level n+1`层中。
  171. 这样引发的问题是:由`DoCompactionWorks`代码可知,不参与合并的文件无法被获取信息,因而即使过期了也无法被清理。在此例子中,若`level n+1`层中有含有过期数据的`SSTable C`,但`SSTable C`与`SSTable A`发生合并时触及的所有文件都没有`overlap`的话,则在该次合并中并没有被触及,因而无法被清理。
  172. 再次强调,合并过程必须清理**所有**已经过期的数据(尽管这听起来有些让人困惑,因为正常使用时,没有被合并的过期数据即使未被清理也不影响正常使用),而无法被清理的`SSTable C`明显是个例外,是个错误。
  173. 而在LevelDB提供的`CompactRange(nullptr,nullptr)`这个“合并所有数据”的功能中,同样会发生这样的错误,导致有部分数据文件并没有在合并过程中被触及——即使它声称合并了**所有**数据。
  174. 从代码层面可以理解导致该错误的原因。原来的代码:
  175. ```
  176. void DBImpl::CompactRange(const Slice* begin, const Slice* end) {
  177. int max_level_with_files = 1;
  178. {
  179. MutexLock l(&mutex_);
  180. Version* base = versions_->current();
  181. for (int level = 1; level < config::kNumLevels; level++) {
  182. if (base->OverlapInLevel(level, begin, end)) {
  183. max_level_with_files = level;
  184. }
  185. }
  186. }
  187. TEST_CompactMemTable(); // TODO(sanjay): Skip if memtable does not overlap
  188. for (int level = 0; level < max_level_with_files; level++) {
  189. TEST_CompactRange(level, begin, end);
  190. }
  191. }
  192. void DBImpl::TEST_CompactRange(int level, const Slice* begin,
  193. const Slice* end) {
  194. assert(level >= 0);
  195. assert(level + 1 < config::kNumLevels);
  196. (......)
  197. }
  198. ```
  199. 这里注意两个数:`config::kNumLevels`和`max_level_with_files`:
  200. + config::kNumLevels:是LevelDB在启动时设定的数,表示总共使用的`level`层数。默认值为7
  201. + max_level_with_files:表示含有`SSTable`文件的最高层的编号。注意,这里的`level`编号是从0开始的,因此`max_level_with_files`理论最大值是`config::kNumLevels - 1`
  202. 在原来的代码中,`DBImpl::CompactRange`函数中的最后一个循环,选择的`level`无法到达`max_level_with_files`,因此即使“合并所有数据”,也没法触及`max_level_with_files`中的所有文件,而是对`max_level_with_files - 1`层的所有文件做了大合并。
  203. 修改后的代码如下:
  204. ```
  205. void DBImpl::CompactRange(const Slice* begin, const Slice* end) {
  206. int max_level_with_files = 1;
  207. {
  208. MutexLock l(&mutex_);
  209. Version* base = versions_->current();
  210. for (int level = 1; level < config::kNumLevels; level++) {
  211. if (base->OverlapInLevel(level, begin, end)) {
  212. max_level_with_files = level;
  213. }
  214. }
  215. }
  216. TEST_CompactMemTable(); // TODO(sanjay): Skip if memtable does not overlap
  217. for (int level = 0; level < max_level_with_files + 1; level++) {
  218. TEST_CompactRange(level, begin, end);
  219. }
  220. }
  221. void DBImpl::TEST_CompactRange(int level, const Slice* begin,
  222. const Slice* end) {
  223. assert(level >= 0);
  224. assert(level < config::kNumLevels);
  225. (......)
  226. }
  227. ```
  228. 简单直接地改了循环的条件和相关函数`TEST_CompactRange`的判断条件,使得`level`可以到达`max_level_with_files`,这样就可以真正地合并所有文件并清理过期数据。