likes
comments
collection
share

对rocksdb 7.x的benchmark与bugfix讨论

作者站长头像
站长
· 阅读数 48

对rocksdb 7.x的benchmark与bugfix讨论

本文跟进了rocksdb 7.8对heavy write不时Stalling导致写QPS骤降为0的bugfix,本文既是一个编译rocksdb、如何使用db_bench的教程,也利用Excel可视化找出了影响Stalling根因几个参数。利用单元测试,验证了新版对target level size改动,进一步,在wiki和论文中,试图寻找旧版本设计的初衷。请关注原文公众号:mp.weixin.qq.com/s/A0g0_sWvO…

已有几个文章在讨论rocksdb 7.x做的一些性能上的优化,连我的老板都问了rocksdb 7.x的compaction stalling 优化怎么样,是否要跟进。然而rocksdb 7.x要求C++ 17+,这阻碍了我进行简单升级评估。rocksdb社区对7.x做了许多修改:string_view、clockcache,全局mutex锁粒度优化,stalling 优化等,未来,我将逐步跟进,评估。

编译rocksdb、db_bench

  • 编译librocksdb.a、编译db_bench。请注意,必须使用release编译以优化性能
git clone https://github.com/facebook/rocksdb.git
mkdir build&& cd build && cmake \
-DWITH_TESTS=0 \
-DCMAKE_BUILD_TYPE=Release \
-DWITH_BENCHMARK_TOOLS=1  ../ &&\
make -j10
  • 修改链接过程,执行下面的命令,重新编译db_bench,这里主要是为了静态链接必要的动态库,如snappy.a,gflags.a,rocksdb.a,这样我们在任何机器都可以直接跑db_bench
/usr/bin/c++  -W -Wextra -Wall -pthread -Wsign-compare -Wshadow\
   -Wno-unused-parameter -Wno-unused-variable -Woverloaded-virtual -Wnon-virtual-dtor -Wno-missing-field-initializers -Wno-strict-aliasing -Wno-invalid-offsetof -fno-omit-frame-pointer -momit-leaf-frame-pointer -march=native -Werror -fno-builtin-memcmp -O3 -DNDEBUG -fno-rtti   CMakeFiles/db_bench.dir/tools/simulated_hybrid_file_system.cc.o CMakeFiles/db_bench.dir/tools/db_bench.cc.o CMakeFiles/db_bench.dir/tools/db_bench_tool.cc.o  -o db_bench  -Wl,-rpath,/root/tmp/incubator-kvrocks/build/_deps/rocksdb-src/build_release\
   librocksdb.a \
   /usr/lib/x86_64-linux-gnu/libgflags.a \
   /usr/lib/x86_64-linux-gnu/libsnappy.a \
   -lpthread 
   mv db_bench db_bench_7.8
  • 填充数据,使用fillseq,请注意db=/data1/bench_7.x路径最好为SSD盘。
sudo ./db_bench_7.8 --benchmarks=fillseq \
--allow_concurrent_memtable_write=false --level0_file_num_compaction_trigger=4 --level0_slowdown_writes_trigger=20 --level0_stop_writes_trigger=30 --max_background_jobs=8 --max_write_buffer_number=8 --db=/data1/bench_7.x --wal_dir=/data1/bench_7.x --num=800000000 --num_levels=8 --key_size=20 --value_size=400 --block_size=8192 --cache_size=51539607552 --cache_numshardbits=6 --compression_max_dict_bytes=0 --compression_ratio=0.5 --compression_type=none --bytes_per_sync=8388608 --cache_index_and_filter_blocks=1 --cache_high_pri_pool_ratio=0.5 --benchmark_write_rate_limit=0 --write_buffer_size=16777216 --target_file_size_base=16777216 --max_bytes_for_level_base=67108864 --verify_checksum=1 --delete_obsolete_files_period_micros=62914560 --max_bytes_for_level_multiplier=8 --statistics=0 --stats_per_interval=1 --stats_interval_seconds=5 --report_interval_seconds=5 --histogram=1 --memtablerep=skip_list --bloom_bits=10 --open_files=-1 --subcompactions=1 --compaction_style=0 --min_level_to_compress=3 \
--level_compaction_dynamic_level_bytes=true \
--pin_l0_filter_and_index_blocks_in_cache=1 --soft_pending_compaction_bytes_limit=24696061952 --hard_pending_compaction_bytes_limit=49392123904 --min_level_to_compress=0 --use_existing_db=0 --sync=0 --threads=1 --memtablerep=vector --allow_concurrent_memtable_write=false --disable_wal=1 --seed=1642906118
  • 对上面的db overwrite,以此,我们的写入的数据的compaction有大量的重叠。
  sudo ./db_bench_7.8 --benchmarks=overwrite\
  --use_existing_db=1 \
  --sync=0 --level0_file_num_compaction_trigger=4 --level0_slowdown_writes_trigger=20 --level0_stop_writes_trigger=30 --max_background_jobs=8 --max_write_buffer_number=8 --db=/data1/bench_7.x --wal_dir=/data1/bench_7.x --num=800000000 --num_levels=8 --key_size=20 --value_size=400 --block_size=8192 --cache_size=51539607552 --cache_numshardbits=6 --compression_max_dict_bytes=0 --compression_ratio=0.5 --compression_type=none --bytes_per_sync=8388608 --cache_index_and_filter_blocks=1 --cache_high_pri_pool_ratio=0.5 --benchmark_write_rate_limit=0 --write_buffer_size=16777216 --target_file_size_base=16777216 --max_bytes_for_level_base=67108864 --verify_checksum=1 --delete_obsolete_files_period_micros=62914560 --max_bytes_for_level_multiplier=8 --statistics=0 --stats_per_interval=1 --stats_interval_seconds=5 --report_interval_seconds=5 --histogram=1 --memtablerep=skip_list --bloom_bits=10 --open_files=-1 --subcompactions=1 --compaction_style=0 --min_level_to_compress=3 \
  --level_compaction_dynamic_level_bytes=true \
  --pin_l0_filter_and_index_blocks_in_cache=1 --soft_pending_compaction_bytes_limit=24696061952 --hard_pending_compaction_bytes_limit=49392123904 --duration=3600 --threads=16 --merge_operator="put" --seed=1642907605 \
  --report_file=./benchmark_overwrite.7.8.csv
  • 使用6.28.4 rocksdb 重复上述行为

可视化qps

将上面得到的benchmark_overwrite.7.8.csv与benchmark_overwrite.6.24.csv使用Excel画图,请注意,X轴为第几秒,Y轴为QPS

对rocksdb 7.x的benchmark与bugfix讨论

对rocksdb 7.x的benchmark与bugfix讨论

请注意,如何你没有打开level_compaction_dynamic_level_bytes,升级rocksdb 7.x可能没有这么迫切,旧版本性能依旧。

对rocksdb 7.x的benchmark与bugfix讨论

Stalling的原因

在常规level_compaction_dynamic_level_bytes为true场景中,会根据max_bytes_for_level_base=1G以及最底层size重新计算target level size,得到[L2->1024MB,L3->1200MB,L4->12000MB,L5->120000MB],case 1如下:

#我写了个单测,测试version_set.cc::CalculateBaseBytes算法:
./version_set_test --gtest_filter=*.AlexDynamicLargeLevel --L0MB=0\
--L1MB=0 --L2MB=0 --L3MB=2048 --L4MB=15360 \
--L5MB=120000 --level_bytes_MB=1024
version_set.cc:3616 level 2==>1024MB
version_set.cc:3616 level 3==>1200MB
version_set.cc:3616 level 4==>12000MB
version_set.cc:3616 level 5==>120000MB
level_multiplier:10
sum level_max_bytes_:134224MB
l0_size:0
estimated_compaction_needed_bytes:42571MB

这很好,有效控制了空间放大率(原理请看下文)。

但当一个突发的写流量导致巨大的L0,将触发一个特殊模式(被称之为L0阻塞模式),每一层的target level size将根据此时巨大的L0重新计算,得到[L3->5120MB L4->24787.1MB L5->120000MB],level_multiplier也将会重新计算为4.84123(而不是根据option设置为10),请看case2。

对rocksdb 7.x的benchmark与bugfix讨论

对rocksdb 7.x的benchmark与bugfix讨论

case 2如下:

# 巨大的L0,--L0MB=5120
./version_set_test --gtest_filter=*.AlexDynamicLargeLevel \
--L0MB=5120 --L1MB=0 --L2MB=0 --L3MB=2048 \
--L4MB=15360 --L5MB=102400 \
--level_bytes_MB=1024
version_set.cc:3616 level 3==>5120MB
version_set.cc:3616 level 4==>24787.1MB
version_set.cc:3616 level 5==>120000MB
level_multiplier:4.84123,
sum level_max_bytes_:
149907MB,
l0_size:5120
estimated_compaction_needed_bytes_:13604.6MB

我们注意到,sum level_max_bytes(各个level总数据)没有多大变化,但target level size的巨大调整导致了estimated_compaction_needed_bytes_发生了巨大的变化,随着L0->L(N)的compaction完成,rocksdb将在上面case 2->case 1-> case 2中反复变化。

请注意,当L0=5120MB时,estimated_compaction_needed_bytes_被估算为13.6G,而当L0=0MB时(被compaction完毕了),estimated_compaction_needed_bytes_居然估算为:42.5G,这很好理解,case 2中,每一level都根据即将到来的sizeof(L0)做了自适应调整,待压缩的数据被控制到了最小,而后compaction完毕,L0=0,target level size被重新计算,而此时,每一层可能都将需要被compacton,以适应最新的target level size

我们可以将compaction 调度器的分数打印出来:其中,score >1才需要被调度。 case 2中:只有level 0的分数>1,需要被compaction。

idx:0==>level:0,score:4.47214
idx:1==>level:4,score:0.67082
idx:2==>level:3,score:0.4
idx:3==>level:1,score:0
idx:4==>level:2,score:0
idx:5==>level:0,score:0

case 1中:level 3 ,level 3都需要被compaction,所以pending compaction bytes非常大。

idx:0==>level:3,score:1.70667
idx:1==>level:4,score:1.28
idx:2==>level:2,score:0
idx:3==>level:0,score:0
idx:4==>level:1,score:0
idx:5==>level:0,score:0

请注意:pending compaction bytes是决定是否反向背压Stalling的主要指标,其调用路径为:

Status DBImpl::FlushMemTableToOutputFile(..)
 void DBImpl::InstallSuperVersionAndScheduleWork(..)
  void ColumnFamilyData::InstallSuperVersion(..)
   WriteStallCondition ColumnFamilyData::RecalculateWriteStallConditions(..){
     .....
    if (write_stall_condition == WriteStallCondition::kStopped &&
        write_stall_cause == WriteStallCause::kPendingCompactionBytes) {                
        write_controller_token_ = write_controller->GetStopToken();
        ROCKS_LOG_WARN(ioptions_.logger,"[%s] Stopping writes because of"
        "estimated pending compaction bytes %" PRIu64,name_.c_str(), compaction_needed_bytes);
}

至此,我们找到了Stalling的根因。

estimated_compaction_needed_bytes的计算方式:

estimated_compaction_needed_bytes的计算方式为每个level的pending compaction bytes求和 ,每个level的pending compaction bytes = (sizeof(level) - target_size(level)) X fanout。如下图:MaxBytesForLevel即level_max_bytes_[level],即target_size

对rocksdb 7.x的benchmark与bugfix讨论

issue 9423:There are too many write stalls because ...[1]做了大量的分析和验证工作。我们可以follow他的思路用Excel可视化验证一次。

QPS与estimated_compaction_needed_bytes变量关系

为了可视化QPS与estimated_compaction_needed_bytes的关系,对齐x轴时间坐标,我们需要修改db_bench源码:如下图在report头上加入date_time,这样我们就有了物理时间,请注意,这里的qps是5s间隔。

对rocksdb 7.x的benchmark与bugfix讨论

接下来,在version_set.cc::ComputeCompactionScore即2700行加入下面代码:目的是5s打印一次estimated_compaction_needed_bytes_,我们无需考虑代码质量和线程安全,因为这只是一次验证工作。

static auto curr_log_ts = immutable_options.env->NowMicros();
if(immutable_options.env->NowMicros() - curr_log_ts > 5000000){
 curr_log_ts =  immutable_options.env->NowMicros();
 ROCKS_LOG_INFO(immutable_options.logger,"estimated_compaction_needed_bytes_:“
 “%f MB",estimated_compaction_needed_bytes_/1024.0/1024.0);
 }

version_set.cc3600行加入下面代码:目的是打印L0在某次compaction后的size,这里都是简单5s打印一次。

static auto curr_log_ts = ioptions.env->NowMicros();
if(ioptions.env->NowMicros() - curr_log_ts > 5000000){
  curr_log_ts =  ioptions.env->NowMicros();
  ROCKS_LOG_INFO(ioptions.logger,"level_multiplier_:%f,"
  "sum level_max_bytes_:%f MB,l0_size:%f ",
  level_multiplier_,sum_size/1024.0/1024.0,l0_size/1024.0/1024.0);
  }

现在,我们得到了X轴时间对齐的多个变量,如下图,estimated_compaction_bytes的剧烈变化(增大),导致了QPS骤降为0:

对rocksdb 7.x的benchmark与bugfix讨论

进一步,我们把L0 size与QPS关联起来:如下图,每当巨大的L0 size被compact下去(case 1),size为0时,我们也遇到了write stall。

对rocksdb 7.x的benchmark与bugfix讨论

新版的修改

PR10057[2]负责了本次重构,但rocksdb wiki并未更新本次改动。正如下面代码所示,level_multiplier_ base_bytes_max不再改变,完全抛弃了旧版本的L0特殊模式(其目标是应对write burst),level_multiplier_不变,L1 size不再由L0改变,而后target level size将不再会剧烈变化了。

对rocksdb 7.x的benchmark与bugfix讨论

至此:pending compaction bytes的计算也稳定下来。这个PR也对compaction score 做了调整,L0的分数被放大了10倍,以及对下层的compaction score也有变更:

Siying: I hope this proposal can handle bursts of writes a little bit. If there are a burst of writes coming from L0, upper level compactions are taking lower priority compared to lower levels, relative to previously.

以及变更如下:

Siying: Once we have adjusted level targets, some levels won't qualify for compactions any more. For example, consider following size per level:

L0: 5GB
L1: 200 MB (unadjusted target 100MB)
L2: 2 GB (unadjusted target 1GB)
L3: 15 GB (unadjusted target 10GB)
L4: 100 GB (unadjusted target 100GB)

All levels would qualify for compaction before adjusting level size, so some L2->L3 and L3->L4 compactions would happen while L0->L1 is happening. However, with adjusted level sizing, the target would look like this:

L0: 5GB
L1: 200 MB (adjusted target 5 GB)
L2: 2 GB (adjusted target 13.6 GB)
L3: 15 GB (adjusted target 36.8 GB)
L4: 100 GB (adjusted target 100GB)

and only L0->L1 compaction will be going on and all other levels' compactions will be on hold. With this change, L3->L4 will also happen if there are free compaction slots.

我这段看了许久,做了个测试,我猜作者应该是在说本次PR中,将adjusted target 变成了unadjusted target(去掉了特殊模式),compaction分数需要重新设计,否则, so some L2->L3 and L3->L4 compactions would happen while L0->L1 is happening.虽然旧版本特殊模式可以应对这个问题,但却带来了Stalling的bug!所以本次的分数调整是,L0 compaction完毕后,L3->L4也将被触发,而后是L2->L3,这样调度compaction的好处是,减少后面L2 compaction的扇出。(如有误请指正)

#rocksdb 6.28 with adjusted target
./version_set_test --num_levels=5 --gtest_filter=*.AlexDynamicLargeLevel \
--L0MB=5120 --L1MB=200 --L2MB=2048 \
--L3MB=15360 --L4MB=102400 --L5MB=0 \
--level_bytes_MB=1024
level i:1==>5120MB
level i:2==>13897.8MB
level i:3==>37724.5MB
level i:4==>102400MB
CompactionScoreLevel:
idx:0==>level:0,score:2.71442
idx:1==>level:3,score:0.407163
idx:2==>level:2,score:0.147361
idx:3==>level:1,score:0.0390625

#rocksdb 7.8 with no adjusted target but adjusted score:
CalculateBaseBytes:
level i:1===>1024MB,level_size:102.4
level i:2===>1024MB,level_size:1024
level i:3===>10240MB,level_size:10240
level i:4===>102400MB,level_size:102400
CompactionScoreLevel :
#L0->L1 compaction完毕后,L3->4先触发
idx:0==>level:0,score:50
idx:1==>level:3,score:9.375  
idx:2==>level:2,score:3.33333
idx:3==>level:1,score:0.195312
idx:4==>level:0,score:0

利用动态target level size减少空间放大和写放大

所谓targe size,即LSM树中每一level的容量,我们想知道,为何rocksdb设计出动态target level size这种模式?

理想情况下,这是一个正三角形的树:

对rocksdb 7.x的benchmark与bugfix讨论

target_size大小如下:

  • • L1 的目标大小是 max_bytes_for_level_base
  • • Ln 的目标大小计算方法为:Target_Size(Ln+1) = Target_Size(Ln) * max_bytes_for_level_multiplier * max_bytes_for_level_multiplier_additional[n]max_bytes_for_level_multiplier_additional 默认都是 1。

例如,如果 设置max_bytes_for_level_base =1G max_bytes_for_level_multiplier = 10max_bytes_for_level_multiplier_additional 未设置(即全都为 1),那么 L1、L2、L3、L4 的目标大小分别为 1GB、10GB、100GB、1000GB。

在LSM树中,空间放大=size_on_file_system/size_of_user_data,rocksdb提供了一个估算理想空间放大率的算法:每一层的实际size之和/最后一层的size,上例中,空间放大率约为 (1000GB + 100GB + 10GB + 1GB) / 1000GB = 1.111,这是一个理想的空间放大率,但在现实中是很难做到的,例如用户真实数据110G,某次写完全量数据后,执行一次full compaction,数据变为:此时空间放大为0,

L1:0G
L2:0G
L3:0G 
L4:110G

第二天,再次导入全量数据,此时每层大小为:

L1:1G
L2:10G
L3:99G 
L4:110G

实际空间放大将为 (110+99+10+1)/110 = 2,这是一个很糟糕的数字:实际存储了多1倍的数据,Auto Compaction对此无能为力,因为每一层都不超过该层的目标容量(target level size)(1GB、10GB、100GB、1000GB)。根因是max_bytes_for_level_base 不契合真实数据size,导致计算的target level size过大,无法触发AutoCompaction。

为了解决空间放大问题,Siying发表了一篇论文《Optimizing Space Amplification in RocksDB》,在rocksdb 引入了level_compaction_dynamic_level_bytes以动态调整target level size:

对rocksdb 7.x的benchmark与bugfix讨论

初始化时,base_level从L1变成L6,即L0首先被compact到L6,例如num_level=6,max_bytes_for_level_base=1G,某次compaction后,L6 size 为2G,超过了max_bytes_for_level_base,于是base_level--,L5上限为2G/10 = 200M,L0 compact到L5,而后L5=300M,需要将L5+L6 compact,此时若L6 size 为2300M,重新调整L5上限230M,随着持续的写入,L5终会大过max_bytes_for_level_base1G,此时我们需要继续base_level--,到L4,其上限为L5新的上限1/10。这很好地控制了空间放大。

请注意,level_compaction_dynamic_level_bytes选项中,不仅是优化空间放大,根据LSM理论,level_multiplier越大,空间放大和读放大则越小,但是写放大会更高,level_compaction_dynamic_level_bytes动态减小了level_multiplier,其目标原来是写放大收益啊。

引用链接

[1] issue 9423:There are too many write stalls because ...: github.com/facebook/ro… [2] PR10057: github.com/facebook/ro…

转载自:https://juejin.cn/post/7194404671014830117
评论
请登录