作者: 韩晨旭 10225101440 李畅 10225102463
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.

389 lines
17 KiB

1 month ago
1 month ago
Release 1.18 Changes are: * Update version number to 1.18 * Replace the basic fprintf call with a call to fwrite in order to work around the apparent compiler optimization/rewrite failure that we are seeing with the new toolchain/iOS SDKs provided with Xcode6 and iOS8. * Fix ALL the header guards. * Createed a README.md with the LevelDB project description. * A new CONTRIBUTING file. * Don't implicitly convert uint64_t to size_t or int. Either preserve it as uint64_t, or explicitly cast. This fixes MSVC warnings about possible value truncation when compiling this code in Chromium. * Added a DumpFile() library function that encapsulates the guts of the "leveldbutil dump" command. This will allow clients to dump data to their log files instead of stdout. It will also allow clients to supply their own environment. * leveldb: Remove unused function 'ConsumeChar'. * leveldbutil: Remove unused member variables from WriteBatchItemPrinter. * OpenBSD, NetBSD and DragonflyBSD have _LITTLE_ENDIAN, so define PLATFORM_IS_LITTLE_ENDIAN like on FreeBSD. This fixes: * issue #143 * issue #198 * issue #249 * Switch from <cstdatomic> to <atomic>. The former never made it into the standard and doesn't exist in modern gcc versions at all. The later contains everything that leveldb was using from the former. This problem was noticed when porting to Portable Native Client where no memory barrier is defined. The fact that <cstdatomic> is missing normally goes unnoticed since memory barriers are defined for most architectures. * Make Hash() treat its input as unsigned. Before this change LevelDB files from platforms with different signedness of char were not compatible. This change fixes: issue #243 * Verify checksums of index/meta/filter blocks when paranoid_checks set. * Invoke all tools for iOS with xcrun. (This was causing problems with the new XCode 5.1.1 image on pulse.) * include <sys/stat.h> only once, and fix the following linter warning: "Found C system header after C++ system header" * When encountering a corrupted table file, return Status::Corruption instead of Status::InvalidArgument. * Support cygwin as build platform, patch is from https://code.google.com/p/leveldb/issues/detail?id=188 * Fix typo, merge patch from https://code.google.com/p/leveldb/issues/detail?id=159 * Fix typos and comments, and address the following two issues: * issue #166 * issue #241 * Add missing db synchronize after "fillseq" in the benchmark. * Removed unused variable in SeekRandom: value (issue #201)
10 years ago
1 month ago
1 month ago
Release 1.18 Changes are: * Update version number to 1.18 * Replace the basic fprintf call with a call to fwrite in order to work around the apparent compiler optimization/rewrite failure that we are seeing with the new toolchain/iOS SDKs provided with Xcode6 and iOS8. * Fix ALL the header guards. * Createed a README.md with the LevelDB project description. * A new CONTRIBUTING file. * Don't implicitly convert uint64_t to size_t or int. Either preserve it as uint64_t, or explicitly cast. This fixes MSVC warnings about possible value truncation when compiling this code in Chromium. * Added a DumpFile() library function that encapsulates the guts of the "leveldbutil dump" command. This will allow clients to dump data to their log files instead of stdout. It will also allow clients to supply their own environment. * leveldb: Remove unused function 'ConsumeChar'. * leveldbutil: Remove unused member variables from WriteBatchItemPrinter. * OpenBSD, NetBSD and DragonflyBSD have _LITTLE_ENDIAN, so define PLATFORM_IS_LITTLE_ENDIAN like on FreeBSD. This fixes: * issue #143 * issue #198 * issue #249 * Switch from <cstdatomic> to <atomic>. The former never made it into the standard and doesn't exist in modern gcc versions at all. The later contains everything that leveldb was using from the former. This problem was noticed when porting to Portable Native Client where no memory barrier is defined. The fact that <cstdatomic> is missing normally goes unnoticed since memory barriers are defined for most architectures. * Make Hash() treat its input as unsigned. Before this change LevelDB files from platforms with different signedness of char were not compatible. This change fixes: issue #243 * Verify checksums of index/meta/filter blocks when paranoid_checks set. * Invoke all tools for iOS with xcrun. (This was causing problems with the new XCode 5.1.1 image on pulse.) * include <sys/stat.h> only once, and fix the following linter warning: "Found C system header after C++ system header" * When encountering a corrupted table file, return Status::Corruption instead of Status::InvalidArgument. * Support cygwin as build platform, patch is from https://code.google.com/p/leveldb/issues/detail?id=188 * Fix typo, merge patch from https://code.google.com/p/leveldb/issues/detail?id=159 * Fix typos and comments, and address the following two issues: * issue #166 * issue #241 * Add missing db synchronize after "fillseq" in the benchmark. * Removed unused variable in SeekRandom: value (issue #201)
10 years ago
1 month ago
  1. <center><font size=7>在LevelDB中实现TTL功能</font></center>
  2. <center><font size=4>10225102463 李畅 10225101440 韩晨旭</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. ```c++
  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. ```c++
  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. ```c++
  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. ```c++
  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. (......)
  142. } else if (ddl <= std::time(nullptr)) { // 根据ttl判断是否为过期数据
  143. // TTL: data expired
  144. drop = true;
  145. }
  146. last_sequence_for_key = ikey.sequence;
  147. }
  148. (......)
  149. input->Next();
  150. }
  151. (......)
  152. }
  153. ```
  154. 理论上,`Compaction`相关的代码也实现了,但实际上还会存在一些问题。具体问题和解决方法详见 [问题和解决方案](#3.-问题和解决方案)。
  155. ## 2. 测试用例和测试结果
  156. ### TestTTL.ReadTTL
  157. `TestTTL`中第一个测试样例,检测插入和读取数据时,读取过期数据能够正确返回`NotFound`。
  158. ![ReadTTL](img/test1_succ.png)
  159. ### TestTTL.CompactionTTL
  160. `TestTTL`中第二个测试样例,检测插入和合并数据,能够在触发合并时清除所有过期数据。
  161. ![CompactionTTL](img/test2_succ.png)
  162. 然而,当数据库的设定的`level`过小时,仍然会出现错误。
  163. ![CompactionTTL_fail](img/test2_fail_with_3levels.png)
  164. 具体问题和解决方法见 [问题和解决方案](#3.-问题和解决方案)。
  165. ### TestTTL.LastLevelCompaction
  166. 在解决了上面遇到的问题之后,本小组添加了一个测试样例,用来检测在含义`SSTable`的最大层能否正确检测文件数量和合并、清理过期数据。
  167. ![LastLevelCompaction](img/test3_pass.png)
  168. 最后,展示三个测试样例一起运行的结果:
  169. ![All_pass](img/all_pass.jpg)
  170. ## 3. 问题和解决方案
  171. ### 3.1 无法合并和清理所有数据文件
  172. 本实验的问题和**合并**`Compaction`有关。
  173. 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`层中。
  174. 这样引发的问题是:由`DoCompactionWorks`代码可知,不参与合并的文件无法被获取信息,因而即使过期了也无法被清理。在此例子中,若`level n+1`层中有含有过期数据的`SSTable C`,但`SSTable C`与`SSTable A`发生合并时触及的所有文件都没有`overlap`的话,则在该次合并中并没有被触及,因而无法被清理。
  175. 再次强调,合并过程必须清理**所有**已经过期的数据(尽管这听起来有些让人困惑,因为正常使用时,没有被合并的过期数据即使未被清理也不影响正常使用),而无法被清理的`SSTable C`明显是个例外,是个错误。
  176. 而在LevelDB提供的`CompactRange(nullptr,nullptr)`这个“合并所有数据”的功能中,同样会发生这样的错误,导致有部分数据文件并没有在合并过程中被触及——即使它声称合并了**所有**数据。
  177. 从代码层面可以理解导致该错误的原因。原来的代码:
  178. ```c++
  179. void DBImpl::CompactRange(const Slice* begin, const Slice* end) {
  180. int max_level_with_files = 1;
  181. {
  182. MutexLock l(&mutex_);
  183. Version* base = versions_->current();
  184. for (int level = 1; level < config::kNumLevels; level++) {
  185. if (base->OverlapInLevel(level, begin, end)) {
  186. max_level_with_files = level;
  187. }
  188. }
  189. }
  190. TEST_CompactMemTable(); // TODO(sanjay): Skip if memtable does not overlap
  191. for (int level = 0; level < max_level_with_files; level++) {
  192. TEST_CompactRange(level, begin, end);
  193. }
  194. }
  195. void DBImpl::TEST_CompactRange(int level, const Slice* begin,
  196. const Slice* end) {
  197. assert(level >= 0);
  198. assert(level + 1 < config::kNumLevels);
  199. (......)
  200. }
  201. ```
  202. 这里注意两个数:`config::kNumLevels`和`max_level_with_files`:
  203. + config::kNumLevels:是LevelDB在启动时设定的数,表示总共使用的`level`层数。默认值为7
  204. + max_level_with_files:表示含有`SSTable`文件的最高层的编号。注意,这里的`level`编号是从0开始的,因此`max_level_with_files`理论最大值是`config::kNumLevels - 1`
  205. 在原来的代码中,`DBImpl::CompactRange`函数中的最后一个循环,选择的`level`无法到达`max_level_with_files`,因此即使“合并所有数据”,也没法触及`max_level_with_files`中的所有文件,而是对`max_level_with_files - 1`层的所有文件做了大合并。
  206. 修改后的代码如下:
  207. ```c++
  208. void DBImpl::CompactRange(const Slice* begin, const Slice* end) {
  209. int max_level_with_files = 1;
  210. {
  211. MutexLock l(&mutex_);
  212. Version* base = versions_->current();
  213. for (int level = 1; level < config::kNumLevels; level++) {
  214. if (base->OverlapInLevel(level, begin, end)) {
  215. max_level_with_files = level;
  216. }
  217. }
  218. }
  219. TEST_CompactMemTable(); // TODO(sanjay): Skip if memtable does not overlap
  220. for (int level = 0; level < max_level_with_files + 1; level++) {
  221. TEST_CompactRange(level, begin, end);
  222. }
  223. }
  224. void DBImpl::TEST_CompactRange(int level, const Slice* begin,
  225. const Slice* end) {
  226. assert(level >= 0);
  227. assert(level < config::kNumLevels);
  228. (......)
  229. }
  230. ```
  231. 简单的方法是直接修改了循环的条件和相关函数`TEST_CompactRange`的判断条件,使得`level`可以到达`max_level_with_files`,这样就可以真正地合并所有文件并清理过期数据。
  232. ### 3.2 最后一层文件堆积且无法清理
  233. 在上一个问题中的解决方法在遇到最后一层(`MaxLevel`),即`level == config::kNumLevels - 1`时失效,因为这个时候的`Compaction`无法再找到`level + 1`的文件合并了,因为`level + 1`层根本不存在。
  234. *在测试样例中,由于存入的文件到达不了`MaxLevel`,并不会遇到这种情况,因此可以顺利通过样例。为了制造出现错误的情况,可以将`config::kNumLevels`设置为较小值(如:3)再运行测试。这种情况放在`git`仓库的`light_ver`分支的修改里了。*
  235. 这同时引发了另一个问题的思考:当存储的数据文件(`SSTable`)已经存放在最高层`MaxLevel`中时,无法被合并和清除,因此本小组决定对涉及`MaxLevel`的合并做优化。
  236. 我们选择的优化方式基于以下思考:
  237. + 通常情况下,`MaxLevel`仅在手动触发的大合并即`Manual Compaction`中使用到,而自动合并`Size Compaction`和`Seek Compaction`几乎不会发生在`MaxLevel`,理由是:
  238. + Seek Compaction的情况:由于`MaxLevel`没有“下一层”,当在该层的`seek`查找不到文件时则返回`NotFound`,不可能触发`Seek Compaction`
  239. + Size Compaction的情况:由于数据库的`SSTable`通常按层顺序存储,若`MaxLevel`的存储文件大小达到上限,说明数据库的存储容量已经满了
  240. + 在大合并中,由于针对上个问题的修改方案,当`level`到达`max_level_with_files`时,会和下一层(此时为空)合并,导致合并后的文件会存入下一层,然而这个操作的目的仅仅是清理`max_level_with_files`的过期数据而已,并不需要移动文件,且这样做更早地使用了更高的`level`
  241. 优化方案:修改`Manual Compaction`相关的函数`CompactRange`、`TEST_CompactRange`和`InstallCompactionResults`,以及`Manual Compaction`类、`Compaction`类,向它们引入布尔类型变量`is_last_level`,判断当前层是否是`max_level_with_files`。
  242. 主要修改的函数:
  243. ```c++
  244. void VersionSet::SetupOtherInputs(Compaction* c) {
  245. const int level = c->level();
  246. InternalKey smallest, largest;
  247. AddBoundaryInputs(icmp_, current_->files_[level], &c->inputs_[0]);
  248. GetRange(c->inputs_[0], &smallest, &largest);
  249. // TTL: manual compaction for last level shouldn't have inputs[1]
  250. if (!c->is_last_level()) {
  251. current_->GetOverlappingInputs(level + 1, &smallest, &largest,
  252. &c->inputs_[1]);
  253. AddBoundaryInputs(icmp_, current_->files_[level + 1], &c->inputs_[1]);
  254. }
  255. (......)
  256. if (!c->inputs_[1].empty()) {
  257. (......)
  258. }
  259. (......)
  260. }
  261. ```
  262. 若当前合并的`level`是`max_level_with_files`,则`Compaction* c`不需要`inputs_[1]`,相当于和一个空的`level`进行合并。
  263. ```c++
  264. Status DBImpl::InstallCompactionResults(CompactionState* compact) {
  265. (......)
  266. for (size_t i = 0; i < compact->outputs.size(); i++) {
  267. const CompactionState::Output& out = compact->outputs[i];
  268. if (!compact->compaction->is_last_level()) {
  269. compact->compaction->edit()->AddFile(level + 1, out.number, out.file_size, out.smallest, out.largest);
  270. } else {
  271. // TTL: outputs of last level compaction should be writen to last level itself
  272. compact->compaction->edit()->AddFile(level, out.number, out.file_size, out.smallest, out.largest);
  273. }
  274. }
  275. return versions_->LogAndApply(compact->compaction->edit(), &mutex_);
  276. }
  277. ```
  278. 添加判断条件,若当前层是最高层,则合并后的文件仍然放在本层。
  279. 修改后,可以同时解决提到的两个问题,成功清理最高层的过期数据文件。