Browse Source

finish benchmark report

main
wesley 6 months ago
parent
commit
824f0600a0
11 changed files with 136 additions and 34 deletions
  1. +105
    -3
      README.md
  2. BIN
      assets/Insert_static_vsize.png
  3. BIN
      assets/db_bench_1.jpg
  4. BIN
      assets/db_bench_2.jpg
  5. BIN
      assets/db_bench_new.png
  6. BIN
      assets/randvalue.png
  7. BIN
      assets/writeampli.png
  8. +3
    -5
      benchmarks/db_bench_new.cc
  9. +1
    -1
      include/leveldb/options.h
  10. +23
    -21
      test/test_bench.cc
  11. +4
    -4
      test/test_bench_gc.cc

+ 105
- 3
README.md View File

@ -545,7 +545,7 @@ GC是KV分离中的一个非常重要的问题,虽然KV分离的功能显著
综上,我们选择使用新的GC方法,并提供config中配置执行Level Merge的层数
- **GC Overview: **
- **GC Overview:**
![gc](./assets/gc.png)
@ -561,13 +561,115 @@ GC是KV分离中的一个非常重要的问题,虽然KV分离的功能显著
## 4. 实验
#### 4.1 正确性实验:
### 4.1 正确性实验:
对KV分离改造后的数据库的正确性进行实验,主要测试 `Fields` 功能和 `leveldb` 的基本功能,即写入,点查询和范围查询等
正确性实验代码位于 `test/*` 目录下,`test_fileds.cc` 验证了 `Fields` 功能的正确性,`test_basicio.cc` 验证了 `leveldb` 的基本功能的正确性,连续多次执行该测试,也能够完成数据库的重启,并且在无效数据大小超过预设 GC 大小后完成了 GC ,通过监控数据库文件夹大小即可发现GC能让数据库大小稳定在一定范围以下 (该测试key范围固定,因此多次插入无效数据必然增多)
#### 4.2 **BenchMark:**
### 4.2 **BenchMark:** v
#### 4.2.1 测试文件及使用
+ 本次实验中用于性能测试的文件有两个:
+ `benchmarks/db_bench_new.cc`:此文件基于`Leveldb`原有的`db_bench.cc`,对其进行了修改和扩写,使其能够胜任实现了字段设计和KV分离后的本实验数据库。对部分重要的、可调整的参数说明:
```c++
// 该字符串规定正式运行本文件时使用的性能测试方法
// 可在Benchmark类定义的方法中选择需要的,填写在这个字符串中
static const char* FLAGS_benchmarks;
// 运行性能测试所使用的K/V对数量,按需修改
static int FLAGS_num = 1000000;
// 规定了每个存储对象的value的大小
// 由于本实验实现的KV分离功能通过value大小判断是否需要分离
// 可以通过修改这个参数开启或关闭KV分离功能的性能测试
static int FLAGS_value_size = 1000;
```
+ `test/test_bench.cc`:由于`db_bench_new.cc`文件是进行整体测试的,对于单个操作和功能的测试并不方便,因此我们同时编写了这个文件,以便于单独测试`Put(), Get(), Iterator(), FindKeysByField()`等操作的性能,包括了吞吐量和延迟,其中对于延迟的测试同时可以输出平均延迟、P75延迟和P99延迟。在不进行修改直接运行`test_bench.cc`的情况下,能够输出以上所有信息。
+ **注意:** 两个文件的`benchmark`测出的性能都是实机性能,会受到机型配置影响,且在同一台主机运行时也会受到实时性能影响。下图是一张成功运行`db_bench_new.cc`的截图,以此为演示样例,本小组进行性能测试的虚拟机配置也如图中信息所示:
![db_bench_new](/assets/db_bench_new.png)
+ 为了尽可能保证测试的准确性,下文所展示的性能测试的结果均是在相同条件下重复运行几十次`benchmark`后取平均值所得。
#### 4.2.2 性能预期
本次实验中要通过`benchmark`检测的功能主要有:
+ `Put(), Get(), Iterator(), FindKeysByField()`等基本功能的吞吐量(Throughput)和延迟(Latency);
+ KV分离功能对读写性能的影响;
+ 后台GC是否成功,对性能产生的影响;
在正式开始性能测试前,应当先分析预测,有的放矢,与实机效果对比,判断功能是否符合预期。
1. `Put(), Get(), Iter()`分别是写入数据和点查询、范围查询读取数据的基本功能函数,这些操作的吞吐量直接反映了数据库的读写速度。
2. `FindKeysByField()`是在实现字段设计功能后添加的函数。为了保证查准率和查全率,该函数用`iterator`依序遍历了整个数据库,其运行速度必然很慢。若想提升其性能,应该考虑二级索引相关的设计。故在本小组的性能测试中,此功能的性能仅作为了解参考,而非检验KV分离性能的标准。
3. KV分离功能会显著地提高写入数据的性能,代价是牺牲一部分读取数据的性能。同时,KV分离后的数据库在合并(Compact)时的性能应该大幅提升,且写放大的开销会有所减少。
4. 后台GC会清除过期的或遭遗弃的VTable,减轻数据库的负载。同时GC过程中,读写等操作会受到阻塞,性能略微下降。而且由于本小组实现的GC只有在`level`达到最高两层时才会生成新的VTable,而这种情况很少发生,因此产生的额外写放大开销几乎可以忽略不计。
#### 4.2.3 性能测试
##### Part. A 通过db_bench观察整体性能
`LevelDB`自带的`db_bench`可以统括地展示数据库的整体性能。这张图是仅仅实现了字段设计的数据库性能:
![bench_1](/assets/db_bench_1.jpg)
下面这张图是实现了完整的KV分离功能的数据库的性能:
![bench_2](/assets/db_bench_2.jpg)
可以明显地看出来,写入数据的性能(`fillseq, fillrandom`)提升了将近两倍,而合并(compact)的耗时缩短了十倍以上。这与上一部分中我们的预期基本吻合,说明从这个角度看KV分离是成功地提升了数据库的性能。
使用`test_bench.cc`则可以直接测出读、写、扫描等操作的性能。不过,只有通过不同情境下的性能比较才能体现出我们实现的KV分离功能的作用。以下是我预设的几个可以体现性能差异的场景。
##### Part. B 面对不同的value_size时数据库的性能,以及GC对性能的影响
在这一部分,我们将数据库根据功能的完善程度不同分为三个版本:实现了KV分离的数据库,触发了GC;KV分离但不触发GC的数据库;仅有字段设计的原LevelDB。
在实际操作时,我们的版本控制实际上只需要分为功能完善的KV分离数据库和没有KV分离功能的LevelDB。对于是否使用新GC,可以在`include/leveldb/options.h`中修改`gc_size_threshold`这一变量的大小。该值越小,后台自动GC越容易被触发;反之,将其调到极大时,就相当于禁用了新GC。通过控制GC是否触发,可以大致测得GC对数据库性能的影响。
使用`test_bench.cc`文件,将全局变量的`value_size_`修改为不同的值,重复测试。这里选择的是50,500,1000,2000。这里设定的KV分离阈值是1000,即对于`value_size`超过1000的文件对象才使用KV分离。
为了直观展现KV分离的性能,这里选择用测得的在不同的`value_size`情况下,`Put()`操作的吞吐量的差异,绘图表现。这里要说明的是,测试脚本`test_bench.cc`里通过`Put()`实现的是随机写功能,因此可以直观反映数据库写性能的提高:
![insert](/assets/Insert_static_vsize.png)
类似地,使用`test_bench.cc`可以测得`Put()`的延迟,以及其他操作的吞吐量和延迟。这里不一一作图展示。
##### Part. C 面对不固定的value_size时数据库的性能
在数据库的实际应用场景中,通常来说不可能遇到`value_size`不变的数据。本小组实现的KV分离数据库在面对`value_size`比较小的数据时不会使用KV分离功能,这是考虑到小数据的写入、合并开销并不大,贸然分离存储反而可能由于读取性能降低导致数据库整体性能受影响。能够灵活面对实际应用场景中不同大小的数据,正是这种设计思路的初衷。
我在`test_bench.cc`插入数据的函数`InsertData()`中加入了随机数,用来随机生成不同大小的`value_size`。
需要注意的是:
+ 由于`InsertData()`本身是随机插入,其`key`值也是个随机数,在生成这两个随机数的时候需要用到不同的随机数生成函数。
+ 测试不同版本数据库时需要控制变量,因此需要控制不同测试时,随机数的种子保持不变。
在这次性能测试中,我们设定的`value_size`的随机范围是`500~2048`,预设的KV分离阈值是1000,正好可以模拟实际应用情况。同时,我们还是设定了对于全部数据都使用KV分离和完全不使用KV分离的两种数据库作为对照组。
![randvsizet](/assets/randvalue.png)
Latency等数据不便于在图中展示,这里省略。
##### Part. D 不同数据量的写放大开销
写放大 = 输入磁盘的数据总量 / 实际数据量。
在`test_bench`中,我加入了一个全局变量`bytes_`用于监控文件大小,可以通过调用它来输出;然后在每次运行`test_bench`后,生成的LOG文件中有每次合并产生的新文件的大小信息。将LOG文件中所有写入文件的大小相加,再与`bytes_`相比,可以算出写放大这个比值。
通过调整`value_size`,我发现写放大与`value_size`有一定相关性,且实现KV分离的数据库写放大明显减少。
![writeampli](/assets/writeampli.png)
##### Part. E 其他指标
关于其他操作的延迟、吞吐量可以运行`test_bench`直接输出。
对于GC的持续时间,我在实施GC的函数里做了标记,可以输出GC函数的持续时间。通过大量的测试,算出GC持续时间的平均值在600微妙-1000微秒之间,而且似乎和`value_size`等关系不大,没有直接影响因素,因此这里给出一个范围而非准确值。
## 5. 可能的优化空间

BIN
assets/Insert_static_vsize.png View File

Before After
Width: 568  |  Height: 453  |  Size: 40 KiB

BIN
assets/db_bench_1.jpg View File

Before After
Width: 1112  |  Height: 862  |  Size: 194 KiB

BIN
assets/db_bench_2.jpg View File

Before After
Width: 996  |  Height: 850  |  Size: 206 KiB

BIN
assets/db_bench_new.png View File

Before After
Width: 564  |  Height: 527  |  Size: 23 KiB

BIN
assets/randvalue.png View File

Before After
Width: 1348  |  Height: 818  |  Size: 63 KiB

BIN
assets/writeampli.png View File

Before After
Width: 1345  |  Height: 868  |  Size: 60 KiB

+ 3
- 5
benchmarks/db_bench_new.cc View File

@ -44,7 +44,7 @@
// stats -- Print DB stats
// sstables -- Print sstable info
// heapprofile -- Dump a heap profile (if supported by this port)
static const char* FLAGS_benchmarks = // TODO: findkeysbyfield()
static const char* FLAGS_benchmarks =
"fillseq,"
"fillsync,"
"fillrandom,"
@ -454,7 +454,7 @@ class Benchmark {
int heap_counter_;
CountComparator count_comparator_;
int total_thread_count_;
int num_target_; //TODO: the num of target fields, for findkeybyfield use
int num_target_;
void PrintHeader() {
const int kKeySize = 16 + FLAGS_key_prefix;
@ -636,7 +636,6 @@ class Benchmark {
fresh_db = true;
method = &Benchmark::WriteTargetRandom;
} else if (name == Slice("findkeysbyfield")) {
// TODO: findkeysbyfield
method = &Benchmark::FindKeysByField;
} else if (name == Slice("seekrandom")) {
method = &Benchmark::SeekRandom;
@ -1033,9 +1032,8 @@ class Benchmark {
thread->stats.AddBytes(bytes);
}
// Used for FindKeysByField test
void FindKeysByField(ThreadState* thread){
// TODO
int found = 0;
Field target = {"target", "given_field"};

+ 1
- 1
include/leveldb/options.h View File

@ -118,7 +118,7 @@ struct LEVELDB_EXPORT Options {
// initially populating a large database.
size_t max_file_size = 2 * 1024 * 1024;
size_t gc_size_threshold = 100 * 1024 * 1024;
size_t gc_size_threshold = 1024 * 1024 * 1024;
// Compress blocks using the specified compression algorithm. This
// parameter can be changed dynamically.

+ 23
- 21
test/test_bench.cc View File

@ -11,7 +11,7 @@ using namespace leveldb;
// Number of key/values to operate in database
constexpr int num_ = 100000;
// Size of each value
constexpr int value_size_ = 2000;
constexpr int value_size_ = 500;
// Number of read operations
constexpr int reads_ = 100000;
// Number of findkeysbyfield operations
@ -148,6 +148,18 @@ void SearchField(DB *db, std::vector &lats) {
}
}
// Insert many k/vs in order to start background GC
void InsertMany(DB *db) {
std::vector<int64_t> lats;
for (int i = 0; i < 2; i++) {
InsertData(db, lats);
GetData(db, lats);
db->CompactRange(nullptr, nullptr);
std::cout << "put and get " << i << " of Many" << std::endl;
}
}
double CalculatePercentile(const std::vector<int64_t>& latencies, double percentile) {
if (latencies.empty()) return 0.0;
@ -167,6 +179,8 @@ TEST(TestBench, Throughput) {
abort();
}
// InsertMany(db);
std::vector<int64_t> lats;
// Put()
@ -175,27 +189,27 @@ TEST(TestBench, Throughput) {
auto end_time = std::chrono::steady_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end_time - start_time).count();
// std::cout << "Throughput of Put(): " << std::fixed << num_ * 1e6 / duration << " ops/s" << std::endl;
std::cout << "Throughput of Put(): " << std::fixed << std::setprecision(2) << std::endl << (bytes_ / 1048576.0) / (duration * 1e-6) << std::endl; // << " MB/s" << std::endl << std::endl;
std::cout << "Throughput of Put(): " << std::fixed << std::setprecision(2) << std::endl << (bytes_ / 1048576.0) / (duration * 1e-6) << " MB/s" << std::endl << std::endl;
// Get()
start_time = std::chrono::steady_clock::now();
GetData(db, lats);
end_time = std::chrono::steady_clock::now();
duration = std::chrono::duration_cast<std::chrono::microseconds>(end_time - start_time).count();
// std::cout << "Throughput of Get(): " << std::fixed << reads_ * 1e6 / duration << " ops/s" << std::endl;
std::cout << "Throughput of Get(): " << std::fixed << std::setprecision(2) << std::endl << (bytes_ / 1048576.0) / (duration * 1e-6) << std::endl; // << " MB/s" << std::endl << std::endl;
std::cout << "Throughput of Get(): " << std::fixed << std::setprecision(2) << std::endl << (bytes_ / 1048576.0) / (duration * 1e-6) << " MB/s" << std::endl << std::endl;
// Iterator()
start_time = std::chrono::steady_clock::now();
ReadOrdered(db, lats);
end_time = std::chrono::steady_clock::now();
duration = std::chrono::duration_cast<std::chrono::microseconds>(end_time - start_time).count();
// std::cout << "Throughput of Iterator(): " << std::fixed << reads_ * 1e6 / duration << " ops/s" << std::endl;
std::cout << "Throughput of Iterator(): " << std::fixed << std::setprecision(2) << std::endl << (bytes_ / 1048576.0) / (duration * 1e-6) << std::endl; // << " MB/s" << std::endl << std::endl;
std::cout << "Throughput of Iterator(): " << std::fixed << std::setprecision(2) << std::endl << (bytes_ / 1048576.0) / (duration * 1e-6) << " MB/s" << std::endl << std::endl;
// FindKeysbyField()
start_time = std::chrono::steady_clock::now();
SearchField(db, lats);
end_time = std::chrono::steady_clock::now();
duration = std::chrono::duration_cast<std::chrono::microseconds>(end_time - start_time).count();
std::cout << "Throughput of FindKeysbyField(): " << std::fixed << std::setprecision(2) << std::endl << search_ * 1e6 / duration << std::endl; // << " ops/s" << std::endl << std::endl;
std::cout << "Throughput of FindKeysbyField(): " << std::fixed << std::setprecision(2) << std::endl << search_ * 1e6 / duration << " ops/s" << std::endl << std::endl;
delete db;
}
@ -229,40 +243,28 @@ TEST(TestBench, Latency) {
double put_avg = std::get<0>(put_latency);
double put_p75 = std::get<1>(put_latency);
double put_p99 = std::get<2>(put_latency);
std::cout << "Put Latency (avg, P75, P99): " << std::fixed << std::endl << std::setprecision(2) << put_avg * 1e-3 // << " micros/op, "
<< std::endl << put_p75 * 1e-3 // << " micros/op, "
<< std::endl << put_p99 * 1e-3 //<< " micros/op"
<< std::endl << std::endl;
std::cout << "Put Latency (avg, P75, P99): " << std::fixed << std::endl << std::setprecision(2) << put_avg * 1e-3 << " micros/op, " << put_p75 * 1e-3 << " micros/op, " << put_p99 * 1e-3 << " micros/op" << std::endl << std::endl;
GetData(db, get_lats);
std::tuple<double, double, double> get_latency = calc_lat(get_lats);
double get_avg = std::get<0>(get_latency);
double get_p75 = std::get<1>(get_latency);
double get_p99 = std::get<2>(get_latency);
std::cout << "Put Latency (avg, P75, P99): " << std::fixed << std::endl << std::setprecision(2) << get_avg * 1e-3 // << " micros/op, "
<< std::endl << get_p75 * 1e-3 // << " micros/op, "
<< std::endl << get_p99 * 1e-3 //<< " micros/op"
<< std::endl << std::endl;
std::cout << "Get Latency (avg, P75, P99): " << std::fixed << std::endl << std::setprecision(2) << get_avg * 1e-3 << " micros/op, " << get_p75 * 1e-3 << " micros/op, " << get_p99 * 1e-3 << " micros/op" << std::endl << std::endl;
ReadOrdered(db, iter_lats);
std::tuple<double, double, double> iter_latency = calc_lat(iter_lats);
double iter_avg = std::get<0>(iter_latency);
double iter_p75 = std::get<1>(iter_latency);
double iter_p99 = std::get<2>(iter_latency);
std::cout << "Put Latency (avg, P75, P99): " << std::fixed << std::endl << std::setprecision(2) << iter_avg * 1e-3 // << " micros/op, "
<< std::endl << iter_p75 * 1e-3 // << " micros/op, "
<< std::endl << iter_p99 * 1e-3 //<< " micros/op"
<< std::endl << std::endl;
std::cout << "Iterator Latency (avg, P75, P99): " << std::fixed << std::endl << std::setprecision(2) << iter_avg * 1e-3 << " micros/op, " << iter_p75 * 1e-3 << " micros/op, " << iter_p99 * 1e-3 << " micros/op" << std::endl << std::endl;
SearchField(db, search_lats);
std::tuple<double, double, double> search_latency = calc_lat(search_lats);
double search_avg = std::get<0>(search_latency);
double search_p75 = std::get<1>(search_latency);
double search_p99 = std::get<2>(search_latency);
std::cout << "Put Latency (avg, P75, P99): " << std::fixed << std::endl << std::setprecision(2) << search_avg * 1e-3 // << " micros/op, "
<< std::endl << search_p75 * 1e-3 // << " micros/op, "
<< std::endl << search_p99 * 1e-3 //<< " micros/op"
<< std::endl << std::endl;
std::cout << "FindKeysByField Latency (avg, P75, P99): " << std::fixed << std::endl << std::setprecision(2) << search_avg * 1e-3 << " micros/op, " << search_p75 * 1e-3 << " micros/op, " << search_p99 * 1e-3 << " micros/op" << std::endl << std::endl;
delete db;

+ 4
- 4
test/test_bench_gc.cc View File

@ -11,7 +11,7 @@ using namespace leveldb;
// Number of key/values to operate in database
constexpr int num_ = 100000;
// Size of each value
constexpr int value_size_ = 2000;
constexpr int value_size_ = 500;
// Number of read operations
constexpr int reads_ = 100000;
// Number of findkeysbyfield operations
@ -32,8 +32,8 @@ void InsertData(DB *db, std::vector &lats) {
bytes_ = 0;
int64_t bytes = 0;
srand(0);
std::mt19937 value_seed(100);
std::uniform_int_distribution<int> value_range(500, 2048);
// std::mt19937 value_seed(100);
// std::uniform_int_distribution<int> value_range(500, 2048);
int64_t latency = 0;
auto end_time = std::chrono::steady_clock::now();
auto last_time = end_time;
@ -41,7 +41,7 @@ void InsertData(DB *db, std::vector &lats) {
for (int i = 0; i < num_; i++) {
int key_ = rand() % num_+1;
int value_ = std::rand() % (num_ + 1);
int value_size = value_range(value_seed);
// int value_size = value_range(value_seed);
std::string value(value_size_, 'a');
std::string key = std::to_string(key_);
FieldArray field_array = {

Loading…
Cancel
Save