RocksDB 的常用調優引數
RocksDB 的引數以其資料多和複雜著稱,要全部弄懂也要費一番功夫,這裡也僅僅會說一下我們使用的一些引數,還有很多我們也需要後面慢慢去研究。
Parallelism
RocksDB 有兩個後臺執行緒,flush 和 compaction,兩個都可以同時並行執行。在優先順序上面,flush 是 HIGH,而 compaction 是 LOW,也就是 flush 的優先順序會比 compaction 更高,這也很容易理解,如果資料都沒有 memtable flush 到 level 0,後面也沒法做 compaction。我們可以設定 flush 和 compaction 的最大執行緒數:
- max_background_compaction:最大 compaction 執行緒數,預設是 1,但通常我們會調大,不然 compaction 會忙不過來
- max_background_flushes:最大 flush 執行緒數,預設是 1。
General
- filter_policy:也就是 bloom filter,通常在點查 Get 的時候我們需要快速判斷這個 key 在 SST 檔案裡面是否存在,如果 bloom filter 已經確定不存在了,就可以過濾掉這個 SST,減少沒必要的磁碟讀取操作了。我們使用 rocksdb::NewBloomFilterPolicy(bits_per_key) 來建立 bloom filter,bits_per_key 預設是 10,表示可能會有 1% 的誤判率,bits_per_key 越大,誤判率越小,但也會佔用更多的 memory 和 space amplification。
- block_cache:為了加快從檔案讀取資料的速度,RocksDB 會將 block 快取,雖然作業系統也有 OS cache,但通常,block cache 是快取的沒有被壓縮的 block,而 OS cache 則是快取的已經壓縮好的 block。現在 RocksDB 也支援 direct IO 模式,這樣就不會有 OS cache 了,但我們還沒有使用過。另外,RocksDB 也支援一種 compressed block cache,類似 OS cache 的機制,但我們現階段也沒有使用過。通常我們會使用 rocksdb::NewLRUCache(cache_capacity, shard_bits) 來建立一個 LRU cache。
- max_open_files:RocksDB 會將開啟的 SST 檔案控制代碼快取這,這樣下次訪問的時候就可以直接使用,而不需要重新在開啟。當 快取的檔案控制代碼超過 max_open_files 之後,一些控制代碼就會被 close 掉。如果使用 -1,RocksDB 將一直快取所有開啟的控制代碼,但這個會造成比較大量的記憶體開銷,尤其是在記憶體較小的機器上面,很容易造成 OOM。
- block_size:RocksDB 會將一批 data 打包放到一個 block 裡面,當需要訪問某一個 key 的時候,RocksDB 會將整個 block 都 load 到記憶體裡面。一個 SST 檔案會包含很多個 block,每個 SST table 都包含一個 index 用來快速定位到對應的 block。如果 block_size 越大,那麼一個 SST 檔案裡面 block 的個數就越少,這樣 index 佔用的 memory 和 space amplification 就越小,但這樣就會增大 read amplification。(為啥會讀放大?沒覺得啊,感覺只不過blockcache讀上去的一個塊比較大而以)
Flush
對於新插入的資料,RocksDB 會首先將其放到 memtable 裡面,所以 RocksDB 的寫入速度是很快的。當一個 memtable full 之後,RocksDB 就會將這個 memtable 變成 immutable 的,然後用另一個新的 memtable 來處理後續的寫入,immutable 的 memtable 就等待被 flush 到 level 0。也就是同時,RocksDB 會有一個活躍的 memtable 和 0 或者多個 immutable memtable。對於 flush,我們需要關注:
- write_buffer_size:memtable 的最大 size,如果超過了這個值,RocksDB 就會將其變成 immutable memtable,並在使用另一個新的 memtable。
- max_write_buffer_number:最大 memtable 的個數,如果 active memtable full 了,並且 active memtable 加上 immutable memtable 的個數已經到了這個閥值,RocksDB 就會停止後續的寫入。通常這都是寫入太快但是 flush 不及時造成的。
- min_write_buffer_number_to_merge:在 flush 到 level 0 之前,最少需要被 merge 的 memtable 個數。如果這個值是 2,那麼當至少有兩個 immutable 的 memtable 的時候,RocksDB 會將這兩個 immutable memtable 先 merge,在 flush 到 level 0。預先 merge 能減小需要寫入的 key 的資料,譬如一個 key 在不同的 memtable 裡面都有修改,那麼我們可以 merge 成一次修改。但這個值太大了會影響讀取效能,因為 Get 會遍歷所有的 memtable 來看這個 key 是否存在。
Level Style Compaction
RocksDB 預設的將 SST 檔案放在不同的 level,自然就是用的 level style compaction。Memtable 的被 flush 到 level 0,level 0 有最新的資料,其他更上層的 level 則是有老的資料。Level 0 裡面的 SST 檔案可能會有重疊,也就是不同的 SST 檔案保護的資料 key range 會重疊,但 level 1 以及之上的 level 則不會重疊。對於一次 Get 操作來說,通常會在所有的 level 0 檔案裡面檢查是否存在,但如果在其他層,如果在一個 SST 裡面找到了這個 key,那麼其他 SST 都不會包含這個 key。每一層都比上一層大 10 倍,當然這個是可以配置的。
一次 compaction 會將 level N 的一些檔案跟 level N + 1 裡面跟這些檔案重疊的檔案進行 compact 操作。兩個不同的 compaction 操作會在不會的 level 或者不同的 key ranges 之間進行,所以可以同時併發的進行多個 compaction 操作。
在 level 0 和 level 1 之間的 compaction 比較 tricky,level 0 會覆蓋所有的 key range,所以當 level 0 和 level 1 之間開始進行 compaction 的時候,所有的 level 1 的檔案都會參與合併。這時候就不能處理 level 1 到 level 2 的 compaction,必須等到 level 0 到 level 1 的 compaction 完成,才能繼續。如果 level 0 到 level 1 的速度比較慢,那麼就可能導致整個系統大多數時候只有一個 compaction 在進行。
Level 0 到 level 1 的 compaction 是一個單執行緒的,也就意味著這個操作其實並不快,RocksDB 後續引入了一個 max_subcompactions,解決了 level 0 到 level 1 的 compaction 多執行緒問題。通常,為了加速 level 0 到 level 1 的 compaction,我們會盡量保證level 0 和 level 1 有相同的 size。
當決定了 level 1 的大概 size,我們就需要決定 level multiplier。假設 level 1 的 size 是 512MB,level multiplier 是 10,整個 DB 的 size 是 500GB。Level 2 的 size 是 5GB,level 3 是 51GB,level 4 是 512GB,level 5 以及更上層的 level 就是空的。
那麼 size amplification 就很容易計算了 (512 MB + 512 MB + 5GB + 51GB + 512GB) / (500GB) = 1.14,write amplification 的計算則是:任何一個 byte 首先寫入 level 0,然後 compact 到 level 1,因為 level 1 的 size 跟 level 0 是一樣的,所以 write amplification 在 level 0 到 level 1 的 compaction 是 2。當這個 byte compact 到 level 2 的時候,因為 level 2 比 level 1 大 10 倍,所以 write amplification 是 10。對於 level 2 到 level 3,level 3 到 level 4 也是一樣。
所以總的 write amplification 就是 1 + 2 + 10 + 10 + 10 = 33。對於點查來說,通常會訪問所有的 level 0 檔案或者其他 level 的至多一個檔案,這裡我們可以使用 bloom filter 來減少 read amplification,但這個對於 range scans(也就是 iterator seek 這些)沒啥作用,所以 range scans 的 read amplification 是 level 0 的檔案資料 + 非空 level 的數量。
理解了上面的 level compaction 的流程,我們就可以開始配置相關的引數了。
- level0_file_num_compaction_trigger:當 level 0 的檔案資料達到這個值的時候,就開始進行 level 0 到 level 1 的 compaction。所以通常 level 0 的大小就是 write_buffer_size * min_write_buffer_number_to_merge * level0_file_num_compaction_trigger。
- max_bytes_for_level_base 和 max_bytes_for_level_multiplier:max_bytes_for_level_base 就是 level1 的總大小,在上面提到,我們通常建議 level 1 跟 level 0 的 size 相當。上層的 level 的 size 每層都會比當前層大 max_bytes_for_level_multiplier 倍,這個值預設是 10,通常也不建議修改。
- target_file_size_base 和 target_file_size_multiplier:target_file_size_base 則是 level 1 SST 檔案的 size。上面層的檔案 size 都會比當前層大 target_file_size_multiplier 倍,預設 target_file_size_multiplier 是 1,也就是每層的 SST 檔案都是一樣的。增加 target_file_size_base 會減少整個 DB 的 size,這通常是一件好事情,也通常建議 target_file_size_base 等於 max_bytes_for_level_base / 10,也就是 level 1 會有 10 個 SST 檔案。
- compression_per_level:使用這個來設定不同 level 的壓縮級別,通常 level 0 和 level 1 不壓縮,更上層壓縮。也可以對更上層的選擇慢的壓縮演算法,這樣壓縮效率更高,而對下層的選擇快的壓縮演算法。
- compression_per_level:使用這個來設定不同 level 的壓縮級別,通常 level 0 和 level 1 不壓縮,更上層壓縮。也可以對更上層的選擇慢的壓縮演算法,這樣壓縮效率更高,而對下層的選擇快的壓縮演算法。TiKV 預設全選擇的 lz4 的壓縮方式。