自動調優 RocksDB

最近看到一篇 Paper,Auto-tuning RocksDB,頓時兩眼放光。RocksDB 以配置多,難優化而著稱,據傳 RocksDB 配置多到連 RocksDB 自己的開發者都沒法提供出一個好的配置,所以很多時候,我們都只能大概給一個比較優的配置,在根據用戶實際的 workload 調整。所以這時候真的希望能有一個自動 tuning 的方案。

對於數據庫來說,auto tuning 是當前一個非常熱門的研究領域,譬如 CMU 知名的 Peloton 項目,但這些項目通常都會關注特別多的配置,使用 TensorFlow 等技術進行機器學習,靠人工智能來調優。這個當然也能用到 RocksDB 上面,不過對作者來說,這些都太複雜了(其實對我們也一樣,雖然人工智能誘惑很大,但坑很多)。所以,作者主要關注的是如何更好的提升寫入性能。而基本原理也很簡單,在寫入負載高的時候關掉 compaction,而在寫入負載低的時候打開 compaction。那麼自然要考慮的就是,如何去實現一個 compaction auto-tuner 了。

RocksDB 介紹

因爲 RocksDB 在之前的文章中已經介紹了太多了,這裏就稍微簡單介紹一下。RocksDB 是基於 LSM-Tree 的,大概如下

雖然大部分讀者對於 LSM 已經非常熟悉了, 但這裏還是簡單的介紹一下。首先,任何的寫入都會先寫到 WAL,然後在寫入 Memory Table(Memtable)。當然爲了性能,也可以不寫入 WAL,但這樣就可能面臨崩潰丟失數據的風險。Memory Table 通常是一個能支持併發寫入的 skiplist,但 RocksDB 同樣也支持多種不同的 skiplist,用戶可以根據實際的業務場景進行選擇。

當一個 Memtable 寫滿了之後,就會變成 immutable 的 Memtable,RocksDB 在後臺會通過一個 flush 線程將這個 Memtable flush 到磁盤,生成一個 Sorted String Table(SST) 文件,放在 Level 0 層。當 Level 0 層的 SST 文件個數超過閾值之後,就會通過 Compaction 策略將其放到 Level 1 層,以此類推。

這裏關鍵就是 Compaction,如果沒有 Compaction,那麼寫入是非常快的,但會造成讀性能降低,同樣也會造成很嚴重的空間放大問題。爲了平衡寫入,讀取,空間這些問題,RocksDB 會在後臺執行 Compaction,將不同 Level 的 SST 進行合併。但 Compaction 並不是沒有開銷的,它也會佔用 I/O,所以勢必會影響外面的寫入和讀取操作。

對於 RocksDB 來說,他有三種 Compaction 策略,一種就是默認的 Leveled Compaction,另一種就是 Universal Compaction,也就是常說的 Size-Tired Compaction,還有一種就是 FIFO Compaction。在之前介紹 Dostoevsky 的文章裏面,已經詳細的介紹了 Leveled 和 Tired,這裏就不在重新說明了。對於 FIFO 來說,它的策略非常的簡單,所有的 SST 都在 Level 0,如果超過了閾值,就從最老的 SST 開始刪除,其實可以看到,這套機制非常適合於存儲時序數據。

實際對於 RocksDB 來說,它其實用的是一種 Hybrid 的策略,在 Level 0 層,它其實是一個 Size-Tired 的,而在其他層就是 Leveled 的。

這裏在聊聊幾個放大因子,對於 LSM 來說,我們需要考慮寫放大,讀放大和空間放大,讀放大可以認爲是 RA = number of queries * disc reads,譬如用戶要讀取一個 page,但實際下面讀取了 3 個 pages,那麼讀放大就是 3。而寫放大則是 WA = data writeen to disc / data written to database,譬如用戶寫入了 10 字節,但實際寫到磁盤的有 100 字節,那麼寫放大就是 10。而對於空間放大來說,則是 SA = size of database files / size of databases used on disk,也就是數據庫可能是 100 MB,但實際佔用了 200 MB 的空間,那麼就空間放大就是 2。

這裏簡單的聊了聊 RocksDB 相關的一些知識,下面就來說說作者是如何做 Auto tuning 的。

Statistics

因爲關注的目標是寫入壓力情況下面的 compaction 優化,所以自然我們需要關注的是 RocksDB 的 compaction 統計。RocksDB 會定期將很多統計信息給寫入到日誌裏面,所以我們只需要分析日誌就行了了。

我們需要關注的 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

具體的分析腳本在 這裏,這個腳本會提取相應的字段,然後繪製成圖表,這樣我們就能直觀的看實際的 I/O 量了。

Compaction Tuner

要控制 auto compaction,RocksDB 有一個 disable_auto_compactions 參數,當設置爲 false 的時候,就會停止 compaction,但這時候需要將 Level 0 的 slowdown 參數也設置大,不然就會出現 write stall 問題。

RocksDB 自身提供了一個 SetOptions 的函數,方便外面動態的去調整參數,但這樣其實就需要自己在外面顯示的維護 RocksDB 實例。另一種方式就是給 RocksDB 傳一個共享的 environment,通過這個來控制幾個參數的修改。權衡之後,作者決定使用共享 env 的方式,因爲容易實現,同時也能更方便的去訪問到 database 的內部。

所以作者定製了一個 env,提供了 Enable 和 Disable 兩個函數,在 Disable 裏面,將 level0_file_num_compaction_trigger 設置成了 (1<<30),這個也是 RocksDB PrepareForBulkLoad 函數裏面的值。

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 類裏面,通過函數 RecalculateWriteStallConditions 來計算的,但 ColumnFamilyData 並沒有 env,所以作者擴展了一下,給 ColumnFamilyData 的構造函數加了個 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 的參數來控制。

-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,通常有幾個參數:

  • 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 發生條件如下:

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 裏面已經有很高效的 I/O 判斷了,但是這個 I/O 包含的是 flush 和 compaction 的請求的,作者需要區分兩種不同的請求。這個在 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 函數,類似原來的 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();
}

觸發規則也比較容易,如果 flush I/O 高於 50%,而總的 I/O 超過了 90%,就關掉 compaction,反之則打開 compaction。

DB bench

準備好了所有東西,下一步自然是測試,驗證 tuning 能否有效了。作者在 RocksDB 官方的 db_bench 上面加入了一種 Sine Wave 模式,也就是讓寫入滿足如下規則:

這個模式現在已經加入了 db_bench 裏面,後面我們也可以嘗試一下。然後就是確定下 RocksDB 的一些參數,開始測試了。這裏具體不說了,反正就是改參數,做實驗,得到一個比較優的配置的過程。然後作者對比了 RocksDB 默認開啓 compaction,不開啓 compaction 以及使用自己的 Auto-tuner 的情況,一些結果:

可以看到,數據還是很不錯的。詳細的數據可以看作者的 Paper。

總結

總的來說,作者實現的 Auto-tuner 通過控制 compaction,取得了比較好的效果,後面對我們的參數調優也有很好的借鑑意義。另外,RocksDB team 也一直在致力於 I/O 的優化,我還是很堅信 RocksDB 會越來越快的。現在我們也在進行 TiKV 的 tuning 工作,會分析 TiKV 當前的 workload 來調整 RocksDB 的參數,如果你對這方面感興趣,歡迎聯繫我 [email protected]

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章