diff --git a/README.md b/README.md index ea0e2ee..d1eaf1a 100644 --- a/README.md +++ b/README.md @@ -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 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 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 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 indexKey; + AppendIndexKey(&indexKey, + ParsedInternalIndexKey(Slice(), field.first, field.second)); + Iterator *indexIterator = indexDB_->NewIterator(ReadOptions()); + indexIterator->Seek(indexKey); + + std::vector 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 deleted_kvlogs_; + std::vector new_kvlogs_; + /*****************************/ +} + +class VersionSet{ + /*****************************/ + void AddLiveKVLogs(std::set* live_kvlogs); + std::vector 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分离方式的最简要的介绍 diff --git a/benchmarks/db_bench.cc b/benchmarks/db_bench.cc index 554a6c5..ff93c64 100644 --- a/benchmarks/db_bench.cc +++ b/benchmarks/db_bench.cc @@ -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; diff --git a/benchmarks/db_bench_FieldDB.cc b/benchmarks/db_bench_FieldDB.cc index a061fcc..61a07a3 100644 --- a/benchmarks/db_bench_FieldDB.cc +++ b/benchmarks/db_bench_FieldDB.cc @@ -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); diff --git a/fielddb/field_db.cpp b/fielddb/field_db.cpp index 1e8cafe..f836556 100644 --- a/fielddb/field_db.cpp +++ b/fielddb/field_db.cpp @@ -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); } diff --git a/fielddb/field_db.h b/fielddb/field_db.h index ed9b87f..2b7f0d9 100644 --- a/fielddb/field_db.h +++ b/fielddb/field_db.h @@ -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) { diff --git a/fielddb/request.cpp b/fielddb/request.cpp index a614f7b..ea4987e 100644 --- a/fielddb/request.cpp +++ b/fielddb/request.cpp @@ -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); diff --git a/fielddb/request.h b/fielddb/request.h index b4847fe..8bfc23a 100644 --- a/fielddb/request.h +++ b/fielddb/request.h @@ -104,8 +104,6 @@ public: // std::string *Value; // }; -//TODO:下面的Field什么的可能通过传引用的方式会更加好? - //创建索引的request class iCreateReq : public Request { public: diff --git a/pics/basic.png b/pics/basic.png new file mode 100644 index 0000000..c07bbed Binary files /dev/null and b/pics/basic.png differ diff --git a/pics/f_bsize.png b/pics/f_bsize.png new file mode 100644 index 0000000..b1e1a4c Binary files /dev/null and b/pics/f_bsize.png differ diff --git a/pics/f_thread.png b/pics/f_thread.png new file mode 100644 index 0000000..195f509 Binary files /dev/null and b/pics/f_thread.png differ diff --git a/pics/field单.png b/pics/field单.png new file mode 100644 index 0000000..f1a2a67 Binary files /dev/null and b/pics/field单.png differ diff --git a/pics/field双.png b/pics/field双.png new file mode 100644 index 0000000..81eb29a Binary files /dev/null and b/pics/field双.png differ diff --git a/pics/fillbatch.png b/pics/fillbatch.png new file mode 100644 index 0000000..568a6ad Binary files /dev/null and b/pics/fillbatch.png differ diff --git a/pics/level单.png b/pics/level单.png new file mode 100644 index 0000000..0c397ce Binary files /dev/null and b/pics/level单.png differ diff --git a/pics/level双.png b/pics/level双.png new file mode 100644 index 0000000..42b2670 Binary files /dev/null and b/pics/level双.png differ diff --git a/pics/level四.png b/pics/level四.png new file mode 100644 index 0000000..503247b Binary files /dev/null and b/pics/level四.png differ diff --git a/pics/parallel.png b/pics/parallel.png new file mode 100644 index 0000000..db8e474 Binary files /dev/null and b/pics/parallel.png differ diff --git a/pics/q&m_bsize.png b/pics/q&m_bsize.png new file mode 100644 index 0000000..f51d1a0 Binary files /dev/null and b/pics/q&m_bsize.png differ diff --git a/pics/queue&mutex.png b/pics/queue&mutex.png new file mode 100644 index 0000000..a2e1706 Binary files /dev/null and b/pics/queue&mutex.png differ diff --git a/pics/reover.png b/pics/reover.png new file mode 100644 index 0000000..0a15575 Binary files /dev/null and b/pics/reover.png differ diff --git a/test/helper.cc b/test/helper.cc index 2ce5e2d..87a8833 100644 --- a/test/helper.cc +++ b/test/helper.cc @@ -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/*随机种子*/) { diff --git a/test/parallel_test.cc b/test/parallel_test.cc index 6406c16..0a6e763 100644 --- a/test/parallel_test.cc +++ b/test/parallel_test.cc @@ -48,6 +48,7 @@ TEST(TestReadPut, Parallel) { GetFieldData(db, allowNotFound, 1); findKeysByCity(db); checkDataInKVAndIndex(db); + // db->CompactRange(nullptr, nullptr); delete db; }