做者:吳毅 王遠立html
TiKV 底層使用了 RocksDB 做爲存儲引擎,然而 RocksDB 配置選項不少,不少狀況下只能經過反覆測試或者依靠經驗來調優,甚至連 RocksDB 的開發者都自嘲,他們沒辦法弄清楚每一個參數調整對性能的影響。若是有一個自動 tuning 的方案就能夠大大減小調優的人力成本,同時也可能在調優的過程當中,發現一些人工想不到的信息。咱們從 AutoML 中獲得啓發,但願能用 Automated Hyper-parameter Tuning 中的一些方法來對數據庫參數進行自動調優。git
經常使用的 Automated Hyper-parameter Tuning 方式大致上有如下三種:github
隨機搜索,或者說叫啓發式搜索。包括 GridSearch 和 RandomSearch。這種方法的改進空間主要體如今使用不一樣的採樣方法生成配置,但本質上仍然是隨機試驗不一樣的配置,沒有根據跑出來的結果來反饋指導採樣過程,效率比較低。算法
Multi-armed Bandit。這種方法綜合考慮了「探索」和「利用」兩個問題,既能夠配置更多資源(也就是採樣機會)給搜索空間中效果更優的一部分,也會考慮嘗試儘可能多的可能性。Bandit 結合貝葉斯優化,就構成了傳統的 AutoML 的核心。數據庫
深度強化學習。強化學習在 AutoML 中最著名的應用就是 NAS,用於自動生成神經網絡結構。另外它在 深度學習參數調優 中也有應用。它的優勢是從「從數據中學習」轉變爲「從動做中學習」(好比 knob 中的 cache size 從小調到大),既能夠從性能好的樣本中學習,也能夠從性能壞的樣本中學習。但強化學習的坑也比較多,體如今訓練可能比較困難,有時結果比較難復現。數組
目前學術界針對 auto-tune 數據庫的研究也有不少,採用的方法大多集中在後面兩種。其中一個比較有名的研究是 OtterTune 。咱們受 OtterTune 的啓發,開發了 AutoTiKV,一個用於對 TiKV 數據庫進行自動調優的工具。項目啓動三個月以來,AutoTiKV 在 TiKV 內部測試和調參的環節起到了較好的效果,有了一個很好的開始。後續咱們還會針對生產環境上的一些特色,對它進行繼續探索和完善。緩存
項目地址:github.com/tikv/auto-t…bash
整個調優過程大體以下圖:服務器
整個過程會循環跑 200 個 round(能夠用戶自定義),或者也能夠定義成到結果收斂爲止。網絡
AutoTiKV 支持在修改參數以後重啓 TiKV(若是不須要也能夠選擇不重啓)。須要調節的參數和須要查看的 metric 能夠在 controller.py 裏聲明。
一開始的 10 輪(具體大小能夠調節)是用隨機生成的 knob 去 benchmark,以便收集初始數據集。以後的都是用 ML 模型推薦的參數去 benchmark。
AutoTiKV 使用了和 OtterTune 同樣的高斯過程迴歸(Gaussian Process Regression,如下簡稱 GP)來推薦新的 knob[1],它是基於高斯分佈的一種非參數模型。高斯過程迴歸的好處是:
和神經網絡之類的方法相比,GP 屬於無參數模型,算法計算量相對較低,並且在訓練樣本不多的狀況下表現比 NN 更好。
它能估計樣本的分佈狀況,即 X
的均值 m(X)
和標準差 s(X)
。若 X
周圍的數據很少,則它被估計出的標準差 s(X)
會偏大(表示這個樣本 X
和其餘數據點的差別大)。直觀的理解是若數據很少,則不肯定性會大,體如今標準差偏大。反之,數據足夠時,不肯定性減小,標準差會偏小。這個特性後面會用到。
但 GP 自己其實只能估計樣本的分佈,爲了獲得最終的預測值,咱們須要把它應用到貝葉斯優化(Bayesian Optimization)中。貝葉斯優化算法大體可分爲兩步:
經過 GP 估計出函數的分佈狀況。
經過採集函數(Acquisition Function)指導下一步的採樣(也就是給出推薦值)。
採集函數(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:根據具體 workload 不一樣又分爲 write throughput、get throughput、scan throughput
latency:根據具體 workload 不一樣又分爲 write latency、get latency、scan latency
store_size
compaction_cpu
其中 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
複製代碼
此次的實驗效果以下(發現一個很出乎意料的現象):
測出來發現推薦配置基本集中在如下兩種:
{3,1,1,0,0}
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
{2,2,0,0,0}
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,會發現一個現象:
30 , 2019-08-23 03:03:42 , [3. 1. 1. 0. 0.] , [4.30542000e+04 1.18890000e+04 8.68628124e+10 5.10200000e+01]
20 , 2019-08-22 16:09:26 , [3. 1. 0. 0. 0.] , [4.24397000e+04 1.20590000e+04 8.68403016e+10 5.07300000e+01]
它們 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][4]:
AutoTiKV 直接和 DB 運行在同一臺機器上,而不是像 OtterTune 同樣設置一個集中式的訓練服務器。但其實這樣並不會佔用不少資源,還避免了不一樣機器配置不同形成數據不一致的問題。
省去了 workload mapping(OtterTune 加了這一步來從 repository 中挑出和當前 workload 最像的訓練樣本,而咱們目前默認 workload 類型只有一種)。
要調的 knobs 比較少,省去了 identity important knobs(OtterTune 是經過 Lasso Regression 選出 10 個最重要的 knob 進行調優)。
另外咱們重構了 OtterTune 的架構,減小了對具體數據庫系統的耦合度。更方便將整個模型和 pipeline 移植到其餘系統上(只需修改 controller.py 中具體操做數據庫系統的語句便可,其它都不用修改),也更適合比起 SQL 更加輕量的 KV 數據庫。
最後咱們解決了 OtterTune 中只能調整 global knob,沒法調節不一樣 session 中同名 knob 的問題。
一個複雜的系統須要不少環節的取捨和平衡,才能使得整體運行效果達到最好。這須要對整個系統各個環節都有很深刻的理解。而使用機器學習算法來作參數組合探索,確實會起到不少意想不到的效果。在咱們的實驗過程當中,AutoTiKV 推薦的配置有些就和人工預期的狀況不符,進而幫助咱們發現了系統的一些問題:
有些參數對結果的影響並無很大。好比這個參數起做用的場景根本沒有觸發,或者說和它相關的硬件並無出現性能瓶頸。
有些參數直接動態調整是達不到效果的,或者須要跑足夠長時間的 workload 才能看出效果。例如 block cache size 剛從小改大的一小段時間確定是裝不滿的,必需要等 workload 足夠把它填滿以後,才能看出大緩存對整體 cache hit 的提高效果。
有些參數的效果和預期相反,分析了發現該參數實際上是有反作用的,在某些場景下就不大行了(好比上面的 bloom filter 那個例子)。
有些 workload 並非徹底的讀或者寫,還會摻雜一些別的操做。而人工判斷預期效果的時候極可能忽略這一點(好比上面的 writeheavy)。特別是在實際生產環境中,DBA 並不能提早知道會遇到什麼樣的 workload。這大概也就是自動調優的做用吧。
後續咱們還會對 AutoTiKV 繼續進行改進,方向集中在如下幾點:
動態適應不斷變化的 workload(好比一會讀一會寫),以及以前沒有出現過的不一樣業務特徵的 workload。
有時 ML 模型有可能陷入局部最優(嘗試的 knob 組合不全,限於若干個當前效果還不錯的 knob 循環推薦了)。
借鑑 AutoML 中的思路,嘗試更多不一樣的 ML 模型來提升推薦效果,減小推薦所需時間。
參考資料
[1] mp.weixin.qq.com/s/y8VIieK0L…