做者:吳毅 王遠立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/