做者:吴毅 王远立html
TiKV 底层使用了 RocksDB 做为存储引擎,然而 RocksDB 配置选项不少,不少状况下只能经过反复测试或者依靠经验来调优,甚至连 RocksDB 的开发者都自嘲,他们没办法弄清楚每一个参数调整对性能的影响。若是有一个自动 tuning 的方案就能够大大减小调优的人力成本,同时也可能在调优的过程当中,发现一些人工想不到的信息。咱们从 AutoML 中获得启发,但愿能用 Automated Hyper-parameter Tuning 中的一些方法来对数据库参数进行自动调优。git
经常使用的 Automated Hyper-parameter Tuning 方式大致上有如下三种:github
目前学术界针对 auto-tune 数据库的研究也有不少,采用的方法大多集中在后面两种。其中一个比较有名的研究是 OtterTune 。咱们受 OtterTune 的启发,开发了 AutoTiKV,一个用于对 TiKV 数据库进行自动调优的工具。项目启动三个月以来,AutoTiKV 在 TiKV 内部测试和调参的环节起到了较好的效果,有了一个很好的开始。后续咱们还会针对生产环境上的一些特色,对它进行继续探索和完善。算法
项目地址:https://github.com/tikv/auto-tikv数据库
整个调优过程大体以下图:数组
整个过程会循环跑 200 个 round(能够用户自定义),或者也能够定义成到结果收敛为止。缓存
AutoTiKV 支持在修改参数以后重启 TiKV(若是不须要也能够选择不重启)。须要调节的参数和须要查看的 metric 能够在 controller.py 里声明。服务器
一开始的 10 轮(具体大小能够调节)是用随机生成的 knob 去 benchmark,以便收集初始数据集。以后的都是用 ML 模型推荐的参数去 benchmark。网络
AutoTiKV 使用了和 OtterTune 同样的高斯过程回归(Gaussian Process Regression,如下简称 GP)来推荐新的 knob[1],它是基于高斯分布的一种非参数模型。高斯过程回归的好处是:session
X
的均值 m(X)
和标准差 s(X)
。若 X
周围的数据很少,则它被估计出的标准差 s(X)
会偏大(表示这个样本 X
和其余数据点的差别大)。直观的理解是若数据很少,则不肯定性会大,体如今标准差偏大。反之,数据足够时,不肯定性减小,标准差会偏小。这个特性后面会用到。但 GP 自己其实只能估计样本的分布,为了获得最终的预测值,咱们须要把它应用到贝叶斯优化(Bayesian Optimization)中。贝叶斯优化算法大体可分为两步:
采集函数(Acquisition Function)的做用是:在寻找新的推荐值的时候,平衡探索(exploration)和利用(exploitation)两个性质:
在推荐的过程当中,须要平衡上述两种指标。exploitation 过多会致使结果陷入局部最优值(重复推荐目前已知的最好的点,但可能还有更好的点没被发现),而 exploration 过多又会致使搜索效率过低(一直在探索新区域,而没有对当前比较好的区域进行深刻尝试)。而平衡两者的核心思想是:当数据足够多时,利用现有的数据推荐;当缺乏数据时,咱们在点最少的区域进行探索,探索最未知的区域能给咱们最大的信息量。
贝叶斯优化的第二步就能够帮咱们实现这一思想。前面提到 GP 能够帮咱们估计 X
的均值 m(X)
和标准差 s(X)
,其中均值 m(x)
能够做为 exploitation 的表征值,而标准差 s(x)
能够做为 exploration 的表征值。这样就能够用贝叶斯优化方法来求解了。
使用置信区间上界(Upper Confidence Bound)做为采集函数。假设咱们须要找 X
使 Y
值尽量大,则 U(X) = m(X) + k*s(X)
,其中 k > 0
是可调的系数。咱们只要找 X
使 U(X)
尽量大便可。
U(X)
大,则可能 m(X)
大,也可能 s(X)
大。s(X)
大,则说明 X
周围数据很少,须要探索未知区域新的点。m(X)
大,说明估计的 Y
值均值大, 则须要利用已知数据找到效果好的点。k
影响着探索和利用的比例,k
越大,越鼓励探索新的区域。在具体实现中,一开始随机生成若干个 candidate knobs,而后用上述模型计算出它们的 U(X)
,找出 U(X)
最大的那一个做为本次推荐的结果。
测试中咱们使用了 YCSB 来模拟 write heavy、long range scan、short range scan 和 point-lookup 四种典型 workload。数据库大小都是 80GB。[2]
咱们试验了以下参数:
Options | Expected behavior | valid range/value set |
---|---|---|
write-buffer-size | point-lookup, range-scan: larger the better | [64MB, 1GB] |
max-bytes-for-level-base | point-lookup, range-scan: larger the better | [512MB, 4GB] |
target-file-size-base | point-lookup, range-scan: larger the better | {8M, 16M, 32M, 64M, 128M} |
disable-auto-compactions | write-heavy: turn on is better point-lookup, range-scan: turn off is better | {1, 0} |
block-size | point-lookup: smaller the better, range-scan: larger the better | {4k,8k,16k,32k,64k} |
bloom-filter-bits-per-key | point-lookup, range-scan: larger the better | [5,10,15,20] |
optimize-filters-for-hits | point-lookup, range-scan: turn off is better | {1,0} |
这些参数的含义以下:
block-size
:RocksDB 会将数据存放在 data block 里面,block-size 设置这些 block 的大小,当须要访问某一个 key 的时候,RocksDB 须要读取这个 key 所在的整个 block。对于点查,更大的 block 会增长读放大,影响性能,可是对于范围查询,更大的 block 可以更有效的利用磁盘带宽。disable-auto-compactions
:定义是否关闭 compaction。compaction 会占用磁盘带宽,影响写入速度。但若是 LSM 得不到 compact, level0 文件会累积,影响读性能。其实自己 compaction 也是一个有趣的 auto-tuning 的方向。write-buffer-size
:单个 memtable 的大小限制(最大值)。理论上说更大的 memtable 会增长二分查找插入位置的消耗,可是以前的初步试验发现这个选项对 writeheavy 影响并不明显。max-bytes-for-level-base
:LSM tree 里面 level1
的总大小。在数据量固定的状况下,这个值更大意味着其实 LSM 的层数更小,对读有利。target-file-size-base
:假设 target-file-size-multiplier=1
的状况下,这个选项设置的是每一个 SST 文件的大小。这个值偏小的话意味着 SST 文件更多,会影响读性能。bloom-filter-bits-per-key
:设置 Bloom Filter 的位数。对于读操做这一项越大越好。optimize-filters-for-hits
:True 表示关闭 LSM 最底层的 bloom filter。这个选项主要是由于最底层的 bloom filter 总大小比较大,比较占用 block cache 空间。若是已知查询的 key 必定在数据库中存,最底层 bloom filter 实际上是没有做用的。咱们选择了以下几个 metrics 做为优化指标。
其中 throughput 和 latency 经过 go-ycsb 的输出结果得到,store_size 和 compaction_cpu 经过 tikv-ctl 得到。
测试平台
AMD Ryzen5-2600 (6C12T),32GB RAM,512GB NVME SSD,Ubuntu 18.04,tidb-ansible 用的 master 版本。
全部的实验都是前 10 轮用随机生成的配置,后面使用模型推荐的配置:
workload=writeheavy knobs={disable-auto-compactions, block-size} metric=write_latency
实验效果以下:
这个实验中推荐结果是启用 compaction、同时 block size 设为 4KB。
虽然通常来讲写入时须要关闭 compaction 以提高性能,但分析后发现因为 TiKV 使用了 Percolator 进行分布式事务,写流程也涉及读操做(写冲突检测),因此关闭 compaction 也致使写入性能降低。同理更小的 block size 提升点查性能,对 TiKV 的写流程性能也有提高。
接下来用 point lookup 这一纯读取的 workload 进行了试验:
workload=pntlookup80 knobs={'bloom-filter-bits-per-key', 'optimize-filters-for-hits', 'block-size', 'disable-auto-compactions'} metric=get_latency
实验效果以下:
推荐结果为:bloom-filter-bits-per-key==20,block-size==4K,不 disable auto compaction。而 optimize-filters-for-hits 是否启用影响不大(因此会出现这一项的推荐结果一直在摇摆的状况)。
推荐的结果都挺符合预期的。关于 optimize-filter 这一项,应该是试验里面 block cache 足够大,因此 bloom filter 大小对 cache 性能影响不大;并且咱们是设置 default CF 相应的选项(关于 TiKV 中对 RocksDB CF 的使用,能够参考 《TiKV 是如何存取数据的》),而对于 TiKV 来讲查询 default CF 以前咱们已经肯定相应的 key 确定存在,因此是否有 filter 并无影响。以后的试验中咱们会设置 writeCF 中的 optimize-filters-for-hits(defaultCF 的这一项默认就是 0 了);而后分别设置 defaultCF 和 writeCF 中的 bloom-filter-bits-per-key,把它们做为两个 knob。
为了能尽可能测出来 bloom filter 的效果,除了上述改动以外,咱们把 workload 也改了一下:把 run phase 的 recordcount 设成 load phase 的两倍大,这样强制有一半的查找对应的 key 不存在,这样应该会测出来 write CF 的 optimize-filters-for-hits 必须关闭。改完以后的 workload 以下:
workload=pntlookup80 knobs={rocksdb.writecf.bloom-filter-bits-per-key, rocksdb.defaultcf.bloom-filter-bits-per-key, rocksdb.writecf.optimize-filters-for-hits, rocksdb.defaultcf.block-size, rocksdb.defaultcf.disable-auto-compactions} metric=get_throughput
此次的实验效果以下(发现一个很出乎意料的现象):
测出来发现推荐配置基本集中在如下两种:
rocksdb.writecf.bloom-filter-bits-per-key ['rocksdb', 'writecf'] bloom-filter-bits-per-key 20
rocksdb.defaultcf.bloom-filter-bits-per-key ['rocksdb', 'defaultcf'] bloom-filter-bits-per-key 10
rocksdb.writecf.optimize-filters-for-hits ['rocksdb', 'writecf'] optimize-filters-for-hits True
rocksdb.defaultcf.block-size ['rocksdb', 'defaultcf'] block-size 4KB
rocksdb.defaultcf.disable-auto-compactions ['rocksdb', 'defaultcf'] disable-auto-compactions False
rocksdb.writecf.bloom-filter-bits-per-key ['rocksdb', 'writecf'] bloom-filter-bits-per-key 15
rocksdb.defaultcf.bloom-filter-bits-per-key ['rocksdb', 'defaultcf'] bloom-filter-bits-per-key 15
rocksdb.writecf.optimize-filters-for-hits ['rocksdb', 'writecf'] optimize-filters-for-hits False
rocksdb.defaultcf.block-size ['rocksdb', 'defaultcf'] block-size 4KB
rocksdb.defaultcf.disable-auto-compactions ['rocksdb', 'defaultcf'] disable-auto-compactions False
分析了一下,感受是由于 write CF 比较小,当 block cache size 足够大时,bloom filter 的效果可能就不很明显了。
若是仔细看一下结果,比较以下两个 sample,会发现一个现象:
它们 knob 的惟一区别就是 30 号关闭了底层 bloom filter(optimize-filters-for-hits==True),20 号启用了底层 bloom filter(optimize-filters-for-hits==False)。结果 20 号的 throughput 比 30 还低了一点,和预期彻底不同。因而咱们打开 Grafana 琢磨了一下,分别截取了这两个 sample 运行时段的图表:
(两种场景 run 时候的 block-cache-size 都是 12.8GB)
图中粉色竖线左边是 load 阶段,右边是 run 阶段。能够看出来这两种状况下 cache hit 其实相差不大,并且 20 号还稍微低一点点。这种状况是由于 bloom filter 自己也是占空间的,若是原本 block cache size 够用,但 bloom filter 占空间又比较大,就会影响 cache hit。这个一开始确实没有预料到。其实这是一个好事情,说明 ML 模型确实能够帮咱们发现一些人工想不到的东西。
接下来再试验一下 short range scan。此次要优化的 metric 改为 scan latency:
workload=shortscan knobs={'bloom-filter-bits-per-key', 'optimize-filters-for-hits', 'block-size', 'disable-auto-compactions'} metric=scan_latency
实验结果以下:
因为篇幅有限咱们先看前 45 轮的结果。这个推荐结果尚未彻底收敛,但基本上知足 optimize-filters-for-hits==False,block-size==32KB 或者 64KB,disable-auto-compactions==False,这三个也是对结果影响最明显的参数了。根据 Intel 的 SSD 白皮书,SSD 对 32KB 和 64KB 大小的随机读性能其实差很少。bloom filter 的位数对 scan 操做的影响也不大。这个实验结果也是符合预期了。
咱们的试验场景和 OtterTune 仍是有一些区别的,主要集中在如下几点3:
一个复杂的系统须要不少环节的取舍和平衡,才能使得整体运行效果达到最好。这须要对整个系统各个环节都有很深刻的理解。而使用机器学习算法来作参数组合探索,确实会起到不少意想不到的效果。在咱们的实验过程当中,AutoTiKV 推荐的配置有些就和人工预期的状况不符,进而帮助咱们发现了系统的一些问题:
后续咱们还会对 AutoTiKV 继续进行改进,方向集中在如下几点:
参考资料[1] https://mp.weixin.qq.com/s/y8VIieK0LO37SjRRyPhtrw
[2] https://github.com/brianfrankcooper/YCSB/wiki/Core-Properties
原文阅读:https://pingcap.com/blog-cn/autotikv/