最近看到一篇 Paper裤唠,Auto-tuning RocksDB莹痢,頓時兩眼放光竞膳。RocksDB 以配置多,難優(yōu)化而著稱坦辟,據(jù)傳 RocksDB 配置多到連 RocksDB 自己的開發(fā)者都沒法提供出一個好的配置,所以很多時候滔吠,我們都只能大概給一個比較優(yōu)的配置挠日,在根據(jù)用戶實際的 workload 調(diào)整。所以這時候真的希望能有一個自動 tuning 的方案冬骚。
對于數(shù)據(jù)庫來說懂算,auto tuning 是當(dāng)前一個非常熱門的研究領(lǐng)域,譬如 CMU 知名的 Peloton 項目计技,但這些項目通常都會關(guān)注特別多的配置,使用 TensorFlow 等技術(shù)進(jìn)行機(jī)器學(xué)習(xí)舍悯,靠人工智能來調(diào)優(yōu)。這個當(dāng)然也能用到 RocksDB 上面饮醇,不過對作者來說秕豫,這些都太復(fù)雜了(其實對我們也一樣,雖然人工智能誘惑很大混移,但坑很多)歌径。所以,作者主要關(guān)注的是如何更好的提升寫入性能沮脖。而基本原理也很簡單芯急,在寫入負(fù)載高的時候關(guān)掉 compaction,而在寫入負(fù)載低的時候打開 compaction免姿。那么自然要考慮的就是榕酒,如何去實現(xiàn)一個 compaction auto-tuner 了。
RocksDB 介紹
因為 RocksDB 在之前的文章中已經(jīng)介紹了太多了紊婉,這里就稍微簡單介紹一下辑舷。RocksDB 是基于 LSM-Tree 的,大概如下
雖然大部分讀者對于 LSM 已經(jīng)非常熟悉了肢础, 但這里還是簡單的介紹一下碌廓。首先,任何的寫入都會先寫到 WAL慨蛙,然后在寫入 Memory Table(Memtable)。當(dāng)然為了性能股淡,也可以不寫入 WAL唯灵,但這樣就可能面臨崩潰丟失數(shù)據(jù)的風(fēng)險。Memory Table 通常是一個能支持并發(fā)寫入的 skiplist埠帕,但 RocksDB 同樣也支持多種不同的 skiplist敛瓷,用戶可以根據(jù)實際的業(yè)務(wù)場景進(jìn)行選擇。
當(dāng)一個 Memtable 寫滿了之后呐籽,就會變成 immutable 的 Memtable狡蝶,RocksDB 在后臺會通過一個 flush 線程將這個 Memtable flush 到磁盤,生成一個 Sorted String Table(SST) 文件苏章,放在 Level 0 層奏瞬。當(dāng) Level 0 層的 SST 文件個數(shù)超過閾值之后,就會通過 Compaction 策略將其放到 Level 1 層并淋,以此類推珍昨。
這里關(guān)鍵就是 Compaction,如果沒有 Compaction酬诀,那么寫入是非陈嫫玻快的,但會造成讀性能降低肴裙,同樣也會造成很嚴(yán)重的空間放大問題。為了平衡寫入甜癞,讀取宛乃,空間這些問題,RocksDB 會在后臺執(zhí)行 Compaction征炼,將不同 Level 的 SST 進(jìn)行合并谆奥。但 Compaction 并不是沒有開銷的,它也會占用 I/O宰译,所以勢必會影響外面的寫入和讀取操作魄懂。
對于 RocksDB 來說,他有三種 Compaction 策略,一種就是默認(rèn)的 Leveled Compaction乡括,另一種就是 Universal Compaction,也就是常說的 Size-Tired Compaction诲泌,還有一種就是 FIFO Compaction。在之前介紹 Dostoevsky 的文章里面哀蘑,已經(jīng)詳細(xì)的介紹了 Leveled 和 Tired葵第,這里就不在重新說明了卒密。對于 FIFO 來說,它的策略非常的簡單哮奇,所有的 SST 都在 Level 0,如果超過了閾值哲身,就從最老的 SST 開始刪除,其實可以看到怔揩,這套機(jī)制非常適合于存儲時序數(shù)據(jù)误辑。
實際對于 RocksDB 來說巾钉,它其實用的是一種 Hybrid 的策略,在 Level 0 層砰苍,它其實是一個 Size-Tired 的赚导,而在其他層就是 Leveled 的。
這里在聊聊幾個放大因子凰锡,對于 LSM 來說圈暗,我們需要考慮寫放大,讀放大和空間放大勇哗,讀放大可以認(rèn)為是 RA = number of queries * disc reads
寸齐,譬如用戶要讀取一個 page,但實際下面讀取了 3 個 pages扰法,那么讀放大就是 3毅厚。而寫放大則是 WA = data writeen to disc / data written to database
,譬如用戶寫入了 10 字節(jié)殴边,但實際寫到磁盤的有 100 字節(jié),那么寫放大就是 10竖幔。而對于空間放大來說是偷,則是 SA = size of database files / size of databases used on disk
蛋铆,也就是數(shù)據(jù)庫可能是 100 MB,但實際占用了 200 MB 的空間刺啦,那么就空間放大就是 2玛瘸。
這里簡單的聊了聊 RocksDB 相關(guān)的一些知識,下面就來說說作者是如何做 Auto tuning 的右核。
Statistics
因為關(guān)注的目標(biāo)是寫入壓力情況下面的 compaction 優(yōu)化渺绒,所以自然我們需要關(guān)注的是 RocksDB 的 compaction 統(tǒng)計。RocksDB 會定期將很多統(tǒng)計信息給寫入到日志里面躏鱼,所以我們只需要分析日志就行了了针炉。
我們需要關(guān)注的 RocksDB 日志如下:
Cumulative compaction: 2.09 GB write, 106.48 MB/s write, 1.19 GB read,
60.66 MB/s read, 14.4 seconds
Interval compaction: 1.85 GB write, 130.27 MB/s write, 1.19 GB read, 83.86
MB/s read, 13.2 seconds
Cumulative writes: 10K writes, 10K keys, 10K commit groups, 1.0 writes per
commit group, ingest: 0.93 GB, 47.57 MB/s
Cumulative WAL: 10K writes, 0 syncs, 10000.00 writes per sync, written:
0.93 GB, 47.57 MB/s
Cumulative stall: 00:00:0.000 H:M:S, 0.0 percent
Interval writes: 7201 writes, 7201 keys, 7201 commit groups, 1.0 writes
per commit group, ingest: 686.97 MB, 47.36 MB/s
Interval WAL: 7201 writes, 0 syncs, 7201.00 writes per sync, written: 0.67
MB, 47.36 MB/s
Interval stall: 00:00:0.000 H:M:S, 0.0 percent
具體的分析腳本在 這里篡帕,這個腳本會提取相應(yīng)的字段贸呢,然后繪制成圖表,這樣我們就能直觀的看實際的 I/O 量了怔鳖。
Compaction Tuner
要控制 auto compaction固蛾,RocksDB 有一個 disable_auto_compactions
參數(shù),當(dāng)設(shè)置為 false 的時候献幔,就會停止 compaction蜡感,但這時候需要將 Level 0 的 slowdown 參數(shù)也設(shè)置大,不然就會出現(xiàn) write stall 問題郑兴。
RocksDB 自身提供了一個 SetOptions
的函數(shù)情连,方便外面動態(tài)的去調(diào)整參數(shù),但這樣其實就需要自己在外面顯示的維護(hù) RocksDB 實例球榆。另一種方式就是給 RocksDB 傳一個共享的 environment禁筏,通過這個來控制幾個參數(shù)的修改。權(quán)衡之后每强,作者決定使用共享 env 的方式州刽,因為容易實現(xiàn),同時也能更方便的去訪問到 database 的內(nèi)部穗椅。
所以作者定制了一個 env,提供了 Enable 和 Disable 兩個函數(shù)门坷,在 Disable 里面袍镀,將 level0_file_num_compaction_trigger
設(shè)置成了 (1<<30)
苇羡,這個也是 RocksDB PrepareForBulkLoad
函數(shù)里面的值。
bool disable_auto_compactions;
int prev_level0_file_num_compaction_trigger;
int level0_file_num_compaction_trigger;
void DisableCompactions() {
if (!disable_auto_compactions) {
prev_level0_file_num_compaction_trigger =
level0_file_num_compaction_trigger;
disable_auto_compactions = true;
level0_file_num_compaction_trigger = (1<<30);
}
};
void EnableCompactions() {
if (disable_auto_compactions) {
disable_auto_compactions = false;
level0_file_num_compaction_trigger =
prev_level0_file_num_compaction_trigger;
}
}
RocksDB 的 compaction 控制在 ColumnFamilyData 類里面锦茁,通過函數(shù) RecalculateWriteStallConditions
來計算的,但 ColumnFamilyData 并沒有 env撑刺,所以作者擴(kuò)展了一下握玛,給 ColumnFamilyData 的構(gòu)造函數(shù)加了個 env 變量:
ColumnFamilyData* new_cfd = new ColumnFamilyData(
id, name, dummy_versions, table_cache_, write_buffer_manager_, options,
*db_options_, env_options_, this, Env::Default());
然后在改了下 RecalculateWriteStallConditions
挠铲,讓其能接受 env 的參數(shù)來控制。
-WriteStallCondition ColumnFamilyData::RecalculateWriteStallConditions(
- const MutableCFOptions& mutable_cf_options) {
+WriteStallCondition ColumnFamilyData::RecalculateWriteStallConditions() {
auto write_stall_condition = WriteStallCondition::kNormal;
+ if (current_ != nullptr) {
+ if (mutable_cf_options_.atuo_tuned_compaction) {
+ mutable_cf_options_.level0_file_num_compaction_trigger = env_->level0_file_num_compaction_trigger;
+ mutable_cf_options_.disable_auto_compations = env_disable_auto_compations;
+ }
+ }
+ const MutableCFOptions& mutable_cf_options = mutable_cf_options_;
Rate Limiter
在 RocksDB 里面安聘,我們也可以通過 Rate Limiter 來 控制 I/O浴韭,通常有幾個參數(shù):
-
rate_limit_bytes_per_sec
:控制 compaction 和 flush 每秒總的寫入量 -
refill_period_us
:控制 tokens 多久再次填滿脯宿,譬如rate_limit_bytes_per_sec
是 10MB/s,而refill_period_us
是 100ms榴芳,那么每 100ms 的流量就是 1MB/s跺撼。 -
fairness
:用來控制 high 和 low priority 的請求,防止 low priority 的請求餓死柿祈。
另外哩至,RocksDB 還提供了一個 Auto-tuned Rate Limiter憨募,它使用了一個 Multiplicative Increase Multiplicative Decrease(MIMD) 算法袁辈,auto-tuned 發(fā)生條件如下:
if (auto_tuned_) {
static const int kRefillsPerTune = 100;
std::chrono::microseconds now(NowMicrosMonotonic(env_));
if (now - tuned_time_ >=
kRefillsPerTune * std::chrono::microseconds(refill_period_us_))
{
Tune();
}
}
Auto-tuned RateLimiter 里面已經(jīng)有很高效的 I/O 判斷了,但是這個 I/O 包含的是 flush 和 compaction 的請求的尾膊,作者需要區(qū)分兩種不同的請求。這個在 RocksDB 里面很容易待笑,因為 compaction 和 low priority 請求抓谴,而 flush 是 high priority 的癌压。作者把 GenericRateLimiter::Request
里面計算 num_drain_
的方式改了下,引入了 num_high_drains_
和 num_low_drains_
兩個變量集侯,然后得到 num_drains
帜消,如下:num_drains_ = num_high_drains_ + num_low_drains_;
。
有了 high 和 low 的 drains 變量辈讶,就可以直接來控制 compaction 了粘衬,作者新增了一個 TuneCompaction
函數(shù)稚新,類似原來的 Tune
:
Status GenericRateLimiter::TuneCompaction(Statistics* stats) {
const int kLowWatermarkPct = 50;
const int kHighWatermarkPct = 90;
std::chrono::microseconds prev_tuned_time = tuned_time_;
tuned_time_ = std::chrono::microseconds(NowMicrosMonotonic(env_));
int64_t elapsed_intervals = (tuned_time_ - prev_tuned_time +
std::chrono::microseconds(refill_period_us_) -
std::chrono::microseconds(1)) /
std::chrono::microseconds(refill_period_us_);
// We tune every kRefillsPerTune intervals, so the overflow and division by
// zero conditions should never happen.
assert(num_drains_ - prev_num_drains_ <= port::kMaxInt64 / 100);
assert(elapsed_intervals > 0);
int64_t drained_high_pct =
(num_high_drains_ - prev_num_high_drains_) * 100 /
elapsed_intervals;
int64_t drained_low_pct =
(num_low_drains_ - prev_num_low_drains_) * 100 /
elapsed_intervals;
int64_t drained_pct = drained_high_pct + drained_low_pct;
if (drained_pct == 0) {
// Nothing
} else if (drained_pct <= kHighWatermarkPct && drained_high_pct <
kLowWatermarkPct) {
env_->EnableCompactions();
} else if (drained_pct >= kHighWatermarkPct && drained_high_pct >=
kLowWatermarkPct) {
env_->DisableCompactions();
RecordTick(stats, COMPACTION_DISABLED_COUNT, 1);
}
num_low_drains_ = prev_num_low_drains_;
num_high_drains_ = prev_num_high_drains_;
num_drains_ = prev_num_drains_;
return Status::OK();
}
觸發(fā)規(guī)則也比較容易褂删,如果 flush I/O 高于 50%,而總的 I/O 超過了 90%缅帘,就關(guān)掉 compaction难衰,反之則打開 compaction盖袭。
DB bench
準(zhǔn)備好了所有東西彼宠,下一步自然是測試弟塞,驗證 tuning 能否有效了。作者在 RocksDB 官方的 db_bench
上面加入了一種 Sine Wave 模式摧冀,也就是讓寫入滿足如下規(guī)則:
這個模式現(xiàn)在已經(jīng)加入了 db_bench
里面索昂,后面我們也可以嘗試一下扩借。然后就是確定下 RocksDB 的一些參數(shù),開始測試了框产。這里具體不說了错洁,反正就是改參數(shù)屯碴,做實驗,得到一個比較優(yōu)的配置的過程忱叭。然后作者對比了 RocksDB 默認(rèn)開啟 compaction今艺,不開啟 compaction 以及使用自己的 Auto-tuner 的情況,一些結(jié)果:
可以看到撵彻,數(shù)據(jù)還是很不錯的实牡。詳細(xì)的數(shù)據(jù)可以看作者的 Paper创坞。
總結(jié)
總的來說,作者實現(xiàn)的 Auto-tuner 通過控制 compaction偎谁,取得了比較好的效果,后面對我們的參數(shù)調(diào)優(yōu)也有很好的借鑒意義。另外婉支,RocksDB team 也一直在致力于 I/O 的優(yōu)化向挖,我還是很堅信 RocksDB 會越來越快的∏砹耍現(xiàn)在我們也在進(jìn)行 TiKV 的 tuning 工作晌块,會分析 TiKV 當(dāng)前的 workload 來調(diào)整 RocksDB 的參數(shù)徊件,如果你對這方面感興趣蒜危,歡迎聯(lián)系我 tl@pingcap.com辐赞。