【翻譯】rocksdb除錯指引

morningli發表於2022-12-02

rocksdb除錯指引


翻譯自官方wiki:https://github.com/facebook/rocksdb/wiki/RocksDB-Tuning-Guide
轉載請註明出處:https://www.cnblogs.com/morningli/p/16788424.html


基本除錯建議

rocksdb是可以靈活地高度地進行配置的。另一方面,rocksdb多年來一直在提高它的自適應性。如果你的應用在SSD上正常執行,我們完全不建議你對rocksdb進行調優。我們建議使用者 設定選項和基本調整 ,除非你看到一個明顯的問題,否則不需要除錯它。一方面在這個頁面建議的典型配置甚至開箱即用的配置通常會對正常的負荷是合理的。另一方面當負荷或者硬體改變時,調優rocksdb常常容易出現更大的效能回退。使用者通常需要持續除錯rocksdb來保持同樣等級的效能。

當你需要調優rocksdb,在調優之前我們建議你先去了解基本的rocksdb設計(有很多 會談 和 一些 出版物 ),特別是rocksdb的LSM樹是怎麼實現的和 壓實

rocksdb統計資料

要了解rocksdb的瓶頸,有一些工具可以幫助到你:

statistics -- 設定成rocksdb::CreateDBStatistics(),任何時候透過呼叫options.statistics.ToString()可以得到一個可讀的rocksdb統計資料。詳情見 statistics

Compaction and DB stats rocksdb一直儲存一些compaction的統計資料和基本的db執行狀態。這是瞭解LSM樹形狀的最簡單的方法,可以透過這來評估讀寫效能。詳情見 Compaction and DB stats

Perf Context and IO Stats Context Perf Context and IO Stats Context 可以幫助瞭解某一指定語句的相關計數。

可能的效能瓶頸

System Metrics

有時,由於系統指標已飽和,效能會受到限制,並且有時會出現意想不到的症狀。微調 RocksDB 的使用者應該能夠從作業系統中查詢系統指標,並確定特定系統指標的使用率是否很高。

  • 磁碟寫頻寬 通常rocksdb compaction會嘗試寫超過SSD驅動處理能力的資料。症狀可能是rocksdb的write stalling(見compaction stats的Stalls或者statistics的STALL_MICROS)。或者會導致讀請求很慢,因為compaction積壓和LSM數結構傾斜。讀請求的Perf context有時候可以顯示一次讀請求開啟了太多的SST檔案。如果發生了這樣的情況應該除錯compaction。

  • Disk Read IOPs 請注意,可以保證合理讀取效能的持續讀取 IOPS 通常遠低於硬體規格。 我們鼓勵使用者透過系統工具測量他們想要使用的讀IOPS(比如fio),並根據此測量檢查系統指標。如果IOPS已經飽和,應該開始檢查compaction。也可以提高block cache命中率。有時候引起問題的是讀index,filter或者大資料block,有不同的方式來處理他們。

  • CPU CPU通常是讀路徑引起的,但是也有可能是compaction引起的。很多選項可能會受到影響,比如compaction,compression,bloom過濾器,block大小等待。

  • 磁碟空間 技術上來說這不是一個瓶頸。但是當系統指標不飽和,效能是足夠好的,我們已經差不多填滿了SSD的空間,我們說它是一個空間瓶頸。如果使用者想透過這個機器服務更多的資料,compaction和compression是需要調優的主要領域。 Space tune 會指導你如何減少磁碟空間的使用。

放大因素

我們用來除錯rocksdb compaction的術語放大因素(amplification factor):寫放大,讀放大和空間放大。這些放大因素將使用者邏輯請求的大小與rocksdb對底層硬體的請求聯絡起來。有時候當我們需要除錯rocksdb的時候哪個因素需要調整是明顯的,但是有時候是不清楚的。不管是哪種情況,compaction都是改變三者之間權衡的關鍵。

寫放大 是寫到儲存的位元組數與寫到資料庫的位元組數的比例。

舉個例子,如果你正在寫 10MB/s的資料到資料庫但是你觀察到磁碟讀寫速率是30MB/s,你的寫放大是3。如果寫放大很高,這個工作負載可能會受限於磁碟吞吐。舉個例子,如果寫放大是50,最大的寫吞吐是500MB/s,你的資料庫可以支援10MB/s的寫速率。在這種情況,減少寫放大會直接增加寫速率。

高寫放大也會減少flash的壽命。有兩種方式可以觀察到你的寫放大。第一種方式是透過DB::GetProperty("rocksdb.stats", &stats)的輸出來讀取。第二種是用你的磁碟寫頻寬去除以資料庫寫速率。

讀放大 是每個請求的讀磁碟數。如果你需要讀取5個頁去響應一個請求,讀放大是5。邏輯讀指從快取獲取資料,包括rocksdb block cache和作業系統 cache。物理讀由flash或者磁碟這些儲存裝置來處理。邏輯讀比物理讀成本低很多,但是還是會影響CPU消耗。你可以從iostat的輸出來評估物理讀的速率,但是這會包含請求和compaction的讀請求。

空間放大 是資料庫檔案在磁碟的大小跟資料大小的比例。如果你放10MB資料到資料庫中,它在磁碟佔用了100MB,那麼空間放大是10。你通常想要設定一個硬限制在空間放大,這樣你不會用完你的磁碟空間或者記憶體。Space tune 會指導你如何減少磁碟空間的使用。

想在不同資料庫演算法的上下文中學習有關這三种放大因素更多資訊,強烈推薦 Mark Callaghan's talk at Highload

系統指標沒有飽和時的慢

通常系統指標沒有飽和,但是rocksdb還是達不到預期的速度。有可能有不同的原因。有一些常見的場景:

  • compaction不夠快 有時候SSD遠沒有飽和但是compacrtion還是趕不上。有可能是rocksdb的compaction受限於compaction並行度的配置,沒有最大化資源使用。預設的配置通常比較低,有很多空間可以改善。見 Parallelism options

  • 寫不夠快 雖然寫入通常受到寫入 I/O 的瓶頸,但在某些情況下,I/O 不夠快,rocksdb 無法以足夠快的速度寫入 WAL 和 memtable。使用者可以嘗試無序寫入、手動 WAL 重新整理和/或將相同的資料分片到多個 DB 並並行寫入它們。

  • 想要更低的讀延遲 又是什麼問題也沒有使用者只是想要更低的讀延遲。可以從透過 Perf Context and IO Stats Context 檢查每個語句的狀態來找出導致延遲高的CPU或者IO問題並嘗試調整響應的選項。

調整flush和compaction

flush和comaction是針對多個瓶頸的重要調優,而且很複雜。可以從 compaction 瞭解rocksdb的compaction是如何工作的。

並行度選項

當compaction之後而磁碟遠沒有飽時,可以試著增加compaction並行度。

在LSM架構,有兩個後臺程式:flush和compaction。兩者可以透過執行緒來併發執行,來利用儲存技術的併發性。flush執行緒在高優先順序的池子,compaction執行緒在低優先順序的池子。增加每個池子的執行緒數量可以透過呼叫:

options.env->SetBackgroundThreads(num_threads, Env::Priority::HIGH);
 options.env->SetBackgroundThreads(num_threads, Env::Priority::LOW);

要想從更多執行緒中獲益你需要設定這些選項來修改併發compaction和flush的最大數量。

max_background_compactions 是後臺compaction最大併發度。預設是1,但是為了充分利用你的CPU和儲存你可能會希望把它增加到系統CPU數和磁碟吞吐除以一個compaction執行緒平均吞吐的最小值。

**max_background_flushes ** 是flush最大併發度,通常設定成1已經足夠好了。

compaction 優先順序(只適用於leveled compaction)

compaction_pri=kMinOverlappingRatio 是rocksdb的預設值,大多數情況已經是最優的了。我們在UDB和msgdb中都是用他,與以前的預設值相比,寫放大下降了一半以上(https://fb.workplace.com/groups/MyRocks.Internal/permalink/1364925913556020/)。

刪除時觸發compaction

當刪除很多行時,一些sst檔案會被墓碑填滿,影響範圍掃描的效能。我們擴充套件了rocksdb的compaction去追蹤長的接近的墓碑。如果發現一個高密度墓碑的key範圍,它會立即觸發另一次compaction來將它們壓縮掉。這幫助了減少因為掃描過多墓碑導致的範圍掃描效能傾斜。在rocksdb,對應的類是CompactOnDeletionCollectorFactory。

定時和TTL compaction

雖然compaction型別有時候通常優先刪除包含更多刪除的檔案,但並不能保證這一點。另外一些選項可以幫助到。options.ttl 指定一個過期的資料會從sst檔案刪除的時間界限。options.periodic_compaction_seconds 保證一個檔案每隔一段時間透過compaction過濾器,這樣compaction 過濾器會刪除相應的資料。

flush 選項

rocksdb的所有寫請求會受限插入到一個記憶體資料結構成為memtable。一旦活躍的memtable(active memtable)被填滿,我們建立一個新的並標記這個就得為只讀。我們稱只讀memtable為不可變的(immutable)。任何時間都只存在一個活躍的memtable和0個或者多個不可變的memtable。不可變的memtable在等待被flush到儲存上。有三個選項控制flush的行為。

write_buffer_size 設定一個memtable的大小。一旦memtyable超出這個大小。它會被標記位不可變的並建立一個新的。

max_write_buffer_number 設定memtable最大的數量,包含活躍的和不可變的。如果活躍的memtable填滿了而且memtable的總數大於max_write_buffer_number我們會停止(stall)繼續寫入。如果flush成語比寫的速率慢會發生這樣的情況。

min_write_buffer_number_to_merge 是在flush到儲存之前要合併的最小memtble數。舉個離職,如果這個選項設定為2,不可變memtable只會在有兩個時才會flush - 一個不可變memtable絕不會被flush。如果多個memtable被合併到一起,有可能會有更少的資料寫到儲存,因為兩個更新被合併到一個key。然而,每次Get()一定會線性遍歷所有的不可變memtable來檢查key是否存在。這個選項設定得臺高容易影響讀效能。

舉例:選項是:

write_buffer_size = 512MB;
max_write_buffer_number = 5;
min_write_buffer_number_to_merge = 2;

寫速率是16MB/s。在這個用例,一個新的memtable會每32秒建立一次,兩個memtable會被合併到一起然後每64秒flush一次。根據工作集大小,flush大小將在 512MB 和 1GB 之間。為了防止flush跟不上寫速率,memtable使用的記憶體上限是5*512MB = 2.5GB。當達到記憶體上限,後續的寫操作會被阻塞住知道flush結束並釋放了memtable使用的記憶體。

Level Style Compaction

詳解見 leveled compation

在level style compaction,資料庫檔案被組織成不同的層次。memtable被flush到0層的檔案,包含最新的資料。更高層包含更老的資料。0層的檔案可能會重疊,但是1層及以上的不會重疊。結果是,Get() 通常餘姚檢查0層的每一個檔案,但是在每個連續的層,不會有超過一個的檔案同時包含一個key。每層比上一層大10倍(倍數可以配置)。

一個compacrtion可能會拿一些N層的檔案和N+1層中重疊的檔案一起壓縮。兩次不同層次或者不同key範圍的compaction操作是獨立的,可以併發執行。compaction速度直接跟最大寫速率成正比。如果compaction不能艮山寫速度,資料庫使用的磁碟空間會持續增加。將rocksdb配置成compaction高併發執行並且充分利用儲存是很重要的。

0層和1層的compaction是個難題。在0層的檔案通常跨越整個key空間。當壓縮L0->L1(0層到1層),compaction包含1次層所有的檔案。當1層所有的檔案正在與0層進行壓縮是,L1->L2的壓縮不能執行;必須等待L0->L1結束。如果L0->L1壓縮很慢,大多數時候只有一個執行中的compaction,因為其他compaction必須等待他結束。

L0->L1 壓縮是單執行緒的。使用單執行緒compaction很難實現好的吞吐。要檢查這是否會導致問題,檢查磁碟利用率。如果磁碟沒有充分使用,有可能在compaction配置除了問題。我們通常建議透過讓0層的大小接近1層大小來讓L0->L1儘可能的快。

一旦你決定1層的合適的大小,你必須決定層間的倍數(level multiplier)。讓我們假設你的1層大小是512MB,level multiplier是10,資料庫大小是500GB。2層的大小會是5GB,3層是51GB,4層是512GB。因為你的資料庫大小是500GB,5層以上會是空的。

大小放大很容易計算。(512 MB + 512 MB + 5GB + 51GB + 512GB) / (500GB) = 1.14。可以這樣計算寫放大:每個位元組首先寫到0層。然後壓縮到1層。因為1層大小跟0層相同,L0->L1的寫放大是2。然而,當一個自己從1層壓縮到2層,它會跟2層的10個位元組(因為2層是1層的10倍)。L2-L3和L3->L4是一樣的。

因此總的寫放大接近於 1 + 2 + 10 + 10 + 10 = 33。點查詢必須查詢所有0層的檔案和其他層最多一個檔案。然而,布隆過濾器大大減少了讀放大。然而,臨時的範圍查詢成本比較大。布隆過濾器無法用於範圍掃描,所以讀放大是number_of_level0_files + number_of_non_empty_levels。

讓我們開始瞭解level compaction的選項。我們會先介紹重要的選項,然後介紹次重要的。

level0_file_num_compaction_trigger -- 0層的檔案達到這個數字會立刻觸發L0->L1的壓縮。因此我們可以將穩定狀態下的0層的大小估計為 write_buffer_size * min_write_buffer_number_to_merge * level0_file_num_compaction_trigger。

max_bytes_for_level_basemax_bytes_for_level_multiplier -- max_bytes_for_level_base是1層總大小。我們建議其大小約為0級的大小。每個連續層是前一個的max_bytes_for_level_multiplier倍大。預設是10,我們不建議去修改它。

target_file_size_basetarget_file_size_multiplier -- 1層的檔案有target_file_size_base個位元組。每層的檔案大小比前一層的大target_file_size_multiplier倍。然而,預設的target_file_size_multiplier 是1,所以L1...Lmax的檔案大小是一樣的。增加target_file_size_base 會減少資料庫的檔案數,一般是一件好事。我們建議設定target_file_size_base為max_bytes_for_level_base / 10,這樣在1層會有10個檔案。

compression_per_level -- 使用這個選項來設定每層的壓縮演算法。通常不會壓縮0層和1層的護甲,只會在更高的層次壓縮資料。你設定可以在最高層設定更慢的壓縮演算法,在低層用更快的壓縮演算法(最高層表示Lmax)。

num_levels -- num_levels大於預期的資料庫層次數量是安全的。一些更高層可能是空的,但是不會影響效能。只有你語氣你的層次數量會大於7層需要修改這個選項(預設是7)。

universal compaction

詳情見 universal compaction

level style compaction 的寫放大有可能會在一些用例很高。在寫請求佔比大的負載,你可能會受限於磁碟吞吐。對這樣的工作負載進行最佳化,rocksdb引入一個新的compaction形勢稱為 universal compaction,想要減少寫放大。然而,它可能會增加讀放大並且一直增加空間放大。 universal compaction有一個大小限制。當你的資料庫(或者列族)大小大於100GB請小心。可以透過universal compaction 瞭解更多。

使用universal compaction,一個壓縮排程有可能臨時讓大小放大翻倍。換而言之,如果你存10GB到資料庫,compaction程式除了空間放大有可能消費額外的10GB。

然而,有一些技術幫助減少臨時的空間防備。如果你使用universal compaction,我們強烈建議給你的資料分片並儲存到不同的rocksdb例項中。讓我們假設你有S個分片。配置Env執行緒池,使壓縮執行緒只有N個。S個分片中只有N個分片會有額外的空間放大,這將額外的空間放大從1降為了N/s。舉個例子,如果你的資料庫是10GB你配置成100個分片,麼個分片包含100MB資料。如果你配置你的執行緒池為20個併發的壓縮執行緒,你會只消耗額外的2GB而不是10GB,compaction會並行執行,充分利用你的儲存併發度。

max_size_amplification_percent -- 大小放大,定義為儲存一個位元組到資料庫需要的額外空間(百分比)。預設是200,意味著100位元組的資料庫會需要300位元組的儲存。300位元組中的200位元組是臨時的,只會在compaction的時候使用。增加這個限制會減少寫放大,但是(顯然)會增加空間放大。

compression_size_percent -- 資料庫壓縮的資料的百分比。更老的資料會被壓縮,更新的資料不會被壓縮。如果設定為-1(預設值),所有的資料會被壓縮。減少compression_size_percent會減少CPU使用率並增加空間放大。

調優其他選項

通用選項

filter_policy -- 如果你正在在一個未壓實的資料庫進行點查詢,你肯定會想要開啟布隆過濾器。我們使用布隆過濾器來避免不必要的磁碟讀。你應該設定filter_policy為rocksdb::NewBloomFilterPolicy(bits_per_key)。預設的bits_per_key是10,這會產生約1%的誤報率。更大的bits_per_key會減少誤報率,但是會增加記憶體使用和空間放大。

block_cache -- 我們通常建議將這個配置設定成rocksdb::NewLRUCache(cache_capacity, shard_bits)的結果。block cache 快取解壓後的block。另一方面作業系統快取壓縮後的block(因為這是儲存在檔案的方式)。所以有理由同時使用block_cache和作業系統快取。我們需要在進入block cache的時候加鎖,所以有時候我們會看到rocksdb的瓶頸在於block cache的mutex,特別是資料庫大小比記憶體小。這種情況下,有理由透過設定shard_bits成一個更大的數字來對block cache分片。如果shard_bits是4,分片的總數是16。

allow_os_buffer -- [已棄用] 如果設定為false,我們不會在系統快取快取檔案。

**max_open_files ** -- rocksdb在table cache儲存所有的檔案描述符。如果檔案描述符超過了max_open_files,一些檔案會從table cache中淘汰並關閉它們的檔案描述符。這表示每次讀請求必須透過table cache找到需要的檔案描述符。設定max_open_files為-1會一直開啟所有的檔案,避免高成本的table cache呼叫。

table_cache_numshardbits -- 這是用來控制table cache分配的選項。如果table cache的mutex有影響可以增加這個選項。

**block_size ** -- rocksdb打包使用者資料到block中。當從一個tabel檔案讀一個key-value對是,會載入整個block到記憶體中 。block size預設是4K。每個table檔案包含一個所有block的索引。增加block_size 表示索引會包含更少的條目(因為每個檔案的block更少了),所以會更小。增加block_size會減少記憶體使用和空間放大,但是增加讀放大。

共享快取和執行緒池

又是你可能會希望在相同的程式裡執行多個rocksdb的例項。rocksdb提供這些例項共享block cache和執行緒池的方法。要共享block cache,將一個cache物件賦值給所有的例項:

first_instance_options.block_cache = second_instance_options.block_cache = rocksdb::NewLRUCache(1GB)

這樣兩個例項都共享了一個總大小為1GB的block cache。

執行緒池是跟Env例項關聯的。當你構建Options的時候,options.env預設會設定為Env::Default(),大多數情況是最好的。因為所有的選項使用相同的靜態物件Env::Default(),執行緒池預設就是共享的。透過這種方式,你可以設定compaction和flush的執行數量上限,及時是執行多個rocksdb例項的時候。

Write stalls

詳情見 Write stalls

字首資料庫

rocksdb儲存所有資料並支援有序的迭代(iteration)。但是,某些應用程式不需要對key進行完全排序。他們只想要使用公共字首排序key。

這些應用會從設定資料庫的prefix_extractor獲利。

prefix_extractor -- 定義key字首的SliceTransform 物件。key字首可以用來實現一些有趣的最佳化:

  1. 定義字首布隆過濾器,可以減少字首範圍查詢的讀放大(例如,拉取字首為xxx的全部key)。請一定要設定Options::filter_policy
  2. 使用hash-map-based memtable來避免在memtable中二分查詢的消耗。
  3. 新增hash索引到table檔案來避免在table檔案中的二分查詢。

想了解更多關於1和2的細節,可以看 Custom memtable and table factories 。請注意1在減少IO上通常已經足夠了。2和3可以在某些場景下減少CPU消耗,但是常常會消耗一定的記憶體。你應該只有在CPU是你的瓶頸,並且你已經使用了所有更容易的方式去減少CPU消耗的時候才嘗試去調整它,這並不常見。確保檢查在include/rocksdb/options.h中關於prefix_extractor的註釋。

布隆過濾器

布隆過濾器(Bloom filters)是用來檢查一個元素是否屬於一個集合的一部分的機率資料結構。在rocksdb的布隆過濾器透過filter_policy選項來控制。當一個使用者小於Get(key)的時候,會有一系列檔案可能包含這個key。這通常是所有在0層的檔案和其他層每層一個檔案。然而在我們讀每個檔案之前,我們先諮詢布隆過濾器。布隆過濾器會過濾掉大多數不包含key的檔案的讀取。在大多數情況下,Get()只需要讀一個檔案。布隆過濾器始終儲存在記憶體中以供開啟的檔案使用,除非BlockBasedTableOptions::cache_index_and_filter_blocks設定成了true。大家檔案的數量由max_open_files 選項來控制。

有兩種型別的布隆過濾器:block-based和全過濾。

block-based 過濾器(已棄用)

可以透過呼叫options.filter_policy.reset(rocksdb::NewBloomFilterPolicy(10, true))設定基於block的過濾器。block-based布隆過濾器為每個block單獨構建。一個讀請求我們首先諮詢索引,索引會返回我們查詢的block。現在我們拿到了block,我們再去諮詢這個block的布隆過濾器。

全過濾

可以透過呼叫options.filter_policy.reset(rocksdb::NewBloomFilterPolicy(10, false))設定全過濾。全過濾是每個檔案構建一個。每個檔案只有一個布隆過濾器。這表示我們可以繞開索引先去諮詢布隆過濾器。與block-based布隆過濾器相比,在key不在布隆過濾器的情況,我們節省了一次查詢索引的時間。

全過濾可以被進一步進行分割槽: Partitioned Filters

自定義memtable和table格式

高階使用者可以配置自定義memtable和table的格式。

**memtable_factory ** -- 定義memtable。這裡有我們支援的memtable的列表:

  1. SkipList -- 預設memtable
  2. HashSkipList -- 只有 prefix_extractor 才有意義。它會根據key的字首儲存key到不同的桶(bucket)中。每個桶是一個跳錶。
  3. HashLinkedList 只有 prefix_extractor 才有意義。它會根據key的字首儲存key到不同的桶(bucket)中。每個桶是一個連結串列。

**table_factory ** -- 定義table格式。這裡有我們支援的table的列表:

  1. Block based -- 這是預設的table。這適用於吧資料儲存到磁碟和flash儲存。它使用block大小的塊來定址和載入(見block_size 選項)。所以叫Block based。
  2. Plain Table -- 只有 prefix_extractor 才有意義。適合存資料到記憶體(tmpfs檔案系統)。它是位元組可定址。

記憶體使用

學習更多可以看https://github.com/facebook/rocksdb/wiki/Memory-usage-in-RocksDB

機械硬碟的區別

Tuning RocksDB on Spinning Disks

配置示例

在這個章節我們會展示一些我們實際在生產環境使用的rocksdb的配置。

在flash儲存的字首資料庫

這個服務使用rocksdb來實現字首範圍查詢和點查詢,它是執行在flash儲存的。

 options.prefix_extractor.reset(new CustomPrefixExtractor());

因為這個服務不需要完整的優先迭代(見 Prefix databases ),我們定義字首提取器(extractor)。

rocksdb::BlockBasedTableOptions table_options;
table_options.index_type = rocksdb::BlockBasedTableOptions::kHashSearch;
table_options.block_size = 4 * 1024;
options.table_factory.reset(NewBlockBasedTableFactory(table_options));	

我們使用table檔案中的hash索引來加速字首查詢,但是它會增加儲存空間和記憶體的使用。

 options.compression = rocksdb::kLZ4Compression;

LZ4壓縮減少CPU使用,但是會增加儲存空間。

 options.max_open_files = -1;

這個設定禁用在table cache中查詢檔案,這會加速所有的請求。如果你的server的開啟檔案數上限比較大,這樣設定是好事。

options.options.compaction_style = kCompactionStyleLevel;
options.level0_file_num_compaction_trigger = 10;
options.level0_slowdown_writes_trigger = 20;
options.level0_stop_writes_trigger = 40;
options.write_buffer_size = 64 * 1024 * 1024;
options.target_file_size_base = 64 * 1024 * 1024;
options.max_bytes_for_level_base = 512 * 1024 * 1024;

我們使用level compaction。memtable大小是64MB,會定時flush導0層。compaction L0->L1在有10個0層的檔案時觸發(一共640MB)。當L0是640MB,會觸發壓縮到最大大小是512MB的L1。總資料庫大小???

options.max_background_compactions = 1
options.max_background_flushes = 1

任何時候只有一個併發的compactrion和一個flush在執行。然而,在系統中有多個分片,所以多個compaction會出現在不同的分配。否則,只有兩個執行緒寫到儲存不會讓儲存得到充分使用。

 options.memtable_prefix_bloom_bits = 1024 * 1024 * 8;

使用memtable布隆過濾器可以避免一些對memtable的訪問。

options.block_cache = rocksdb::NewLRUCache(512 * 1024 * 1024, 8);

block cache 配置成了512MB。(是所有分配共享的嗎?)

全排序資料庫,flash儲存

這個資料庫同時執行Get()和全排序迭代。分片?

options.env->SetBackgroundThreads(4);

我們先設定執行緒池的執行緒數為4。

options.options.compaction_style = kCompactionStyleLevel;
options.write_buffer_size = 67108864; // 64MB
options.max_write_buffer_number = 3;
options.target_file_size_base = 67108864; // 64MB
options.max_background_compactions = 4;
options.level0_file_num_compaction_trigger = 8;
options.level0_slowdown_writes_trigger = 17;
options.level0_stop_writes_trigger = 24;
options.num_levels = 4;
options.max_bytes_for_level_base = 536870912; // 512MB
options.max_bytes_for_level_multiplier = 8;

我們使用高併發度的level compaction。memtable大小是64MB,0層檔案數量是8。這便是compaction在L0的大小增長到512MB的時候被觸發。L1的大小是512MB,每層比上一層大8倍。L2是4GB,L3是32GB。

機械硬碟的資料庫

功能齊全的記憶體資料庫

在這個例子中,資料庫是安裝在tmpfs檔案系統。

使用mmap讀:

options.allow_mmap_reads = true;

禁用block cache,啟用布隆過濾器並減少增量編碼重啟間隔:

BlockBasedTableOptions table_options;
table_options.filter_policy.reset(NewBloomFilterPolicy(10, true));
table_options.no_block_cache = true;
table_options.block_restart_interval = 4;
options.table_factory.reset(NewBlockBasedTableFactory(table_options));

如果你想要速度優先,你可以禁用壓縮:

options.compression = rocksdb::CompressionType::kNoCompression;

或者,啟用一個 輕量級的壓縮,LZ4或者snappy。

更積極地設定壓縮併為重新整理和compaction分配更多的執行緒:

options.level0_file_num_compaction_trigger = 1;
options.max_background_flushes = 8;
options.max_background_compactions = 8;
options.max_subcompactions = 4;

保持所有的檔案開啟:

options.max_open_files = -1;

當讀資料的時候,考慮設定ReadOptions.verify_checksums = false。

記憶體字首資料庫

在這個離職中,資料庫是安裝在tmpfs檔案系統。我們使用自定義格式來加速,有一些功能是不支援的。我們只支援Get()和字首範圍掃描。預寫日誌(Write-ahead logs,WAL)儲存在硬碟避免在查詢以外的地方消耗記憶體。Pre()不支援。

因為資料庫是在記憶體的,我們不關心寫放大。我們更關心讀放大和空間放大。這是個有趣的離職,因為我們將compaction調到一個極端,系統中通常只會有一個sst table。我們隱藏減少讀和空間放大,寫放大非常大。

因為使用了通用compaction( universal compaction),我們將在壓縮期間有效地增加一倍我們的空間使用量。這在記憶體資料庫是非常危險的。我們隱藏將資料分片成400個rocksdb例項。我們允許只有兩個併發的compaction,這樣在一個時間只有兩個分片可能會使空間翻倍。

在這個例子,字首hash可以用來執行系統使用hash索引代替二進位制索引,已經在可能的情況下使用布隆過濾器來迭代:

options.prefix_extractor.reset(new CustomPrefixExtractor());

使用為低延遲訪問構建的記憶體定址table格式,這需要開啟mmap讀取模式:

options.table_factory = std::shared_ptr<rocksdb::TableFactory>(rocksdb::NewPlainTableFactory(0, 8, 0.85));
options.allow_mmap_reads = true;
options.allow_mmap_writes = false;

使用hash連結串列memtable來將記憶體table查詢從二分查詢改為hash查詢:

options.memtable_factory.reset(rocksdb::NewHashLinkListRepFactory(200000));

為hash table啟用布隆過濾器來減少key不存在在記憶體table時的記憶體訪問(通常意味著 CPU 快取未命中):

options.memtable_prefix_bloom_bits = 10000000;
options.memtable_prefix_bloom_probes = 6;

設定compaction,只要有兩個檔案就開始全量壓縮(full compaction):

options.compaction_style = kUniversalCompaction;
options.compaction_options_universal.size_ratio = 10;
options.compaction_options_universal.min_merge_width = 2;
options.compaction_options_universal.max_size_amplification_percent = 1;
options.level0_file_num_compaction_trigger = 1;
options.level0_slowdown_writes_trigger = 8;
options.level0_stop_writes_trigger = 16;

設定布隆過濾器最小化記憶體訪問:

options.bloom_locality = 1;

所有表的redser物件一直在快取中,避免在讀的時候訪問table cache:

options.max_open_files = -1;

一次使用一個記憶體table。它的大小取決於我們能接受的全量壓縮的間隔。我們調整compaction成每次flush都會觸發一次全量compaction,這會消耗cpu。memtable大小越大,compaction的間隔越長,同時我們看到記憶體效率越低,查詢效能越差,重啟DB時間恢復時間越長。

options.write_buffer_size = 32 << 20;
options.max_write_buffer_number = 2;
options.min_write_buffer_number_to_merge = 1;

多個資料庫分片共享限制為2的compaction執行緒池:

options.max_background_compactions = 1;
options.max_background_flushes = 1;
options.env->SetBackgroundThreads(1, rocksdb::Env::Priority::HIGH);
options.env->SetBackgroundThreads(2, rocksdb::Env::Priority::LOW);

設定WAL日誌:

options.bytes_per_sync = 2 << 20;

memory block table的建議

hash_index:在新的版本,hash索引是為block base table啟用的。相比於二分查詢索引,它會使用5%的額外空間但是會提升隨機讀50%的速度。

table_options.index_type = rocksdb::BlockBasedTableOptions::kHashSearch;

block_size:預設的,這個值設定為4K。如果壓縮是開啟的,更小的blcok size會導致更高的隨機讀速度因為解壓的開銷減少了。但是block size不能設定得太小導致壓縮失效了。建議設定成1k。

verify_checksum:我們儲存資料到tmpfs並且更多關係讀效能,校驗和可以禁用。

最後

不幸的是,調優rocksdb不是簡單的事情。及時我們作為rocksdb的開發者也不完全知道每個配置的修改會帶來的影響。如果你想要完全為你的工作負載進行最佳化,我們建議實驗和benchmark,同時關注三個放大因素。同時,請不要猶豫來 RocksDB Developer's Discussion Group 找我們尋求幫助。

相關文章