Browse Source

update doc

xxy
xxy 9 months ago
parent
commit
3714a14e2e
22 changed files with 743 additions and 47 deletions
  1. +2
    -2
      CMakeLists.txt
  2. +3
    -3
      benchmarks/db_bench.cc
  3. +13
    -16
      db/db_impl.cc
  4. +0
    -2
      db/db_impl.h
  5. +2
    -3
      db/write_batch.cc
  6. +122
    -21
      util/coding.cc
  7. +2
    -0
      util/coding.h
  8. BIN
      设计文档.assets/ValueLog-17325302026895.png
  9. BIN
      设计文档.assets/ValueLog-17325304026617.png
  10. BIN
      设计文档.assets/ValueLog-17325310409799.png
  11. BIN
      设计文档.assets/ValueLog.png
  12. BIN
      设计文档.assets/d85456f5f58abb55d9e83f3c020f0b7.png
  13. BIN
      设计文档.assets/image-20241125122706607.png
  14. BIN
      设计文档.assets/image-20241208162802629.png
  15. BIN
      设计文档.assets/image-20241208165823491.png
  16. BIN
      设计文档.assets/image-20241208233143593.png
  17. BIN
      设计文档.assets/image-20241209012054587.png
  18. BIN
      设计文档.assets/image-20241209012231561.png
  19. BIN
      设计文档.assets/image-20241209014113365.png
  20. BIN
      设计文档.assets/image-20241209020150442.png
  21. BIN
      设计文档.assets/leveldb_values.png
  22. +599
    -0
      设计文档.md

+ 2
- 2
CMakeLists.txt View File

@ -10,7 +10,7 @@ project(leveldb VERSION 1.23.0 LANGUAGES C CXX)
if(NOT CMAKE_C_STANDARD)
# This project can use C11, but will gracefully decay down to C89.
# 17
set(CMAKE_C_STANDARD 17)
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED OFF)
set(CMAKE_C_EXTENSIONS OFF)
endif(NOT CMAKE_C_STANDARD)
@ -18,7 +18,7 @@ endif(NOT CMAKE_C_STANDARD)
# C++ standard can be overridden when this is used as a sub-project.
if(NOT CMAKE_CXX_STANDARD)
# This project requires C++17.
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
endif(NOT CMAKE_CXX_STANDARD)

+ 3
- 3
benchmarks/db_bench.cc View File

@ -74,7 +74,7 @@ static int FLAGS_reads = -1;
static int FLAGS_threads = 1;
// Size of each value
static int FLAGS_value_size = 1000;
static int FLAGS_value_size = 5000;
// Arrange to generate values that shrink to this fraction of
// their original size after compression
@ -1127,8 +1127,8 @@ int main(int argc, char** argv) {
// Choose a location for the test database if none given with --db=<path>
if (FLAGS_db == nullptr) {
leveldb::g_env->GetTestDirectory(&default_db_path);
default_db_path += "/dbbench";
//leveldb::g_env->GetTestDirectory(&default_db_path);
default_db_path = "dbbench";
FLAGS_db = default_db_path.c_str();
}

+ 13
- 16
db/db_impl.cc View File

@ -18,7 +18,6 @@
#include <atomic>
#include <cstdint>
#include <cstdio>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <set>
@ -39,7 +38,6 @@
#include "util/coding.h"
#include "util/logging.h"
#include "util/mutexlock.h"
namespace fs = std::filesystem;
namespace leveldb {
@ -1645,12 +1643,6 @@ std::vector> DBImpl::WriteValueLog(
return res;
}
void DBImpl::writeValueLogForCompaction(WritableFile* target_file,
std::vector<Slice> values) {
for (int i = 0; i < values.size(); i++) {
target_file->Append(values[i]);
}
}
void DBImpl::addNewValueLog() {
valuelogfile_number_ = versions_->NewFileNumber();
@ -1740,17 +1732,16 @@ void DBImpl::GarbageCollect() {
gc_mutex_.AssertHeld();
// 遍历数据库目录,找到所有 valuelog 文件
Log(options_.info_log, "start gc ");
auto files_set = fs::directory_iterator(dbname_);
std::vector<std::string> filenames;
Status s = env_->GetChildren(dbname_, &filenames);
assert(s.ok());
std::set<std::string> valuelog_set;
for (const auto& cur_log_file : files_set) {
if (fs::exists(cur_log_file) &&
fs::is_regular_file(fs::status(cur_log_file)) &&
IsValueLogFile(cur_log_file.path().filename().string())) {
valuelog_set.emplace(cur_log_file.path().filename().string());
for (const auto& filename:filenames) {
if (IsValueLogFile(filename)) {
valuelog_set.emplace(filename);
}
}
for (std::string valuelog_name : valuelog_set) {
// std::cout << valuelog_name << std::endl;
uint64_t cur_log_number = GetValueLogID(valuelog_name);
valuelog_name = ValueLogFileName(dbname_, cur_log_number);
if (cur_log_number == valuelogfile_number_) {
@ -1883,6 +1874,10 @@ void DBImpl::GarbageCollect() {
// Key 不存在,忽略此记录
continue;
}
else if(stored_value.data()[0]==(char)(0x00)){
//value is too small
continue;
}
if (!status.ok()) {
std::cerr << "Error accessing sstable: " << status.ToString()
@ -1919,7 +1914,9 @@ void DBImpl::GarbageCollect() {
// 清理旧文件(如果需要)
cur_valuelog.close();
fs::remove(valuelog_name.c_str()); // 删除旧的 ValueLog 文件
env_->RemoveFile(valuelog_name);
Log(options_.info_log, "remove file during gc %s", valuelog_name.c_str());
}
}

+ 0
- 2
db/db_impl.h View File

@ -68,8 +68,6 @@ class DBImpl : public DB {
// WriteValueLog(std::vector<Slice> value)override;
std::vector<std::pair<uint64_t, uint64_t>> WriteValueLog(
std::vector<std::pair<Slice, Slice>> value) override;
void writeValueLogForCompaction(WritableFile* target_file,
std::vector<Slice> value);
void addNewValueLog() override EXCLUSIVE_LOCKS_REQUIRED(mutex_);
;
std::pair<WritableFile*, uint64_t> getNewValuelog(); // use for compaction

+ 2
- 3
db/write_batch.cc View File

@ -140,7 +140,7 @@ class ValueLogInserter : public WriteBatch::Handler {
Slice new_value;
std::string buf;
if(value.size()<100){
buf+=(char)(0x00);
buf+=(char)(0x00);// should set in key
buf.append(value.data(),value.size());
}
else{
@ -149,13 +149,12 @@ class ValueLogInserter : public WriteBatch::Handler {
kv.push_back({key,value});
auto res=db_->WriteValueLog(kv);
PutVarint64(&buf,res[0].first);
// PutVarint64(&buf,res[0].second.first);
// PutVarint64(&buf,res[0].second.second);
PutVarint64(&buf,res[0].second);
}
new_value=Slice(buf);
writeBatch_.Put(key,new_value);
}
void Delete(const Slice& key) override {
writeBatch_.Delete(key);
}

+ 122
- 21
util/coding.cc View File

@ -3,9 +3,12 @@
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#include "util/coding.h"
#include <filesystem>
#include <sstream>
using namespace leveldb;
using Field=std::pair<Slice,Slice>;
using FieldArray=std::vector<std::pair<Slice, Slice>>;
namespace leveldb {
@ -156,7 +159,13 @@ bool GetLengthPrefixedSlice(Slice* input, Slice* result) {
}
}
// 判断文件是否为 valuelog 文件
/**
* @brief valuelog
*
* @param filename
* @return true valuelog
* @return false valuelog
*/
bool IsValueLogFile(const std::string& filename) {
// 检查文件是否以 ".valuelog" 结尾
const std::string suffix = ".valuelog";
@ -164,7 +173,13 @@ bool IsValueLogFile(const std::string& filename) {
filename.substr(filename.size() - suffix.size()) == suffix;
}
// 示例:解析 sstable 中的元信息
/**
* @brief valuelog_id offset
*
* @param stored_value "valuelog_id|offset"
* @param valuelog_id ValueLog ID
* @param offset
*/
void ParseStoredValue(const std::string& stored_value, uint64_t& valuelog_id,
uint64_t& offset) {
// 假设 stored_value 格式为:valuelog_id|offset
@ -173,28 +188,43 @@ void ParseStoredValue(const std::string& stored_value, uint64_t& valuelog_id,
GetVarint64(&tmp, &offset);
}
// 示例:获取 ValueLog 文件 ID
/**
* @brief ValueLog ID
*
* @param valuelog_name "123.valuelog"
* @return uint64_t ValueLog ID
*/
uint64_t GetValueLogID(const std::string& valuelog_name) {
// 使用 std::filesystem::path 解析文件名
std::filesystem::path file_path(valuelog_name);
std::string filename = file_path.filename().string(); // 获取文件名部分
// 查找文件名中的 '.' 位置,提取数字部分
auto pos = filename.find('.');
if (pos == std::string::npos) {
assert(0);
}
// 提取数字部分
std::string id_str = filename.substr(0, pos);
// 检查提取的部分是否为有效数字
for (char c : id_str) {
if (!isdigit(c)) {
assert(0);
// 获取文件名部分(假设文件名格式为 "number.extension")
size_t pos = valuelog_name.find_last_of('/');
std::string filename;
if (pos != std::string::npos) {
filename = valuelog_name.substr(pos + 1);
} else {
filename = valuelog_name;
}
}
return std::stoull(id_str); // 转换为 uint64_t
// 查找文件名中的 '.' 位置,提取数字部分
pos = filename.find('.');
assert(pos != std::string::npos);
// 提取数字部分
std::string id_str = filename.substr(0, pos);
// 检查文件扩展名是否为 .valuelog
if (filename.substr(pos + 1) != "valuelog") {
assert(0);
}
// 转换为 uint64_t
uint64_t id;
std::istringstream iss(id_str);
if (!(iss >> id)) {
assert(0);
}
return id;
}
// Helper function to split the set of files into chunks
@ -208,4 +238,75 @@ void SplitIntoChunks(const std::set& files, int num_workers,
}
}
bool CompareFieldArray(const FieldArray &a, const FieldArray &b) {
if (a.size() != b.size()) return false;
for (size_t i = 0; i < a.size(); ++i) {
if (a[i].first != b[i].first || a[i].second != b[i].second) return false;
}
return true;
}
bool CompareKey(const std::vector<std::string> a, std::vector<std::string> b) {
if (a.size() != b.size()){
return false;
}
for (size_t i = 0; i < a.size(); ++i) {
if (a[i] != b[i]){
return false;
}
}
return true;
}
std::string SerializeValue(const FieldArray& fields){
std::string res_="";
PutVarint64(&res_,(uint64_t)fields.size());
for(auto pr:fields){
PutLengthPrefixedSlice(&res_, pr.first);
PutLengthPrefixedSlice(&res_, pr.second);
}
return res_;
}
void DeserializeValue(const std::string& value_str,FieldArray* res){
Slice slice=Slice(value_str.c_str());
uint64_t siz;
bool tmpres=GetVarint64(&slice,&siz);
assert(tmpres);
res->clear();
for(int i=0;i<siz;i++){
Slice value_name;
Slice value;
tmpres=GetLengthPrefixedSlice(&slice,&value_name);
assert(tmpres);
tmpres=GetLengthPrefixedSlice(&slice,&value);
assert(tmpres);
res->emplace_back(value_name,value);
}
}
Status Get_keys_by_field(DB *db,const ReadOptions& options, const Field field,std::vector<std::string> *keys){
auto it=db->NewIterator(options);
it->SeekToFirst();
keys->clear();
while(it->Valid()){
auto val=it->value();
FieldArray arr;
auto str_val=std::string(val.data(),val.size());
DeserializeValue(str_val,&arr);
for(auto pr:arr){
if(pr.first==field.first&&pr.second==field.second){
Slice key=it->key();
keys->push_back(std::string(key.data(),key.size()));
break;
}
}
it->Next();
}
delete it;
return Status::OK();
}
} // namespace leveldb

+ 2
- 0
util/coding.h View File

@ -18,6 +18,8 @@
#include "leveldb/slice.h"
#include "port/port.h"
#include "leveldb/db.h"
namespace leveldb {

BIN
设计文档.assets/ValueLog-17325302026895.png View File

Before After
Width: 672  |  Height: 322  |  Size: 15 KiB

BIN
设计文档.assets/ValueLog-17325304026617.png View File

Before After
Width: 672  |  Height: 362  |  Size: 20 KiB

BIN
设计文档.assets/ValueLog-17325310409799.png View File

Before After
Width: 752  |  Height: 362  |  Size: 24 KiB

BIN
设计文档.assets/ValueLog.png View File

Before After
Width: 592  |  Height: 182  |  Size: 9.5 KiB

BIN
设计文档.assets/d85456f5f58abb55d9e83f3c020f0b7.png View File

Before After
Width: 752  |  Height: 362  |  Size: 22 KiB

BIN
设计文档.assets/image-20241125122706607.png View File

Before After
Width: 683  |  Height: 148  |  Size: 39 KiB

BIN
设计文档.assets/image-20241208162802629.png View File

Before After
Width: 1110  |  Height: 731  |  Size: 101 KiB

BIN
设计文档.assets/image-20241208165823491.png View File

Before After
Width: 888  |  Height: 729  |  Size: 98 KiB

BIN
设计文档.assets/image-20241208233143593.png View File

Before After
Width: 835  |  Height: 783  |  Size: 104 KiB

BIN
设计文档.assets/image-20241209012054587.png View File

Before After
Width: 1034  |  Height: 786  |  Size: 104 KiB

BIN
设计文档.assets/image-20241209012231561.png View File

Before After
Width: 826  |  Height: 784  |  Size: 103 KiB

BIN
设计文档.assets/image-20241209014113365.png View File

Before After
Width: 998  |  Height: 783  |  Size: 107 KiB

BIN
设计文档.assets/image-20241209020150442.png View File

Before After
Width: 833  |  Height: 790  |  Size: 104 KiB

BIN
设计文档.assets/leveldb_values.png View File

Before After
Width: 667  |  Height: 222  |  Size: 12 KiB

+ 599
- 0
设计文档.md View File

@ -0,0 +1,599 @@
# 代码设计
## 1.项目概述
### 1.1 实现字段查询功能
LevelDB 的基本数据结构是由一个 key 和对应的 value 组成,其中 value 是一个简单的字节序列(可以是字符串或二进制数据)。默认情况下,LevelDB 不支持像关系型数据库那样的字段查询功能。然而,在实际应用中,用户可能需要对存储的数据进行更加精细的操作,特别是当值包含多个逻辑字段时,直接使用现有的 LevelDB 接口难以满足需求。
在本实验中,我们的目标是扩展 LevelDB 的功能,使其 value 支持多字段结构,并实现通过字段值查询对应的 key 的功能。
### 1.2 KV 分离
在 LevelDB 及其采用的 LSM 树结构中,性能挑战之一在于 Compaction 操作的效率。Compaction 是指将内存中的数据合并到磁盘上的过程,此过程中涉及大量的读写操作,对于系统的整体性能有着重要影响。在 Compaction 时,所有涉及到的旧 sstable 中的键值对都将被写入到新 sstable 中,而 Value 通常比 Key 大得多。如果将 Key 和 Value 分离存储,合并时只涉及 key 写入 sstable 的过程,可以显著减少 Compaction 的开销,从而提升性能。
基于此我们计划实施键值分离策略。具体而言,键将保持原有的排序方式,而值将被独立存储。这样做可以在不影响查询性能的前提下,大幅降低 Compaction 过程中的数据迁移量,进而减少不必要的磁盘 I/O ,提升系统的合并效率。
## 2.功能设计
Andy Pavlo在15445课程中说,完成一个项目,应先写出能够完成正确性要求的代码,再在此基础上提升性能,避免不成熟的优化方式。
因此,我们的项目流程将保持每周推进代码进度,在完成目标要求的代码的基础上,不断迭代优化性能。
![image-20241125122706607](设计文档.assets/image-20241125122706607.png)
### 2.1.字段设计
- **设计目标**
- 将 LevelDB 中的 `value` 组织成字段数组,每个数组元素对应一个字段(字段名:字段值)。
- 字段会被序列化为字符串,然后插入LevelDB。
- 这些字段可以通过解析字符串得到,字段名与字段值都是字符串类型。
- 允许任意调整字段。
- 实现通过字段值查询对应的 `key`
- **实现思路**
函数 `Put_with_fields` 负责插入含字段的数据。原字段数据经过序列化函数 `SerializeValue` 处理后,函数 `Put_with_fields` 调用 `Put` 将序列化后的字段插入 leveldb。
函数 `Get_with_fields` 负责获得含字段的数据。使用 `Get` 从 leveldb 中获取 `key` 和序列化后的 `value`,调用 `ParseValue` 可以将字段反序列化。
函数 `Get_keys_by_field` 遍历数据库中的所有键值对,解析每个 `Value`,提取字段数组 `FieldArray`。检查字段数组中是否存在目标字段,如果匹配,则记录其对应的 `Key`。将所有匹配 `key` 汇总到 `keys` 中返回。
**初步实现**(第一周 已完成):在 leveldb 内部实现以上功能。内部实现会导致读取时无法区分多字段类型和原生 kv 对,扩展性不足。
**后续改进**(第二周):为了解决无法区分多字段类型和原生 kv 对的问题,将以上函数功能实现在用户层级,使 leveldb 内部对多字段类型无感知。
### 2.2.KV分离
#### 设计思路
- **KV 分离设计**
a. 将LevelDB的key-value存储结构进行扩展,分离存储key和value
b. Key存储在一个LevelDB实例中,LSM-tree中的value为一个指向Value log文件和偏移地址的指针,用户Value存储在Value log中。
- **读取操作。**
KV分离后依然支持点查询与范围查询操作。
- **Value log的管理。**
a.当Value log超过一定大小后通过后台GC操作释放Value log中的无效数据。
b.GC能把旧Value log中没有失效的数据写入新的Value log,并更新LSM-tree里的键值对。
c.新旧Value log的管理功能。
- **确保操作的原子性**
#### 实现思路
#### 初步实现(第一周 已完成)
使用单一Value Log简单的实现KV分离,该实现较为简单,仅需在Put/Get函数内部进行简单修改,但在大数据量场景下性能极差。
##### 优点:实现简单,合并时开销小
##### 缺点:大数据量下性能极差,不能作为最终方案。
#### 第二种实现(第二周 已完成)
对每个SSTable和MemTable建立一个Value Log。该实现相比于初步实现更加复杂,需要在合并时查询所有相关Value Log,并建立新Value Log。此外还要考虑在合并结束后将废弃的Value Log异步删除。
**Trick 1.为什么要在Put到MemTable时就放入Value Log而非dump至SS Table时才放入Value Log?**
原因:将写ValueLog推迟至SSTable并没有减少Put时写入磁盘的总数据量(写ValueLog:ValueLog中写Value,WAl中写Key和Value元数据;不写ValueLog:WAL中写Key和Value),优点是将两次无法并行的写文件操作变为一次写文件操作。但该方法有一个缺陷,即leveldb原生的管理数据的方式是MemTable和SSTable大小相等。而经过这样改变后,MemTable在dump成SSTable后其大小会突然减少(Value全部转移至ValueLog),导致一个SSTable中存储的数据量过少。而原本valuelog的优势(一个SSTable可以放更多键值对使得table cache命中率变高)也将不存在了。我们将两个做法都进行了实现,通过对比性能发现后者不如前者,因此选择保留前者设计。
##### 优点:随合并自动GC,无需考虑GC。
##### 缺点:合并时开销未能减小。
**第三种实现**(第三周):使用相对固定大小的Value Log,例如每个Value Log大小约为2KB。新添加的键值对依次将值计入最新Value Log,当Value Log大小满了之后就创建新Value Log。需要设计一种不改变SSTable内记录Value元数据的GC方法。
##### 优点:合并时开销小。
##### 缺点:需要设计一种GC方式,能够在异步GC的同时不改变SSTable。
## 3. 数据结构设计
### KV分离后 Value 结构设计
一个Value,开头是使用Varint64存储的FieldNum,表示有FieldNum个Field组成。然后是使用Varint64存储的Field X name size,表示该field的字段名长度,然后是字段名,然后是使用Varint64存储的Field X Value size,表示该field的值长度,然后是值。
![leveldb_values](设计文档.assets/leveldb_values.png)
### ValueLog结构设计
**第一版设计**
使用一个Value Log文件的设计中,我们只需记录Value在Value Log中对应的偏移量和Value长度即可。
Value Log中只记录Value值,无需记录元信息。
![ValueLog](设计文档.assets/ValueLog-17325302026895.png)
**第二版设计**
Value设计为:1字节标志位+Varint64文件ID+Varint64偏移量+Varint64长度。
在存储时根据Value大小是否较大选择进行KV分离。若分离则标志位为true,否则标志位为false。
日志文件中仍然只需记录Value值即可,无需记录元信息。
![ValueLog](设计文档.assets/ValueLog-17325304026617.png)
**第三版设计**
![ValueLog](设计文档.assets/ValueLog-17325310409799.png)
这一版设计有些复杂。
和第二版设计一样,在Value开头使用一字节标志位表示是否KV分离。
如果KV分离,则接下来是Varint64的文件ID和Varint64的文件内offset。
在ValueLog中,在开头记录当前会索引到该Value Log的键值对数量Using count。如果Using count==0,则表示该ValueLog不被任何键值对使用,可以删除。
**Using count在Value Log添加键值对时进行+1**。
**Using count**在**其中任意键值对被合并**,**并且 该键值对由于合并时被更加新的键值对覆盖 或者 该键值对的True using Sign=False**时,进行**-1**。
在一个Value通过SSTable索引到Value Log后,其索引到的开头是一个**Value True Using Sign**。该标志位同样是一字节,标志了当前该Value是否是真正的Value。
**若标志位为True表示是真正的Value,那么标志位后是Varint64的Value长度+Value本身。**
**若标志位为False表示不是真正的Value,那么标志位后是Varint64的下一个可能存在有真实对应Value的Value Log文件ID和Varint64的在下一个Value Log文件中的offset。**
**Value True Using Sign发生变化有两种情况:**
**1**
一个键值对由于合并时被更加新的键值对覆盖时,不仅将Using count进行-1,同时也将其Value True Using Sign设置为False。
**2**
在键值对加入到Value Log时,其Value True Using Sign设置为True。
当后台异步GC过程检测到一个Value Log的Using count较小时,将对其中Value True Using Sign仍为True的Value做以下处理:
1.将Value对应的数据(设为True的标志位,Value len和Value本身)像新数据一样写入最新的Value Log中。
2.将原Value True Using Sign置为False
3.将原Value Log中标志位后的数据修改为新写入的Value Log的ID和数据所处的Offset。
(保证原Value大小大于16,以防Varint64(Value len)+Value len<Varint64(File ID)+Varint64(Offset))
**注意**
在SSTable合并时要检查所合并的Value直接指向的Value Log是否Value True Using Sign=True,不是的话,要不断通过Next File找下一个文件,直到找到Value True Using Sign=True的文件。然后将新SSTable中的Value指向该新文件。**除此之外,在此过程中找到的所有文件的Using count都要-1**。
这样的GC设计可以确保,GC过程中无需修改原SSTable数据,且合并过程中仅需修改较少的数据。
> [!CAUTION]
>
> **!注意!在实现过程中发现性能缺陷**
当合并时由于需要扫描合并的SSTable,要对其中每个Value做读ValueLog操作(因为可能可以更新Value指向的ValueLog),导致一次合并会涉及很多次的ValueLog文件读写,性能过于低效,因此想到了新的操作方法。
### 3.1 fixsize_valuelog实际设计
![d85456f5f58abb55d9e83f3c020f0b7](设计文档.assets/d85456f5f58abb55d9e83f3c020f0b7.png)
#### 新的`valuelog` 文件的组织方式:
`valuelog` 文件存储了 **键值对(KV)** 数据,每条记录按照以下格式组织:
1. **键的长度**(`key_len`):`uint64_t`,标识键的字节长度。
2. **键**(`key`):实际的键数据,长度为 `key_len`
3. **值的长度**(`value_len`):`uint64_t`,标识值的字节长度。
4. **值**(`value`):实际的值数据,长度为 `value_len`
在sstable中key对应的value位置存储了对应valuelog文件的id和在文件中的offset。
#### gc过程:
垃圾回收的核心思想是扫描所有的 `valuelog` 文件,检查文件中的记录是否有效。如果记录的键已失效(比如键在 `sstable` 中不存在或元数据不匹配),则该记录会被忽略,最终删除整个无效的 `valuelog` 文件。
#### **详细过程:**
1. **扫描数据库目录**
- 遍历 `valuelog` 文件。
2. **处理每个 `valuelog` 文件**
- 打开文件,逐条读取记录。
3. **读取每条记录**
- 按文件结构读取 `key_len`、`key`、`value_len`、`value`。
- 检查 sstable是否包含该键:
- 如果键不存在(或无效),忽略此条记录。
- 如果键存在,验证元数据(包括 `valuelog_id``offset`)。
- 有效的键值对会被重新 put 进入数据库,sstable中重复的key会在compaction过程中被回收。
4. **清理无效文件**
- 如果整个 `valuelog` 文件的记录均无效或已被迁移,删除该文件。
## 4. 接口/函数设计
#### 4. 1Value多字段设计
##### **4.1.1 数据序列化与反序列化**
**序列化字段数组为字符串值**
```cpp
std::string SerializeValue(const FieldArray& fields);
```
- **输入**:字段数组 `fields`
- **输出**:序列化后的字符串。
**反序列化字符串值为字段数组**
```c++
void DeserializeValue(const std::string& value_str, FieldArray* res);
```
- **输入**:序列化字符串 `value_str`
- **输出**:字段数组 `res`
##### **4.1.2 数据查询接口**
**按字段查找键**
```c++
Status DB::Get_keys_by_field(const ReadOptions& options, const Field field, std::vector<std::string>* keys);
```
- **输入**
- 读取选项 `options`
- 字段值 `field`
- **输出**
- 操作状态 `Status`
- 符合条件的键列表 `keys`
##### **4.1.3 判断文件是否为 `valuelog` 文件**
**判断给定文件是否为 `.valuelog` 格式的文件。**
```c++
bool IsValueLogFile(const std::string& filename) {
```
**输入**:
- 文件名 `filename`,可以是完整路径或纯文件名。
**输出**:
- 布尔值 `true`:文件是 `valuelog` 文件。
- 布尔值 `false`:文件不是 `valuelog` 文件。
##### **4.1.4 解析 `sstable` 中的元信息**
**解析 `sstable` 中存储的值,提取 `valuelog_id``offset` 信息。**
```c++
void ParseStoredValue(const std::string& stored_value, uint64_t& valuelog_id, uint64_t& offset);
```
- **输入**
- 存储值 `stored_value`,格式为 `"valuelog_id|offset"`
- **输出**
- `valuelog_id`:解析出的 ValueLog 文件 ID。
- `offset`:解析出的记录偏移量。
##### **4.1.5 获取 `ValueLog` 文件 ID**
**从文件名中提取 ValueLog 文件的 ID(假设文件名格式为 `number.valuelog`)。**
```
uint64_t GetValueLogID(const std::string& valuelog_name);
```
- **输入**
- 文件名 `valuelog_name`,可以是完整路径或仅文件名,格式需符合 `number.valuelog`
- **输出**
- `uint64_t` 类型,返回提取的文件 ID。
#### 4.2 Value Log设计
##### **4.2.1 WriteValueLog**
将一堆键值对的值顺序写入Value Log,用于writebatch写入数据库,以及Value Log GC的时候。两者都会对多个键值对同时操作,因此设计为批处理。
函数内将使用写锁保证正确性。同一时间最多只有一个WriteValueLog可以进行。
```cpp
std::vector<std::pair<uint64_t,uint64_t>> WriteValueLog(std::vector<const slice&> value);
```
- **输入**:一个Slice vector,表示要写入Value Log的Value们。
- **输出**:一个std::pair<uint64_t,uint64_t> vector,每个pair中:第一个uint64_t是Value Log文件ID,第二个uint64_t是处在Value Log中的偏移量。
> [!NOTE]
>
> 在第三版设计中,valuelog中会存储key,所以有部分改动。
##### **4.2.2 ReadValueLog**
通过Value Log读取目标键值对的值。
函数内将使用读锁保证正确性。在一个ValueLog正在被读取时,GC和WriteValueLog(?)无法对该ValueLog操作。
```cpp
Status ReadValueLog(uint64_t file_id, uint64_t offset,Slice* value);
```
- **输入**:第一个uint64_t是Value Log文件ID,第二个uint64_t是处在Value Log中的偏移量,第三个是指向要传回的value的指针。
- **输出**:一个Status,表示是否成功传回对应Value。
> [!NOTE]
>
> 在第三版设计中,valuelog中会存储key,所以有部分改动。
##### **4.2.3 测试GC**
调用`MaybeScheduleGarbageCollect()`来安排一个后台线程执行垃圾回收任务。它会等待所有已安排的垃圾回收任务完成,这通过循环检查`background_garbage_collect_scheduled_`标志,并在该标志为真时等待`background_gc_finished_signal_`信号来实现。
```cpp
void DBImpl::TEST_GarbageCollect()
```
##### **4.2.4 调用线程进行GC**
启动一个新的后台线程执行`BGWorkGC`方法。这里使用了`gc_mutex_.Lock()`来确保线程安全。
```cpp
void DBImpl::MaybeScheduleGarbageCollect()
```
##### **4.2.5 调用负责GC函数**
调用`BackgroundGarbageCollect()`进行实际的垃圾回收工作。
```cpp
void DBImpl::BGWorkGC(void* db)
```
##### **4.2.6 后台GC函数**
负责执行后台垃圾回收任务。它确保在进行垃圾回收时,只有一个线程能够访问共享资源,并且在完成任务后通知等待的线程。
```cpp
void DBImpl::BackgroundGarbageCollect()
```
##### **4.2.7 后台GC函数**
垃圾回收的核心实现。在目前的设计下,它遍历数据库目录中的所有valuelog文件,并尝试回收不再需要的数据。
```cpp
void DBImpl::GarbageCollect()
```
------
## 5. 功能测试
### 5.1**单元测试(测试用例)**:
#### 依据我们的设计,每周的工作内容完成后,都将对当前完成的功能进行正确性检验。以下以第一周我们完成的功能为例:
#### 第一周
**字段数组的存储与读取:**
验证了 `Put_with_fields``Get_with_fields` 的正确性,确保字段数组可以正确序列化存储并反序列化读取。
**基于字段的键查询:**
验证了 `Get_keys_by_field` 的逻辑,确保能够根据字段值查找所有匹配的键。
**Key Value分离:**
并未额外设计,通过上两个功能的正确运行能够证明Key Value分离的初步实现大体是正确的。
```c++
#include "gtest/gtest.h"
#include "leveldb/env.h"
#include "leveldb/db.h"
using namespace leveldb;
constexpr int value_size = 2048;
constexpr int data_size = 128 << 20;
Status OpenDB(std::string dbName, DB **db) {
Options options;
options.create_if_missing = true;
return DB::Open(options, dbName, db);
}
TEST(TestTTL, OurTTL) {
DB *db;
WriteOptions writeOptions;
ReadOptions readOptions;
if(OpenDB("testdb_for_XOY", &db).ok() == false) {
std::cerr << "open db failed" << std::endl;
abort();
}
std::string key = "k_1";
std::string key1 = "k_2";
FieldArray fields = {
{"name", "Customer#000000001"},
{"address", "IVhzIApeRb"},
{"phone", "25-989-741-2988"}
};
FieldArray fields1 = {
{"name", "Customer#000000001"},
{"address", "abc"},
{"phone", "def"}
};
db->Put_with_fields(WriteOptions(), key, fields);
db->Put_with_fields(WriteOptions(), key1, fields1);
// 读取并反序列化
FieldArray value_ret;
db->Get_with_fields(ReadOptions(), key, &value_ret);;
for(auto pr:value_ret){
std::cout<<std::string(pr.first.data(),pr.first.size())<<" "<<std::string(pr.second.data(),pr.second.size())<<"\n";
}
std::vector<std::string> v;
db->Get_keys_by_field(ReadOptions(),fields[0],&v);
for(auto s:v)std::cout<<s<<"\n";
delete db;
}
int main(int argc, char** argv) {
// All tests currently run with the same read-only file limits.
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
```
#### 进一步设计
**对KV分离实现更细粒度的测试,以及对KV分离GC操作实现测试**
1.向表内插入一些value较小的键值对以及value较大的键值对,随后通过检查ValueLog内部数据(也可以是ValueLog文件长度)来判断是否对长短数据各自进行了处理。
2.向表内插入大量value较大的键值对后,查询ValueLog文件总数,删除其中绝大多数键值对,然后再查一次ValueLog文件总数,期望文件总数变少。
### 5.2**性能测试(Benchmark)**:
这一部分我们希望在完成大部分功能后再根据代码调整。
初步计划:
1.测试大数据量下短键值对和长键值对分别的插入和查询效率,与原版LevelDB作对比。
2.测试大数据量下磁盘使用率,与原版LevelDB作对比。
3.测试大数据量下合并的速率,与原版LevelDB作对比。
4.完成了多种KV分离方案后,将不同方案在Benchmark下进行测试。
原leveldb:
![image-20241209020150442](设计文档.assets/image-20241209020150442.png)
version_2:
![image-20241209014113365](设计文档.assets/image-20241209014113365.png)
version_3:
![image-20241208233143593](设计文档.assets/image-20241208233143593.png)
## 6. 可能遇到的挑战与解决方案
如何处理**GC开销、数据同步**
**如何实现GC**
在数据结构设计中已经进行了详细说明。
第二种设计通过合并过程自动完成了GC的功能。
第三种设计通过设计了一种异步的GC操作,使GC无需改变SSTable数据。
**数据同步**
写Value Log的时机和写WAL的时机一致,都在写MemTable之前完成。如果用户的Sync参数设置为True,则要保证Value Log一定写入完成后才能返回给用户写入成功的信息。
**减少GC开销**
有一种可能的优化是仅在数据写入SSTable之后才会使用Value Log。
## 7. 分工和进度安排
| 功能 | 完成日期 | 分工 |
| ------------------------------------------------- | -------- | ------------- |
| 完成初步的多字段Value实现和KV分离实现 | 11月20日 | 谢瑞阳 |
| 完成设计文档 | 11月27日 | 徐翔宇&谢瑞阳 |
| 将多字段Value实现迁移至用户层级 | 11月27日 | 徐翔宇 |
| 完成第二版ValueLog的设计 | 11月27日 | 谢瑞阳 |
| 完成第二版ValueLog的测试 | 11月27日 | 徐翔宇 |
| 完成第三版ValueLog的函数接口实现以及测试 | 12月1日 | 徐翔宇 |
| 完成第三版ValueLog的函数实现 | 12月4日 | 谢瑞阳 |
| 完成BenchMark设计 | 12月8日 | 徐翔宇&谢瑞阳 |
| 完成BenchMark,对不同KV分离方案进行测试 | 12月11日 | 徐翔宇 |
| 基于测试结果进行优化,完成第四版ValueLog的设计... | 12月??日 | 徐翔宇&谢瑞阳 |
## 8. 每周进度更新
12.3-12.9:实现version_2,修改version_3设计,完成version_3,benchmark实验。
- [ ] 在valuelog中调整value和key的顺序,kv_len的储存形式,get的时机应该在读取value之前
- [ ] 调整kv分离标志位的位置
- [ ] 实现valuelog的block_cache

Loading…
Cancel
Save