Ver código fonte

Merge pull request 'ld' (#3) from ld into main

Reviewed-on: 10225501448/leveldb_proj2#3
main
李度 8 meses atrás
pai
commit
c1d487d2f5
22 arquivos alterados com 682 adições e 84 exclusões
  1. +609
    -12
      README.md
  2. +2
    -0
      benchmarks/db_bench.cc
  3. +40
    -39
      benchmarks/db_bench_FieldDB.cc
  4. +2
    -4
      fielddb/field_db.cpp
  5. +18
    -18
      fielddb/field_db.h
  6. +6
    -9
      fielddb/request.cpp
  7. +0
    -2
      fielddb/request.h
  8. BIN
      pics/basic.png
  9. BIN
      pics/f_bsize.png
  10. BIN
      pics/f_thread.png
  11. BIN
      pics/field单.png
  12. BIN
      pics/field双.png
  13. BIN
      pics/fillbatch.png
  14. BIN
      pics/level单.png
  15. BIN
      pics/level双.png
  16. BIN
      pics/level四.png
  17. BIN
      pics/parallel.png
  18. BIN
      pics/q&m_bsize.png
  19. BIN
      pics/queue&mutex.png
  20. BIN
      pics/reover.png
  21. +4
    -0
      test/helper.cc
  22. +1
    -0
      test/parallel_test.cc

+ 609
- 12
README.md Ver arquivo

@ -6,11 +6,12 @@ leveldb中的存储原本只支持简单的字节序列,在这个项目中我
# 2. 功能实现
## 2.1 字段
设计目标:对value存储读取时进行序列化编码,使其支持字段。
设计目标:对value存储读取时进行序列化编码,使其支持字段。
这一部分的具体代码在util/serialize_value.cc中
实现思路:设计之初有考虑增加一些元数据(例如过滤器、字段偏移支持二分)来加速查询。但考虑到在数据库中kv的数量是十分庞大的,新加数据结构会带来巨大的空间开销。因此我们决定在这里牺牲时间换取空间,而将时间的加速放在索引中。
在这一基础上,我们对序列化进行了简单的优化:将字段名排序后,一一调用leveldb中原本的编码方法`PutLengthPrefixedSlice`存入value。这样不会有额外的空间开销,而好处在于遍历一个value的字段时,如果得到的字段名比目标大,就可以提前结束遍历。
```
```c++
std::string SerializeValue(const FieldArray& fields){
std::sort(sortFields.begin(), sortFields.end(), compareByFirst);
for (const Field& pairs : sortFields) {
@ -22,13 +23,237 @@ std::string SerializeValue(const FieldArray& fields){
```
最终db类提供了新接口`putFields`, `getFields`,分别对传入的字段序列化后调用原来的`put`, `get`接口。
`FindKeysByField`调用`NewIterator`遍历所有数据,field名和值符合则加入返回的key中。
**这一部分的具体代码在util/serialize_value.cc中**
## 2.2 二级索引
设计目标:对某个字段(属性)建立索引,提高对该字段的查询效率。
这一部分的具体代码在field/下
### 2.2.1 总体架构
fielddb
1. **二级索引的设计**
二级索引的难点主要包括以下几点:索引数据与kv数据的存储需要进行隔离,不同操作之间存在同步与异步问题,每一次的写入操作都需要额外考虑数据库原本的索引情况,任何操作还需要考虑两种数据间的一致性。为了使设计简洁化,避免不同模块耦合带来潜在的问题,我们的设计如下:
总体上,我们对两种数据分别创建一个db类的对象kvDb, indexDb。对外的接口类FieldDb包含了这两个对象,提供原先的leveldb各种接口,以及新功能,并在这一层完成两个对象的管理。这两个子数据库共同协作,完成了二级索引的各核心操作。在此基础上,为了保证数据库崩溃时两个子数据库的一致性,我们设计了第三个子数据库metadb,它的作用类似于日志。
```c++
class FieldDB : DB {
public:
FieldDB() : indexDB_(nullptr), kvDB_(nullptr), metaDB_(nullptr) {};
~FieldDB();
/*lab1的要求,以及作为db派生类要实现的虚函数*/
Status Put(const WriteOptions &options, const Slice &key, const Slice &value) override;
Status PutFields(const WriteOptions &, const Slice &key, const FieldArray &fields) override;
Status Delete(const WriteOptions &options, const Slice &key) override;
Status Write(const WriteOptions &options, WriteBatch *updates) override;
Status Get(const ReadOptions &options, const Slice &key, std::string *value) override;
Status GetFields(const ReadOptions &options, const Slice &key, FieldArray *fields) override;
std::vector<std::string> FindKeysByField(Field &field) override;
Iterator * NewIterator(const ReadOptions &options) override;
const Snapshot * GetSnapshot() override;
void ReleaseSnapshot(const Snapshot *snapshot) override;
bool GetProperty(const Slice &property, std::string *value) override;
void GetApproximateSizes(const Range *range, int n, uint64_t *sizes) override;
void CompactRange(const Slice *begin, const Slice *end) override;
/*与索引相关*/
Status CreateIndexOnField(const std::string& field_name, const WriteOptions &op);
Status DeleteIndex(const std::string &field_name, const WriteOptions &op);
std::vector<std::string> QueryByIndex(const Field &field, Status *s);
IndexStatus GetIndexStatus(const std::string &fieldName);
static Status OpenFieldDB(Options& options,const std::string& name,FieldDB** dbptr);
private:
//根据metaDB的内容进行恢复
Status Recover();
private:
leveldb::DB *kvDB_;
leveldb::DB *metaDB_;
leveldb::DB *indexDB_;
std::string dbname_;
const Options *options_;
Env *env_;
}
```
这样的设计带来了如下的好处:
kv和index实现了完全的分离,并且由于各自都使用了leveldb构建的lsmtree,完全保证了内部实现的正确性。相应的,我们的工作基本只处在fielddb层,减少了模块的耦合,对于我们自己实现的正确性也有极大的提升。
所有leveldb原本的功能仍然能够支持,并且有些实现起来十分简单,比如:
```c++
Iterator * FieldDB::NewIterator(const ReadOptions &options) {
return kvDB_->NewIterator(options);
}
Status FieldDB::GetFields(const ReadOptions &options, const Slice &key, FieldArray *fields) {
return kvDB_->GetFields(options, key, fields);
}
```
此外,性能开销增加也只在fielddb层,使我们能够进行比较和优化。
2. **index的编码**
index编码仍然采用了leveldb提供`PutLengthPrefixedSlice`,保留信息的同时,提高空间利用率。
对于一个`key : {name : val}`的字段,索引采用如下编码:
```c++
inline void AppendIndexKey(std::string* result, const ParsedInternalIndexKey& key){
PutLengthPrefixedSlice(result, key.name_);
PutLengthPrefixedSlice(result, key.val_);
PutLengthPrefixedSlice(result, key.user_key_);
```
这一部分也被模块化的封装在field/encode_index.h中。
由此产生了索引读的方法:根据name和val构建一个新的iterator,迭代获取范围内的所有key:
```c++
std::vector<std::string> FieldDB::QueryByIndex(const Field &field, Status *s) {
if (index_.count(field.first) == 0 || index_[field.first].first != Exist){
*s = Status::NotFound(Slice());
return std::vector<std::string>();
}
std::string indexKey;
AppendIndexKey(&indexKey,
ParsedInternalIndexKey(Slice(), field.first, field.second));
Iterator *indexIterator = indexDB_->NewIterator(ReadOptions());
indexIterator->Seek(indexKey);
std::vector<std::string> result;
for (; indexIterator->Valid(); indexIterator->Next()) {
ParsedInternalIndexKey iterKey;
if (ParseInternalIndexKey(indexIterator->key(), &iterKey)){
if (iterKey.name_ == field.first && iterKey.val_ == field.second){
result.push_back(iterKey.user_key_.ToString());
continue; //查到说明在范围里,否则break
}
}
break;
}
delete indexIterator;
*s = Status::OK();
return result;
}
```
3. **新的写流程**
索引功能的出现,使得写的逻辑需要重新设计。因为每一次写,不仅需要关注本次写入的字段,是不是需要同时写入索引,还需要关注本次写入的key,是不是覆盖了数据库原本的key,导致需要修改原本key的索引情况。这也意味着,即使是put简单的kv(不带字段),实际上还是需要修改put逻辑。方便起见,我们为原本的put中的value加入一个""的字段名,也视为putfield(这只是为了使我们的数据库支持原本的所有功能,也并不是本项目的重点,完全可以索性删除put功能,让我们的数据库只支持字段value)。
下面是putfield的实现思路:
```c++
void FieldsReq::ConstructBatch(WriteBatch &KVBatch,WriteBatch &IndexBatch,
WriteBatch &MetaBatch,fielddb::FieldDB *DB,
SliceHashSet &batchKeySet)
{
if (batchKeySet.find(Key) != batchKeySet.end()){
return;//并发的被合并的put/delete请求只处理一次
} else {
batchKeySet.insert(Key);
}
std::string val_str;
s = DB->kvDB_->Get(ReadOptions(), Key, &val_str);
FieldSliceArray oldFields;
if (s.IsNotFound()){
// oldFields = nullptr;
} else if (s.ok()) { //得到数据库之前key的fields, 判断需不需要删除其中潜在的索引
Slice nameSlice, valSlice;
Slice Value(val_str);
while(GetLengthPrefixedSlice(&Value, &nameSlice)) {
if(GetLengthPrefixedSlice(&Value, &valSlice)) {
oldFields.push_back({nameSlice,valSlice});
} else {
std::cout << "name and val not match! From FieldsReq Init" << std::endl;
assert(0);
}
nameSlice.clear(), valSlice.clear();
}
} else {
assert(0);
}
bool HasIndex = false;
bool HasOldIndex = false;
{
DB->index_mu.AssertHeld();
//1.将存在冲突的put pend到对应的请求
for(auto &[field_name,field_value] : SliceFields) {
if(field_name.data() == EMPTY) break;
if(DB->index_.count(field_name.ToString())) {
auto [index_status,parent_req] = DB->index_[field_name.ToString()];
if(index_status == IndexStatus::Creating || index_status == IndexStatus::Deleting) {
parent_req->PendReq(this->parent);
return;
} else if(index_status == IndexStatus::Exist) {
HasIndex = true;
}
}
}
//冲突也可能存在于,需要删除旧数据的索引,但该索引正在创删中
if (!oldFields.empty()){
for(auto &[field_name,field_value] : oldFields) {
if(field_name.data() == EMPTY) break;
if(DB->index_.count(field_name.ToString())) {
auto [index_status,parent_req] = DB->index_[field_name.ToString()];
if(index_status == IndexStatus::Creating || index_status == IndexStatus::Deleting) {
parent_req->PendReq(this->parent);
return;
} else if(index_status == IndexStatus::Exist) {
HasOldIndex = true;
}
}
}
}
std::string scrach = SerializeValue(SliceFields);
KVBatch.Put(Slice(Key), Slice(scrach));
//2.对于没有冲突但含有索引操作的put,构建metaKV
if(HasIndex || HasOldIndex) {
std::string MetaKey,MetaValue;
std::string serialized = SerializeValue(SliceFields);
MetaKV MKV = MetaKV(Key,serialized);
MKV.TransPut(MetaKey, MetaValue);
MetaBatch.Put(MetaKey, serialized);
//3.1对于含有索引的oldfield删除索引
if (HasOldIndex) {
for(auto &[field_name,field_value] : oldFields) {
if(field_name.data() == EMPTY) continue;
if(DB->index_.count(field_name.ToString()) && //旧数据有,新数据没有的字段,删索引
std::find(SliceFields.begin(), SliceFields.end(),
std::make_pair(field_name, field_value)) == SliceFields.end()) {
std::string indexKey;
AppendIndexKey(&indexKey, ParsedInternalIndexKey(
Key,field_name,field_value));
IndexBatch.Delete(indexKey);
}
}
}
//3.2对于含有索引的field建立索引
if (HasIndex) {
for(auto &[field_name,field_value] : SliceFields) {
if(field_name.data() == EMPTY) continue;
if(DB->index_.count(field_name.ToString())) {
std::string indexKey;
AppendIndexKey(&indexKey, ParsedInternalIndexKey(
Key,field_name,field_value));
IndexBatch.Put(indexKey, Slice());
}
}
}
}
}
}
```
同理,delete也需要先读最新的数据,再进行相应的处理,这里简单贴上实现逻辑:
```
//1. 读取当前的最新的键值对,判断是否存在含有键值对的field
//2.1 如果无,则正常构造delete
//2.2 如果是有的field的索引状态都是exist,则在meta中写KV_Deleting类型的记录
//在kvDB和indexDB中写入对应的delete
//2.3 如果存在field的索引状态是Creating或者Deleting,那么在那个队列上面进行等待
```
上面的代码也展现了并发与恢复的部分,接下来会一一阐述。
### 2.2.2 如何并发创删索引与读写
1. **为什么要并发控制创删索引与读写**
@ -238,8 +463,106 @@ BatchReq::BatchReq(WriteBatch *Batch,port::Mutex *mu):
}
```
### 2.2.3 如何保证两个kv与index的一致性
metadb
### 2.2.3 如何保证kv与index的一致性
metadb为异常恢复服务,只涉及到putfield和delete部分。(这里最初的设计有些问题,当时认为异常恢复也需要考虑创删索引部分,但实际上创删索引的本质,是一次往indexdb的writebatch,只会有索引整体写入成功和不成功两种情况,并不会出现不一致问题。)
因此metadb的编码,只要在原本kv编码的基础上,加一个标志位,标识本条是来自putfield还是delete。
metadb提供的功能被封装在fielddb/meta.cc中,包括编码:
```c++
void MetaKV::TransPut(std::string &MetaKey,std::string &MetaValue) {
MetaKey.clear();
MetaValue.clear();
std::string &buf = MetaKey;
PutFixed32(&buf, KV_Creating);
PutLengthPrefixedSlice(&buf, Slice(name));
}
void MetaKV::TransDelete(std::string &MetaKey) {
MetaKey.clear();
std::string &buf = MetaKey;
PutFixed32(&buf, KV_Deleting);
PutLengthPrefixedSlice(&buf, Slice(name));
}
```
以及kv和index写完后的清理(构建一个都是delete的writebatch,向metadb中写入):
```c++
class CleanerHandler : public WriteBatch::Handler {
public:
WriteBatch *NeedClean;
void Put(const Slice& key, const Slice& value) override {
//将所有之前put的meta数据进行delete
NeedClean->Delete(key);
}
void Delete(const Slice& key) override {
//所有的传入的MetaBatch都是Put的
assert(0);
}
};
void MetaCleaner::Collect(WriteBatch &MetaBatch) {
if(MetaBatch.ApproximateSize() <= 12) return;
CleanerHandler Handler;
Handler.NeedClean = &NeedClean;
MetaBatch.Iterate(&Handler);
}
void MetaCleaner::CleanMetaBatch(DB *metaDB) {
if(NeedClean.ApproximateSize() <= 12) return;
metaDB->Write(WriteOptions(), &NeedClean);
}
```
相应的,我们数据库的恢复也是建立在三个数据库的协作之上:
在重新打开三个数据库,依靠各自的日志恢复各自的数据后,完成对索引相关内容的恢复:
```c++
Status FieldDB::Recover() {
//1. 遍历所有Index类型的meta,重建内存中的index_状态表
Iterator *Iter = indexDB_->NewIterator(ReadOptions());
std::string IndexKey;
Iter->SeekToFirst();
while(Iter->Valid()) {
IndexKey = Iter->key().ToString();
ParsedInternalIndexKey ParsedIndex;
ParseInternalIndexKey(Slice(IndexKey),&ParsedIndex);
index_[ParsedIndex.name_.ToString()] = {Exist,nullptr};
//构建下一个搜索的对象,在原来的fieldname的基础上加一个最大的ascii字符(不可见字符)
std::string Seek;
PutLengthPrefixedSlice(&Seek, ParsedIndex.name_);
Seek.push_back(0xff);
Iter->Seek(Slice(Seek));
}
delete Iter;
//2. 寻找所有KV类型的meta,再次提交一遍请求
Iter = metaDB_->NewIterator(ReadOptions());
Slice MetaValue;
Iter->SeekToFirst();
while (Iter->Valid()) {
MetaValue = Iter->key();
MetaType type = MetaType(DecodeFixed32(MetaValue.data()));
MetaValue.remove_prefix(4);//移除头上的metaType的部分
Slice extractKey;
GetLengthPrefixedSlice(&MetaValue, &extractKey);
if(type == KV_Creating) {
FieldArray fields;
ParseValue(Iter->value().ToString(), &fields);
PutFields(WriteOptions(), extractKey, fields);
} else if(type == KV_Deleting) {
Delete(WriteOptions(), extractKey);
} else {
assert(0 && "Invalid MetaType");
}
Iter->Next();
}
delete Iter;
//在所有的请求完成后,会自动把metaDB的内容清空。
Iter = metaDB_->NewIterator(ReadOptions());
Iter->SeekToFirst();
delete Iter;
return Status::OK();
}
```
## 3. 测试
### 3.1 正确性测试
@ -261,7 +584,7 @@ InsertOneField:只插一条特定数据的测试(让key<=0, 和批量写入
DeleteOneField:只删一条特定数据的测试,功能与单条插入相似,主要用来测试delete。
GetOneField:只读一条特定数据的测试,与上面两条对应。
对于所有涉及随机性的函数,传参一个随机种子。只要随机种子一致,随机生成的内容就一致,相应的读需要和相应的写、删保持一致。此外测试的数据量也是可以修改的。
对于所有涉及随机性的函数,传参一个随机种子。只要随机种子一致,随机生成的内容就一致,相应的读需要和相应的写、删保持一致。此外测试的数据量也是可以修改的。需要注意的是,之前项目使用的srand线程不安全,使用std::mt19937可以保证多线程时随机序列也一致。
InsertFieldData:批量写入,按照上面提到的测试规则生成kv,并调用putfields。检查返回状态是否ok。同时对于生成的字段,如果是address为shanghai或age为20,统计入相应的set。
@ -289,7 +612,8 @@ TestLab1流程: 批量写 -> 必须读到 -> findkeysbycity -> 批量删 ->
TestLab2流程:批量写 -> 创索引address,age -> 索引查询address,age -> 删索引address -> 索引查询address(haveindex=false) -> 索引查询age -> 批量删 -> 索引查询age(索引还在能查,但返回的key数量为0) -> write -> 必须读到 -> 索引查询age。
至此,上面的流程基本覆盖了我们数据库的每个基础功能。
至此,上面的流程基本覆盖了我们数据库的每个基础功能。
![alt text](pics/basic.png)
#### 3.1.2 并发测试
相关代码在`parallel_test.cc`中
@ -308,7 +632,8 @@ TestPutDelete:创索引 -> 并发:两线程写0、1种子数据,两线程
TestWrite(在之前基础上并发所有,并加入write):创索引address、批量写种子2 -> 并发:线程0创索引age,其他线程忙等至开始创建,线程1批量写种子0,线程2write种子1, 线程3删索引age -> 检测:种子012所有数据都应被读到,一致性, age索引被删除。
这里的测试也可以加入delete,或不删索引age检测age的一致性,具体见注释。
至此,上面的流程基本覆盖了我们数据库的每个基础功能之间的并发。
至此,上面的流程基本覆盖了我们数据库的每个基础功能之间的并发。
![alt text](pics/parallel.png)
#### 3.1.2 恢复测试
相关代码在`recover_test.cc`中
@ -317,14 +642,286 @@ TestWrite(在之前基础上并发所有,并加入write):创索引addres
TestNormalRecover:创索引、批量写、此时之前测试都检测过能被读到 -> delete db -> 重新open -> 读数据、索引查(之前写入的数据仍能被读到)。
TestParalRecover**该测试比较特别,需要运行两次**:创索引 -> 并发:线程0批量写,线程1write,线程2delete,线程3 在单条插入后,deletedb。线程3导致了其他线程错误,测试会终止(模拟数据库崩溃),这会导致各线程在各种奇怪的时间点崩溃。此时注释掉上半部分代码,运行下半部分:单条写入能被读到,并检测一致性。
这里我们运行了几十次,前半部分的崩溃报错有多种,但后半部分的运行都是成功的。同时也追踪了恢复的运行过程,确实有数据从metadb中被正确解析。
这里我们运行了几十次,前半部分的崩溃报错有多种,但后半部分的运行都是成功的。同时也追踪了恢复的运行过程,确实有数据从metadb中被正确解析。
![alt text](pics/reover.png)
### 3.2 性能测试、分析、优化
#### 3.2.1 性能测量的实现
我们主要采用了外部测量和内部测量相互结合的方式,来评估数据库系统的性能表现和定位性能瓶颈。外部测量的方式主要借助于benchmark完成。内部测量的主要采用插桩来测量数据库的各个部分的性能损耗情况。
相较于原版的leveldb,FieldDB增加了Field和二级索引功能,因此我们针对新的常见使用场景,增加了benchmark的测试点。新增部分有:
```c++
"CreateIndex," //创建索引
"FindKeysByField," //得到包含所有Field的KV对(不使用索引)
"QueryByIndex," //通过索引得到对应的主键
"DeleteIndex," //删除索引
"WriteSeqWhileCreating," //创建索引的同时顺序写
"WriteSeqWhileDeleting," //删除索引的同时顺序写
"WriteRandomWhileCreating," //创建索引的同时随机写
"WriteRandomWhileDeleting," //删除索引的同时随机写
"ReadSeqWhileCreating," //创建索引的同时顺序读
"ReadSeqWhileDeleting," //删除索引的同时顺序读
"ReadRandomWhileCreating," //创建索引的同时随机读
"ReadRandomWhileDeleting," //删除索引的同时随机读
"WriteRandomWithIndex," //随机写带索引的键值
"WriteSeqWithIndex," //顺序写带索引的键值
"WriteSeqWhileIndependentCCD," //在不断创建删除索引的情况下,顺序写与创删索引无关的数据
"WriteSeqWhileCCD," //在不断创建删除索引的情况下,顺序写与创删索引有关的数据
```
通过上述新增加的benchmark,可以更加全面的了解增加了新功能后的,各个常见使用场景下的FieldDB的性能指标。各个benchmark的具体实现可以在`/benchmarks/db_becnh_FieldDB.cc`中找到。
为了能够进一步的定位性能瓶颈,我们对于操作的关键路径进行了层次化的插桩分析,实现更加精准的性能测量。根据外部测量得到的数据,相较于leveldb,对于读性能,FieldDB几乎没有影响,但是对于写性能,FieldDB性能有所下降,因此我们着重使用插桩分析了写入的关键路径。由于所收集的数据如下:
```c++
int count = 0;//总计完成请求数量
int count_Batch = 0;//总计完成的Batch数量
int count_Batch_Sub = 0;//总计完成的Batch_sub数量
uint64_t elapsed = 0;//总计时间消耗
uint64_t construct_elapsed = 0;//构建写入内容消耗
uint64_t construct_BatchReq_init_elapsed = 0;//请求初始化消耗
uint64_t construct_BatchReq_elapsed = 0;//构建batch的消耗
uint64_t construct_BatchReq_Sub_elapsed = 0;//构建batch_sub消耗
uint64_t construct_BatchReq_perSub_elapsed = 0;//每个Batch_sub消耗
uint64_t construct_FieldsReq_Read_elapsed = 0;//构建时读取的消耗
uint64_t write_elapsed = 0;//写入的总耗时
uint64_t write_meta_elapsed = 0;//写入meta的耗时
uint64_t write_index_elapsed = 0;//写入index的耗时
uint64_t write_kv_elapsed = 0;//写入kv的耗时
uint64_t write_clean_elapsed = 0;//清除meta的耗时
uint64_t write_bytes = 0;
uint64_t write_step = 500 * 1024 * 1024;
uint64_t write_bytes_lim = write_step;
uint64_t temp_elapsed = 0;
uint64_t waiting_elasped = 0; //等待耗时
inline void dumpStatistics() {
if(count && count % 500000 == 0 || write_bytes && write_bytes > write_bytes_lim) {
std::cout << "=====================================================\n";
std::cout << "Total Count : " << count;
std::cout << "\tTotal Write Bytes(MB) : " << write_bytes / 1048576.0 << std::endl;
std::cout << "Average Time(ms) : " << elapsed * 1.0 / count;
std::cout << "\tAverage Write rates(MB/s) : " << write_bytes / 1048576.0 / elapsed * 1000000 << std::endl;
std::cout << "Construct Time(ms) : " << construct_elapsed * 1.0 / count << std::endl;
std::cout << "\tConstruct BatchReq Init Time(ms) : " << construct_BatchReq_init_elapsed * 1.0 / count << std::endl;
std::cout << "\tConstruct BatchReq Time(ms) : " << construct_BatchReq_elapsed * 1.0 / count << std::endl;
std::cout << "\tConstruct BatchReq Sub Time(ms) : " << construct_BatchReq_Sub_elapsed * 1.0 / count << std::endl;
std::cout << "\tConstruct BatchReq perSub Time(ms) : " << construct_BatchReq_perSub_elapsed * 1.0 / count_Batch_Sub << std::endl;
std::cout << "\tConstruct FieldsReq Read Time(ms) : " << construct_FieldsReq_Read_elapsed * 1.0 / count << std::endl;
std::cout << "Write Time(ms) : " << write_elapsed * 1.0 / count << std::endl;
std::cout << "\tWrite Meta Time(ms) : " << write_meta_elapsed * 1.0 / count << std::endl;
std::cout << "\tWrite Index Time(ms) : " << write_index_elapsed * 1.0 / count << std::endl;
std::cout << "\tWrite KV Time(ms) : " << write_kv_elapsed * 1.0 / count << std::endl;
std::cout << "\tWrite Clean Time(ms) : " << write_clean_elapsed * 1.0 / count << std::endl;
std::cout << "TaskQueue Size : " << taskqueue_.size() << std::endl;
std::cout << "temp_elased : " << temp_elapsed * 1.0 / count << std::endl;
std::cout << "waiting elapsed : " << waiting_elasped * 1.0 / count << std::endl;
std::cout << "=====================================================\n";
write_bytes_lim = write_bytes + write_step;
std::fflush(stdout);
}
}
```
数据收集的方式如下所示(仅展示部分):
```c++
Status FieldDB::HandleRequest(Request &req, const WriteOptions &op) {
uint64_t start_ = env_->NowMicros();
MutexLock L(&mutex_);
taskqueue_.push_back(&req);
while(true){
uint64_t start_waiting = env_->NowMicros();
while(req.isPending() || !req.done && &req != taskqueue_.front()) {
req.cond_.Wait();
}
waiting_elasped += env_->NowMicros() - start_waiting;
if(req.done) {
elapsed += env_->NowMicros() - start_;
count ++;
dumpStatistics();
return req.s; //在返回时自动释放锁L
}
Request *tail = GetHandleInterval();
WriteBatch KVBatch,IndexBatch,MetaBatch;
SliceHashSet batchKeySet;
Status status;
}
}
/************************************************************************/
```
#### 3.2.2 性能分析与优化
通过外部测量和内部测量,我们定位到了许多的值得优化的点,并进行迭代,下面将对于两个比较显著的点进行阐述:
1. 消除冗余的写入请求
通过插桩分析,我们发现对于一个普通的写入,理论上只有对于kvDB写入的流量,但是在实际写入的时候,对于kvDB、metaDB、indexDB都会产生时间消耗。通过审阅代码,我们发现原因在于产生了对于空WriteBatch的写入,因此在写入之前对于WriteBatch的大小进行判断,如果是一个空的WriteBatch,则直接跳过实际写入流程。
### 3.2 性能测试
测试、分析、优化
2. 使用Slice代替std::string
进一步分析了写入流程的各个部分后,我们认为实际写入数据库部分的写入消耗已经到了理论上限,在现有的结构上并没有优化的办法。同时,在写入流程中的读数据库也无法很好地优化。我们将目光聚焦于构建请求部分的实现上的优化。经过了代码分析,我们发现了在一开始实现的时候为了实现的便捷,我们大量的使用了stl库的`std::string`,但是有每次`string`的拷贝构造问题,会导致数据的多次在内存中的拷贝操作,在数据量较大的时候,我们认为对于性能会产生影响。
基于上述的考量,我们通过几轮的commit,将`request`内部的数据结构、相关辅助数据结构以及实现方式全部尽可能的使用`Slice`替换`std::string`。经过测试,我们发现性能确实有所提高。
#### 3.2.3 最终版本的性能分析
1. 对于leveldb本身的一些分析
在对fielddb进行性能测试之前,我们首先运行了leveldb自带的db_bench对原版leveldb进行测试。单线程的测试结果总体上符合预期,但是对多线程并发写的测试结果有一些困惑:双线程相比单线程的各种写性能降低了一倍多,四线程再继续降低。考虑到leveldb的写是通过维护写队列、合并writebatch写完成,理论上并发的锁竞争只在写队列,是非常小的。起初我们以为是因为db_bench的数据量随线程翻倍而翻倍,导致了后台合并增加,影响了性能,但修改总数据量为一致后,并没有改变测试结果。
单线程
![alt text](pics/level单.png)
双线程
![alt text](pics/level双.png)
四线程
![alt text](pics/level四.png)
最后经过多个方面的尝试,我们发现问题的出处。原本的db_bench中所有的写,默认都是每个batch数据量为1。如果扩大了每个batch到1000(总数据量不变),也就是fillbatch测试,多线程这一因素不会影响到性能。
![alt text](pics/fillbatch.png)
从这一结果倒推可能的原因,我们认为主要问题在于如果每个write的batch过小,实际处理速度过快,使得性能的瓶颈处在了写队列竞争上,而合并写这一策略并没有实际产生效果。我们使用了一个小尝试印证了这一推测:直接对write函数开头使用一把全局互斥锁,对写进行同步。尝试结果是,在原本的batch=1测试中,复杂的写队列策略甚至性能不如直接上全局锁,而随着batch的扩大,写队列策略的性能优势体现了出来,逐渐超过全局锁方法。下面是两种方式的一些比较,测量了顺序写的情况,实验数据取五次平均值:
双线程情况下,batchsize对性能的影响:
![alt text](pics/q&m_bsize.png)
batchsize=1000下,线程数对性能的影响:
![alt text](pics/queue&mutex.png)
这一实验体现了leveldb写队列策略在不同情况下的优劣。而我们fielddb的请求队列策略和这个基本一致,性能使用场景具有相似性。
2. 对于FieldDB的分析
单线程
![alt text](pics/field单.png)
双线程
![alt text](pics/field双.png)
1) 所有涉及读取性能的:和原版leveldb相比,损耗非常少(一个必要的字段解析步骤),还是非常好的
2) 常规的写入性能:有所下降,但是由于需要支持索引功能,一些额外的开销无法避免(例如先读一遍,并发控制,一致性维护)。fillbatch的测试,基于我们合并请求和一段请求只处理一次同名key的算法,多线程性能甚至比单线程能够提高许多
![alt text](pics/f_bsize.png)
![alt text](pics/f_thread.png)
其中单线程fillbatch差异比较明显。这里面的主要原因,一是因为leveldb本身这方面性能就非常优秀,二是因为在有索引功能的情况下,为了避免之前提到的问题,我们处理batch实际上还是需要把里面的每一条数据拆分出来,分别创建请求处理(而不是直接批量写入)。
这也反映在batchsize图中,我们的性能上升速率不如leveldb。但同样可观的上升速率意味着,我们的请求处理算法是有一定效果的。
3) 对于创删索引:没有比较对象,但是总体可以接受
4) 对于创删索引和写并发:如果是无关的,那么还是保持了高吞吐;如果是相关的,那么不得不受限于创删索引。考虑到数据库的创删索引请求还是比较少的(不太可能出现我们测试中,不停并发创删索引和写入的情况),一定的性能牺牲可以接受
#### 3.2.4 写放大问题的一些讨论
相比原版leveldb,我们的写放大问题主要在于metadb需要多一次日志写入。但leveldb的写放大本身比较严重,问题在于文件合并时的处理。这一具体的数值,取决于数据量变化造成的文件数量变化(通常在30~50倍)。我们在测试中进行了一些插桩,比较数据本身和log中统计得到的磁盘写入,在不触发大合并、不插入重复数据、不删除数据的前提下,kv本身写放大大约在1.8倍(索引不计入其中)。即使加上kv和meta的两份日志,和合并时几十倍相比仍然很少,并不是系统的主要问题。至于metadb本身的数据,我们会在写完后进行清理,不会进行实际的落盘,所以也没有额外的开销。(当然上述前提是写入数据有索引,需要写meta,否则我们的写是和原版保持一致的)
## 4. 问题与解决
### 设计层面
1. 我们对并发的写入请求进行了合并,但在测试中发现了一个问题:之前提到,putfield和delete都需要先读一次原来的数据,但他们读不到合并在一起的请求中,之前的那个数据。这就导致了不一致(e.g. 将对于同一个key的putfield和delete请求合并,处理putfield时本次处理要写kv和index,处理delete时先读,发现数据库原来的数据中字段没有索引,于是删kv但没有删index)。我们的解决方式是,对于并发的写入只处理第一个相同的key。这同样提高了处理请求的效率。
### 一些调试较久的bug
1. 测试部分srand、全局set多线程问题。
2. destroydb是全局的函数,而fielddb重新实现了一次。但在实际使用时忘记加fielddb的命名空间,导致调用了leveldb的destroy,而fielddb数据库一直没有正确删除。
3. 迭代器使用后忘记delete,导致版本一直没有unref。
4. 性能测试中创建索引时间慢于读,如果并发一个读和创会把创建的时间统计入读的时间。最后改为读线程一直循环读,直到创建完成后通知该线程结束。
## 5. 潜在优化点
1. 使用一些高性能并发设计模式,如reactor来优化多线程写入时由于锁竞争导致的性能问题
2. 采用一些高性能的库,如dpdk等
3. 使用一些基于polling的请求处理手段等
4. 对于各个log进行合并,减少写放大
5. `GetHandleInterval`中选择一段request时,设置一个上限(综合考量max_batchsize和索引写入开销),和子数据库的批量写对齐。
6. 创删索引时会先产生中间结果,再向indexdb批量写。设置一个单次写的上限,分批次写入中间结果。
7. 涵盖复杂的数据库故障问题,比如硬件故障、恢复文件丢失等。
## 6. 分工
功能 | 完成日期 | 分工
:------|:---------|:------
value序列化、lab1功能实现|11.19|李度
fieldDb接口框架|11.25|陈胤遒
lab1整体+测试|11.30|李度、陈胤遒、高宇菲
fieldDb功能实现(没有并发和恢复)|12.10|李度、陈胤遒
并发框架|12.15|陈胤遒
lab2测试、并发完成|12.20|李度
恢复|12.25|李度、陈胤遒
整体系统整合+测试|12.28|李度、陈胤遒、高宇菲
性能测试|1.1|李度、陈胤遒、高宇菲
## 7. KV分离
由于在讨论的时候我们想到了一种感觉非常思路清晰、实现简单的实现kv分离的方案,在基本完成了实验报告后,我们抽空花了一天半基本上实现了kv分离,代码总计700行左右下面对于设计进行最简单的介绍
### 7.1 KVLog
我们实现kv分离的主要途径是使用KVLog和FilePointer。KVLog和SSTable在生成的时候是一一对应的,但是并不随着SSTable的合并而合并。KVLog是一个如下结构的文件类型:
```
Batchsize|WriteBatch|Batchsize|WriteBatch|......
```
`FilePointer`包含三个信息,分别是KVLog的文件编号,偏移量以及信息的长度。
从逻辑结构来看,非常类似于leveldb的log,两者本质上存储的都是一系列的WriteBatch。但是不同的是,leveldb的log会每4KB对于内容进行切分,这会导致无法直接通过`(偏移量,长度)`读出内容,而KVLog是连续的存储,是可以直接通过`(偏移量,长度)`读出内容的。
由于leveldb的写入原子性是通过基于log文件的结构完成的,且log在写入和恢复的路径中被大量的使用,因此我们不直接使用KVLog替换log,而是先将WriteBatch写入KVLog,然后将该WriteBatch对应的`FilePointer`写入log中。这样子我们只要对于涉及log的代码进行最简单的更改即可。
之后,我们对于写入到memtable和SSTable中的内容进行处理,将所有的value替换成相应的`FilePointer`。
以上就是对于写入流程的处理,部分核心代码如下:
```c++
Status DBImpl::Write(const WriteOptions& options, WriteBatch* updates) {
/******************************/
FilePointer fp;
//1. 将WriteBatch写入到kvlog中
status = kvlog_->AddRecord(WriteBatchInternal::Contents(write_batch), fp);
//2. 将writebatch的filepointer写入到log中
// status = log_->AddRecord(WriteBatchInternal::Contents(write_batch));
char rep[8 * 3];
EncodeFP(fp, rep);
status = log_->AddRecord(Slice(rep, 3 * 8));
bool sync_error = false;
if (status.ok() && options.sync) {
status = logfile_->Sync();
if (!status.ok()) {
sync_error = true;
}
}
//3. 根据write_batch里面的内容,构建kp_batch
WriteBatchInternal::ConstructKPBatch(tmp_kp_batch_, write_batch, fp);
WriteBatchInternal::SetSequence(tmp_kp_batch_,temp_seq + 1);
if (status.ok()) {
status = WriteBatchInternal::InsertInto(tmp_kp_batch_, mem_);
}
/******************************/
}
class KVLog; //用于KVLog的写入
class KVLogReader;//用于读取KVLog中的每一个键值对以及相应的sequence
struct FilePointer {
uint64_t FileNumber;
uint64_t FileOffset;
uint64_t Size;
};
```
### 7.2 KVLog的管理
KVLog的管理主要参考的是SSTable的方式,采用多版本并发控制的思路。原因主要有两点:1. 有利于KVLog回收的实现;2.可以大量的复用或者参考SSTableFile的实现方式。核心代码如下:
```c++
class VersionEdit {
/*****************************/
void AddKVLogs(uint64_t file) {
FileMetaData f;
f.number = file;
new_kvlogs_.push_back(f);
}
void RemoveKVLogs(uint64_t file) {
deleted_kvlogs_.insert(file);
}
std::set<uint64_t> deleted_kvlogs_;
std::vector<FileMetaData> new_kvlogs_;
/*****************************/
}
class VersionSet{
/*****************************/
void AddLiveKVLogs(std::set<uint64_t>* live_kvlogs);
std::vector<FileMetaData*> kvlogs_;
void Apply(const VersionEdit* edit);
void SaveTo(Version* v);
/*****************************/
}
```
### 7.3 KVLog的回收
我们观察到Level0的SSTable是允许重叠的。因此,我们将KVLog的回收放在后台,同时禁止小合并将新生成SSTable放于L0以下的位置,这样可以保证L0以下的level不变。然后只回收KVLog不与mem、imm和L0SSTableu对应的kvlog。回收的时候新构建一个放在L0的SSTable以及对应的KVLog。新生成的SSTable包含的key和原来的sequence完全相同,value指向新的KVLog。这样子,随着之后的合并,自然就能够完成回收的操作也不会和并发的写入发生冲突。代码主体是`bool DBImpl::CollectKVLogs()`.
以上就是对于我们实现KV分离方式的最简要的介绍

+ 2
- 0
benchmarks/db_bench.cc Ver arquivo

@ -45,6 +45,7 @@
// sstables -- Print sstable info
// heapprofile -- Dump a heap profile (if supported by this port)
static const char* FLAGS_benchmarks =
"fillbatch,"
"fillseq,"
"fillsync,"
"fillrandom,"
@ -582,6 +583,7 @@ class Benchmark {
if (num_ < 1) num_ = 1;
} else if (name == Slice("fillseq")) {
fresh_db = true;
// entries_per_batch_ = 1000;
method = &Benchmark::WriteSeq;
} else if (name == Slice("fillbatch")) {
fresh_db = true;

+ 40
- 39
benchmarks/db_bench_FieldDB.cc Ver arquivo

@ -50,41 +50,42 @@ using namespace fielddb;
// sstables -- Print sstable info
// heapprofile -- Dump a heap profile (if supported by this port)
static const char* FLAGS_benchmarks =
// "fillseq,"
// "fillsync,"
// "fillrandom,"
// "overwrite,"
// "readrandom,"
// "readrandom," // Extra run to allow previous compactions to quiesce
// "readseq,"
// "readreverse,"
// "compact,"
// "readrandom,"
// "readseq,"
// "readreverse,"
// "fill100K,"
// "crc32c,"
// "CreateIndex,"
// "FindKeysByField,"
// "QueryByIndex,"
// "DeleteIndex,"
// "compact,"
// "WriteSeqWhileCreating,"
// "WriteSeqWhileDeleting,"
// "compact,"
// "WriteRandomWhileCreating,"
// "WriteRandomWhileDeleting,"
// "compact,"
// "ReadSeqWhileCreating,"
// "ReadSeqWhileDeleting,"
// "ReadRandomWhileCreating,"
// "ReadRandomWhileDeleting,"
// "WriteRandomWithIndex,"
// "WriteSeqWithIndex,"
"fillbatch,"
"fillseq,"
"WriteSeqWhileIndependentCCD,"
"fillseq,"
"WriteSeqWhileCCD,"
"fillsync,"
"fillrandom,"
"overwrite,"
"readrandom,"
"readrandom," // Extra run to allow previous compactions to quiesce
"readseq,"
"readreverse,"
"compact,"
"readrandom,"
"readseq,"
"readreverse,"
"fill100K,"
"crc32c,"
"CreateIndex," //创建索引
"FindKeysByField," //得到包含所有Field的KV对(不使用索引)
"QueryByIndex," //通过索引得到对应的主键
"DeleteIndex," //删除索引
"compact,"
"WriteSeqWhileCreating," //创建索引的同时顺序写
"WriteSeqWhileDeleting," //删除索引的同时顺序写
"compact,"
"WriteRandomWhileCreating," //创建索引的同时随机写
"WriteRandomWhileDeleting," //删除索引的同时随机写
"compact,"
"ReadSeqWhileCreating," //创建索引的同时顺序读
"ReadSeqWhileDeleting," //删除索引的同时顺序读
"ReadRandomWhileCreating," //创建索引的同时随机读
"ReadRandomWhileDeleting," //删除索引的同时随机读
"WriteRandomWithIndex," //随机写带索引的键值
"WriteSeqWithIndex," //顺序写带索引的键值
"fillseq,"
"WriteSeqWhileIndependentCCD," //在不断创建删除索引的情况下,顺序写与创删索引无关的数据
"fillseq,"
"WriteSeqWhileCCD," //在不断创建删除索引的情况下,顺序写与创删索引有关的数据
;
// Number of key/values to place in database
@ -356,8 +357,8 @@ class Stats {
}
AppendWithSpace(&extra, message_);
std::fprintf(stdout, "%-12s : %11.3f micros/op(%10d);%s%s\n",
name.ToString().c_str(), seconds_ * 1e6 / done_,done_,
std::fprintf(stdout, "%-12s : %11.3f micros/op;%s%s\n",
name.ToString().c_str(), seconds_ * 1e6 / done_,
(extra.empty() ? "" : " "), extra.c_str());
if (FLAGS_histogram) {
std::fprintf(stdout, "Microseconds per op:\n%s\n",
@ -811,9 +812,9 @@ class Benchmark {
}
shared.mu.Unlock();
for(int i = 0; i < n; i++) {
arg[i].thread->stats.Report(name);
}
// for(int i = 0; i < n; i++) {
// arg[i].thread->stats.Report(name);
// }
for (int i = 1; i < n; i++) {
arg[0].thread->stats.Merge(arg[i].thread->stats);

+ 2
- 4
fielddb/field_db.cpp Ver arquivo

@ -23,10 +23,9 @@
namespace fielddb {
using namespace leveldb;
//TODO:打开fieldDB
//打开fieldDB
Status FieldDB::OpenFieldDB(Options& options,
const std::string& name, FieldDB** dbptr) {
// options.env->CreateDir("./abc")
if(*dbptr == nullptr){
return Status::NotSupported(name, "new a fieldDb first\n");
}
@ -375,11 +374,10 @@ Iterator * FieldDB::NewIterator(const ReadOptions &options) {
return kvDB_->NewIterator(options);
}
// TODO:使用统一seq进行snapshot管理
const Snapshot * FieldDB::GetSnapshot() {
return kvDB_->GetSnapshot();
}
// TODO:同上
void FieldDB::ReleaseSnapshot(const Snapshot *snapshot) {
kvDB_->ReleaseSnapshot(snapshot);
}

+ 18
- 18
fielddb/field_db.h Ver arquivo

@ -92,23 +92,23 @@ private:
Request *GetHandleInterval(); //
// private:
// int count = 0;
// int count_Batch = 0;
// int count_Batch_Sub = 0;
// uint64_t elapsed = 0;
// uint64_t construct_elapsed = 0;
// uint64_t construct_BatchReq_init_elapsed = 0;
// uint64_t construct_BatchReq_elapsed = 0;
// uint64_t construct_BatchReq_Sub_elapsed = 0;
// uint64_t construct_BatchReq_perSub_elapsed = 0;
// uint64_t construct_FieldsReq_Read_elapsed = 0;
// uint64_t write_elapsed = 0;
// uint64_t write_meta_elapsed = 0;
// uint64_t write_index_elapsed = 0;
// uint64_t write_kv_elapsed = 0;
// uint64_t write_clean_elapsed = 0;
// int count = 0;//
// int count_Batch = 0;//Batch数量
// int count_Batch_Sub = 0;//Batch_sub数量
// uint64_t elapsed = 0;//
// uint64_t construct_elapsed = 0;//
// uint64_t construct_BatchReq_init_elapsed = 0;//
// uint64_t construct_BatchReq_elapsed = 0;//batch的消耗
// uint64_t construct_BatchReq_Sub_elapsed = 0;//batch_sub消耗
// uint64_t construct_BatchReq_perSub_elapsed = 0;//Batch_sub消耗
// uint64_t construct_FieldsReq_Read_elapsed = 0;//
// uint64_t write_elapsed = 0;//
// uint64_t write_meta_elapsed = 0;//meta的耗时
// uint64_t write_index_elapsed = 0;//index的耗时
// uint64_t write_kv_elapsed = 0;//kv的耗时
// uint64_t write_clean_elapsed = 0;//meta的耗时
// uint64_t write_bytes = 0;
// uint64_t write_step = 500 * 1024 * 1024;
@ -116,7 +116,7 @@ private:
// uint64_t temp_elapsed = 0;
// uint64_t waiting_elasped = 0;
// uint64_t waiting_elasped = 0;//
// inline void dumpStatistics() {
// if(count && count % 500000 == 0 || write_bytes && write_bytes > write_bytes_lim) {

+ 6
- 9
fielddb/request.cpp Ver arquivo

@ -59,13 +59,10 @@ void FieldsReq::ConstructBatch(WriteBatch &KVBatch,WriteBatch &IndexBatch,
//uint64_t start_ = DB->env_->NowMicros();
s = DB->kvDB_->Get(ReadOptions(), Key, &val_str);
//DB->construct_FieldsReq_Read_elapsed += DB->env_->NowMicros() - start_;
// FieldArray *oldFields;
FieldSliceArray oldFields;
if (s.IsNotFound()){
// oldFields = nullptr;
} else if (s.ok()) { //得到数据库之前key的fields, 判断需不需要删除其中潜在的索引
// oldFields = new FieldArray;
// oldFields = ParseValue(val_str,oldFields);
} else if (s.ok()) { //得到数据库之前key的fields, 判断需不需要删除其中潜在的索引
Slice nameSlice, valSlice;
Slice Value(val_str);
while(GetLengthPrefixedSlice(&Value, &nameSlice)) {
@ -73,6 +70,7 @@ void FieldsReq::ConstructBatch(WriteBatch &KVBatch,WriteBatch &IndexBatch,
oldFields.push_back({nameSlice,valSlice});
} else {
std::cout << "name and val not match! From FieldsReq Init" << std::endl;
assert(0);
}
nameSlice.clear(), valSlice.clear();
}
@ -130,7 +128,9 @@ void FieldsReq::ConstructBatch(WriteBatch &KVBatch,WriteBatch &IndexBatch,
if (HasOldIndex) {
for(auto &[field_name,field_value] : oldFields) {
if(field_name.data() == EMPTY) continue;
if(DB->index_.count(field_name.ToString())) {
if(DB->index_.count(field_name.ToString()) && //旧数据有,新数据没有的字段,删索引
std::find(SliceFields.begin(), SliceFields.end(),
std::make_pair(field_name, field_value)) == SliceFields.end()) {
std::string indexKey;
AppendIndexKey(&indexKey, ParsedInternalIndexKey(
Key,field_name,field_value));
@ -153,10 +153,7 @@ void FieldsReq::ConstructBatch(WriteBatch &KVBatch,WriteBatch &IndexBatch,
}
}
//优化:对于3.1,3.2中都有的索引只写一次
}
// if(oldFields) delete oldFields;
}
@ -173,7 +170,7 @@ void DeleteReq::ConstructBatch(WriteBatch &KVBatch,WriteBatch &IndexBatch,
//1. 读取当前的最新的键值对,判断是否存在含有键值对的field
//2.1 如果无,则正常构造delete
//2.2 如果是有的field的索引状态都是exist,则在meta中写KV_Deleting类型的记录
//在kvDB和metaDB中写入对应的delete
//在kvDB和indexDB中写入对应的delete
//2.3 如果存在field的索引状态是Creating或者Deleting,那么在那个队列上面进行等待
std::string val_str;
Status s = DB->kvDB_->Get(ReadOptions(), Key, &val_str);

+ 0
- 2
fielddb/request.h Ver arquivo

@ -104,8 +104,6 @@ public:
// std::string *Value;
// };
//TODO:Field什么的可能通过传引用的方式会更加好
//request
class iCreateReq : public Request {
public:

BIN
pics/basic.png Ver arquivo

Antes Depois
Largura: 986  |  Altura: 254  |  Tamanho: 24 KiB

BIN
pics/f_bsize.png Ver arquivo

Antes Depois
Largura: 936  |  Altura: 666  |  Tamanho: 34 KiB

BIN
pics/f_thread.png Ver arquivo

Antes Depois
Largura: 894  |  Altura: 674  |  Tamanho: 28 KiB

BIN
pics/field单.png Ver arquivo

Antes Depois
Largura: 1496  |  Altura: 1078  |  Tamanho: 195 KiB

BIN
pics/field双.png Ver arquivo

Antes Depois
Largura: 1475  |  Altura: 1072  |  Tamanho: 200 KiB

BIN
pics/fillbatch.png Ver arquivo

Antes Depois
Largura: 1273  |  Altura: 1152  |  Tamanho: 129 KiB

BIN
pics/level单.png Ver arquivo

Antes Depois
Largura: 1441  |  Altura: 415  |  Tamanho: 76 KiB

BIN
pics/level双.png Ver arquivo

Antes Depois
Largura: 1449  |  Altura: 421  |  Tamanho: 77 KiB

BIN
pics/level四.png Ver arquivo

Antes Depois
Largura: 1465  |  Altura: 426  |  Tamanho: 78 KiB

BIN
pics/parallel.png Ver arquivo

Antes Depois
Largura: 1032  |  Altura: 264  |  Tamanho: 25 KiB

BIN
pics/q&m_bsize.png Ver arquivo

Antes Depois
Largura: 902  |  Altura: 659  |  Tamanho: 36 KiB

BIN
pics/queue&mutex.png Ver arquivo

Antes Depois
Largura: 935  |  Altura: 671  |  Tamanho: 41 KiB

BIN
pics/reover.png Ver arquivo

Antes Depois
Largura: 1019  |  Altura: 268  |  Tamanho: 20 KiB

+ 4
- 0
test/helper.cc Ver arquivo

@ -84,6 +84,7 @@ void GetOneField(FieldDB *db, std::string key = "0") {
void InsertFieldData(FieldDB *db, int seed = 0/*随机种子*/) {
std::cout << "-------inserting-------" << std::endl;
// size_t writeSize = 0;
WriteOptions writeOptions;
int key_num = data_size / value_size;
// srand线程不安全,这种可以保证多线程时随机序列也一致
@ -94,6 +95,7 @@ void InsertFieldData(FieldDB *db, int seed = 0/*随机种子*/) {
//让批量写入的key>0, 单独写入的key<=0,方便测试观察
int key_ = std::abs(randThisTime) % key_num + 1;
std::string key = std::to_string(key_);
// std::string key = std::to_string(seed*key_num+i+1);
std::string name = "customer#" + std::to_string(key_);
std::string address = cities[randThisTime % cities.size()];
@ -111,7 +113,9 @@ void InsertFieldData(FieldDB *db, int seed = 0/*随机种子*/) {
}
Status s = db->PutFields(WriteOptions(), key, fields);
ASSERT_TRUE(s.ok());
// writeSize += key.size() + SerializeValue(fields).size();
}
// std::cout << writeSize << std::endl;
}
void DeleteFieldData(FieldDB *db, int seed = 0/*随机种子*/) {

+ 1
- 0
test/parallel_test.cc Ver arquivo

@ -48,6 +48,7 @@ TEST(TestReadPut, Parallel) {
GetFieldData(db, allowNotFound, 1);
findKeysByCity(db);
checkDataInKVAndIndex(db);
// db->CompactRange(nullptr, nullptr);
delete db;
}

Carregando…
Cancelar
Salvar