存儲引擎的基本功能和數據結構html
一個存儲引擎須要實現三個基本的功能:git
上述的存儲引擎和普通的哈希表不一樣。最大的區別就是存儲引擎內要求數據的存儲順序是按照key有序的。這比哈希表更節省空間,也容易實現scan()操做。github
乍一看使用普通的有序數組好像就能夠解決問題啦,可是普通的有序數組也有個問題:當一個新元素要write插入進來時,爲保證數組有序,須要把後面的數據都移動一位,這樣開銷是很大的。算法
還有一種有序的結構叫作平衡二叉樹。若是把數據有序放入平衡二叉樹好像也不是不行。可是平衡二叉樹會佔用不少的額外空間(用於存放節點指針),另外局部性不好,讀性能(read/scan)低。數據庫
(在OS的頁面置換這一節中咱們學過工做集的概念,其實這個和局部性很像。硬件、操做系統等等系統,絕大部分時候,執行一次 操做流程會有額外的開銷(overhead)。所以不少部件、模塊都設計成:連續執行相似或相同 的操做、訪問空間相鄰的內容時,則將屢次操做合併爲一次,或屢次之間共享上下文信息。這樣能極大提高性能。這種時間、空間上的連續性,叫作局部性。)數組
那用什麼數據結構纔好呢?能夠考慮把數組和二叉樹結合一下,把平衡二叉樹的每一個節點都改爲一片數組,作成一個大葉節點樹。這樣一方面經過把拆分紅若干小數組,減小了數組插入時的開銷(寫操做)。另外一方面,擴大了二叉樹中每一個節點的大小,增長了讀操做的局部性,改善了scan的性能。那具體每一個節點的數組要多大才好呢?這就要根據需求進行trade off啦。緩存
在實踐中不少存儲引擎會使用B+ Tree做爲存儲結構(好比MYSQL):安全
B+ Tree的靈魂基本就是上述的大葉節點樹。具體細節可參考http://www.javashuo.com/article/p-dgncxeef-cp.html服務器
存儲引擎的持久化數據結構
爲了保證好比關機重啓以後數據仍然能夠繼續使用,咱們須要把數據保存到硬盤上。但硬盤有如下幾個特色:
所以硬盤上存儲引擎的設計和以前要截然不同。WAL(Write Ahead Log)就是一種成熟的解決方案。它是一種異構鏡像方案(也叫作semi-DB):
前面說的好抽象啊......其實WAL能夠理解成是一個log文件,寫 WAL 都在末尾追加寫入,順序地記錄全部修改動做(類比數據庫系統的日誌)。爲了存盤數據的安全,避免進程非正常退出丟數據,WAL 通常每次寫完數據都執行 fsync 操做,不然數據可能還留在操做系統的 Page Cache 中沒有寫到盤上(不實時fsync會有丟失數據的風險,但fsync很佔磁盤資源,可能成爲性能瓶頸。所以數據庫系統會提供參數設置fsync的頻率)
WAL工做時其實就是[傻傻的]依次記錄每次的寫操做,但這樣效率也不高:1. WAL 中可能存在相同 key 的屢次 Write 的多個版本的數據,佔用了 額外空間,也下降重放性能。2. WAL中記錄的寫入操做太多時,總體效率也會下降。 爲解決這些問題,咱們能夠設計一個機制,在某些特定的時刻將WAL記錄的全部操做作成一個快照(即至關於提早執行了到目前爲止全部的WAL record,並將數據存盤)。這樣既提升了重啓時重放WAL的效率,也節省了空間。這個機制就叫作Compaction。compaction過程會佔用一些IO資源,好比用戶只插入了k GB的數據,因爲compaction的存在,硬盤總共會執行大於k GB的IO寫操做。這個問題就叫作寫放大。假如硬盤是SSD,寫放大太嚴重就會影響硬盤的壽命。compaction其實就是以寫放大做爲代價,換取更好的讀取性能。
按照上面的方案讓WAL和內存中的B+ tree配合,看起來就很完美啦!可是別忘了內存空間是有限的,不可能全部的寫操做都能丟進內存。因此內存中就只能存放部分數據(至關於一個cache),硬盤中才存放全部數據。
另外,從硬盤向內存讀數據也是須要較好的局部性的(還記得連續寫入比隨機寫入快得多嘛?)。所以在實際操做時,咱們在硬盤的WAL中,以B+tree中的葉節點大小做爲單位存儲,爲B+tree的每一個葉節點都啓用WAL。內存中的B+tree在讀取時,遇到當前不在內存的葉節點時,就去硬盤加載(相似於虛擬內存中遇到缺頁中斷的處理機制)。如圖所示:
上圖的結構中,B+ tree的每一個葉子節點都有一個WAL。當葉子節點不少的時候這樣也不大好....若是compaction的頻率很高,並且WAL作compaction時,數據能夠從內存得到,那麼真正須要從WAL讀數據的機會就不多。這樣咱們能夠把一些葉子節點的WAL合併起來,以提升局部性。(具體實現暫時略)
B+ tree存儲引擎分析與改進
通過上面這一頓操做後,咱們暫時就有了這樣一個存儲引擎:
這個模型就很好了咩?咱們來分析一下:
另外,若是有奇怪的用戶在不一樣的key值域上隨機寫入(可能每一個key值域上寫入量很小,但會寫不少不一樣的key值域),那麼WriteCache就很難覆蓋全部用戶寫過的key值域。爲了騰出writecache,葉節點必須在修改佔比還很小的時候,就compact寫盤。在這種狀況下會形成巨大的寫放大,還會形成寫盤次數相對於總寫入量過多(全是分散IO,寫入效率就比較低)。其根本緣由是B+tree中,每一個葉子節點覆蓋的key範圍過小啦。並且存量數據越大,葉節點的key覆蓋範圍越窄。
另外,B+ tree的葉節點是分散存儲在硬盤上的,也致使屢次IO之間不存在連續性。
那麼怎麼辦捏?咱們能夠用另外一種局部性好的有序結構,叫作LSM Tree。這也就是RocksDB所用的結構。
LSM Tree
LSM Tree長醬紫:
各個小有序數組的key覆蓋範圍是相互重疊的,它們合併起來能夠看作一個大的虛擬有序數組。同時由於範圍是重疊的,所以某個key有可能會在多個小數組上都存在,所以不一樣數組設置了不一樣的優先級。
這樣設計既採納了B+Tree中將數組分散存儲以防止寫開銷太大的問題,又能夠保證每一個小數組都有局部性。
LSM Tree的Read操做:最簡單的思路是按優先級從高到低,二分查找每一個小數組。但這樣會存在讀放大問題(找了好屢次才找到對應的小數組)。爲解決這一問題,咱們能夠在數組生成時,對每一個小數組都作一個Bloom Filter(能夠理解爲一個高效率的hashset)來記錄當前小數組裏都有哪些key。在讀操做時先查Bloom Filter,若是不存在就不須要二分查找這個小數組了。
注意若是要讀取暫存在硬盤上的小有序數組:由於這個數組仍是比較大的,因此不能像B+Tree那樣直接全load到內存再二分查找。對於硬盤上的數組文件,能夠把它分紅多個小的block。維護一個Bloom Filter記錄每一個key在哪一個block,還有一個索引記錄每一個block的範圍信息[begin, end]。讀取到內存時以block做爲單位。
LSM Tree的Scan操做:找到全部覆蓋了begin、end範圍的小數組,而後進行多路合併(merge k sorted array)。對於重複的key,取優先級高的數組裏的元素。
LSM Tree的Write操做:分爲兩部分: 1. 純內存的LSM Tree:write只插入到最上層的有序結構(最上層使用其餘的有序結構而不是小有序數組啦,來避免插入時要移動其餘元素的問題)。當最上層過大時將最上層下移一層,而後生成一個新的最上層。(這樣一來,前面的優先級其實就成了根據寫入時間重新到舊排序啦) 2. 磁盤的LSM Tree的write:和內存的基本一致,只是爲最上層的有序結構加一個WAL防止數據丟失。
LSM Tree的Compaction
前面說到write會不停產生新數組,而數組個數太多了會影響scan/read的性能,所以LSM Tree也須要Compaction操做,把若干個小數組合併成一個新的有序數組,從而控制數組的個數不能太多。
LSM Tree有不少種Compaction策略。最簡單的策略就是把相鄰T層的數組進行合併。因爲Compaction的次數不一樣,就會造成相應的多層結構。以下圖(這裏T=3)
(上面只是一個最簡單的Compaction策略,具體優化以及在RocksDB中的實現還涉及不少細節,暫時忽略)
RocksDB中的LSM Tree
上圖中,SST至關於以前提到的小有序數組,MemTable至關於LSM Tree的數據在內存中的Cache。
每層Level的意義至關於對數據按新舊順序進行了時域切割。以下圖:
LSM Tree解決了B+Tree中攢批不足帶來的寫放大(參考B+Tree那一段中 某個奇怪的用戶的操做) ,但帶來的代價就是層層Compaction帶來的新的寫放大。因此說一個複雜的系統須要大量的取捨和平衡叭
RocksDB的項目起源於Facebook的一個實驗,但願可以開發一個高效的數據庫實現可以在快速存儲設備(特別是Flash)上存儲數據並服務服務器的負載,同時徹底挖掘這類存儲設備的潛能。RocksDB是一個C++庫用於存儲kv數據而且支持原子讀寫。RocksDB實現了在配置上的較高的靈活性而且能夠運行到各類生產環境中,包括純內存、Flash、HDD或者HDFS。RocksDB支持多種壓縮算法以及多種工具用於生產支持以及debug。RocksDB借用了許多LevelDB的代碼以及Apache HBase中的思想。最初是基於LevelDB1.5開發。
RocksDB是一個嵌入式的K-V(任意字節流)存儲。全部的數據在引擎中是有序存儲,能夠支持Get(key)、Put(Key)、Delete(Key)和NewIterator()。RocksDB的基本組成是memtable、sstfile和logfile。
RocksDB中的key和value徹底是byte stream,key和value的大小沒有任何限制。Get接口提供用戶一種從DB中查詢key對應value的方法,MultiGet提供批量查詢功能。DB中的全部數據都是按照key有序存儲,其中key的compare方法能夠用戶自定義。Iterator方法提供用戶RangeScan功能,首先seek到一個特定的key,而後從這個點開始遍歷。Iterator也能夠實現RangeScan的逆序遍歷,當執行Iterator時,用戶看到的是一個時間點的一致性視圖。
Fault Torlerance
RocksDB經過checksum來檢測磁盤數據損壞。每一個sst file的數據塊(4k-128k)都有相應的checksum值。寫入存儲的數據塊內容不容許被修改。
Multi-Threaded Compactions
當用戶重複寫入一個key時,在DB中會存在這個key的多個value,compaction操做就是來刪除這個key的冗餘數據。當一個key被刪除時,compation也能夠用來真正執行這個底層數據的刪除工做,若是用戶配置合適的話,compation操做能夠多線程執行。DB的數據都存儲在sstfile中,當內存表的數據滿的時候,會將內存數據(去重、刪除無效數據後)寫入到L0 文件中。每隔一段時間小文件中的數據會從新merge到更大的文件中,這就是compation。LSM引擎的寫吞吐直接依賴於compation的性能,特別是數據存儲在SSD或者RAM的狀況。
RocksDB也支持多線程並行compaction。後臺的compaction線程用來將內存數據flush到存儲,當全部的後臺線程都正在執行compaction時,瞬時大量寫操做會很快將內存表寫滿,這就會引發寫停頓。能夠配置少一些的線程用於執行數據flush操做,
Block Cache -- Compressed and Uncompressed Data
RocksDB使用LRU cache提供block的讀服務。block cache partition爲兩個獨立的cache,其中一塊能夠cache未壓縮RAM數據,另外一塊cache 壓縮RAM數據。若是壓縮cache配置打開的話,用戶通常會開啓direct io,以免OS的也緩存從新cache相同的壓縮數據。
可用配置
不管是在option string仍是option map中,option name是目標類中的變量名,這些包括:DBOptions, ColumnFamilyOptions, BlockBasedTableOptions, or PlainTableOptions。DBOptions and ColumnFamilyOptions中的變量名和變量描述信息能夠在options.h中找到,BlockBasedTableOptions, and PlainTableOptions中的變量信息能夠在table.h中找到。須要注意的是,儘管絕大部分的配置項均可以在option string和option map中支持,仍然有一些例外。RocksDB支持的全部配置項能夠在db_options_type_info, cf_options_type_info and block_based_table_type_info中查閱,源文件是util/options_helper.h。
LSM-Tree
RocksDB 是基於 LSM-Tree 的,大概以下
sst文件是在硬盤上的。SST files按照key 排序,且每一個文件的key range互相不重疊。爲了check一個key可能存在於哪個一個SST file中,RocksDB並無依次遍歷每個SST file而後去檢查key是否在這個file的key range 內,而是執行二分搜索算法(FileMetaData.largest )去定位這個SST file。(更詳細能夠參考https://yq.aliyun.com/articles/669316)
首先,任何的寫入都會先寫到 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。對於 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。
LSM-Tree 能將離散的隨機寫請求都轉換成批量的順序寫請求(WAL + Compaction),以此提升寫性能。但也帶來了一些問題:
RocksDB 和 LevelDB 經過後臺的 compaction 來減小讀放大(減小 SST 文件數量)和空間放大(清理過時數據),但也所以帶來了寫放大(Write Amplification)的問題。
在 HDD 做爲主流存儲的時代,RocksDB 的 compaction 帶來的寫放大問題並無很是明顯。這是由於:
如今 SSD 逐漸成爲主流存儲,compaction 帶來的寫放大問題顯得愈來愈嚴重:
因此,在 SSD 上,LSM-Tree 的寫放大是一個很是值得關注的問題。而寫放大、讀放大、空間放大,三者就像 CAP 定理同樣,須要作好權衡和取捨。
Ref:https://cloud.tencent.com/developer/article/1352666
RocksDB 的寫放大分析:
+1 - redo log 的寫入
+1 - Immutable Memtable 寫入到 L0 文件
+2 - L0 和 L1 compaction(L0 SST 文件的 key 範圍是重疊的,出於性能考慮,通常儘可能保持 L0 和 L1 的數據大小是同樣的,每次拿全量 L0 的數據和全量 L1 的數據進行 compaction)
+11 - Ln-1 和 Ln 合併的寫入(n >= 2,默認狀況下,Ln 的數據大小是 Ln-1 的 10 倍,見max_bytes_for_level_multiplier )。
因此,總的寫放大是 4 + 11 * (n-1) = 11 * n - 7 倍。關鍵是 n 的取值。
假設 max_bytes_for_level_multiplier 取默認值 10,則 n 的取值受 L1 的大小和 LSM-Tree 的大小影響。
L1 的大小由 max_bytes_for_level_base 決定,默認是 256 MB。
默認狀況下 L0 的大小和 L1 同樣大,也是 256 MB。不過 L0 比較特殊,當 L0 的 SST 文件數量達到 level0_file_num_compaction_trigger 時,觸發 L0 -> L1 的 comapction。因此 L0 的最大大小爲 write_buffer_size * min_write_buffer_number_to_merge * level0_file_num_compaction_trigger。
write_buffer_size 默認 64 MB
min_write_buffer_number_to_merge 默認 1
level0_file_num_compaction_trigger 默認 4
因此 L0 默認最大爲 64 MB * 1 * 4 = 256 MB
所以,RocksDB 每一層的默認大小爲 :
L0 - 256 MB
L1 - 256 MB
L2 - 2.5 GB
L3 - 25 GB
L4 - 250 GB
L5 - 2500 GB
Tiered Compaction vs Leveled Compaction
你們應該都知道,對於 LSM 來講,它會將寫入先放到一個 memtable 裏面,而後在後臺 flush 到磁盤,造成一個 SST 文件,這個對寫入實際上是比較友好的,但讀取的時候,極可能會遍歷全部的 SST 文件,這個開銷就很大了。同時,LSM 是多版本機制,一個 key 可能會被頻繁的更新,那麼它就會有多個版本留在 LSM 裏面,佔用空間。
爲了解決這兩個問題,LSM 會在後臺進行 compaction,也就是將 SST 文件從新整理,提高讀取的性能,釋放掉無用版本的空間,一般,LSM 有兩種 Compaction 方式,一個就是 Tiered,而另外一個則是 Leveled。
上圖是兩種 compaction 的區別,當 Level 0 刷到 Level 1,讓 Level 1 的 SST 文件達到設定的閾值,就須要進行 compaction。對於 Tiered 來講,咱們會將全部的 Level 1 的文件 merge 成一個 Level 2 SST 放在 Level 2。也就是說,對於 Tiered 來講,compaction 其實就是將上層的全部小的 SST merge 成下層一個更大的 SST 的過程。
而對於 Leveled 來講,不一樣 Level 裏面的 SST 大小都是一致的,Level 1 裏面的 SST 會跟 Level 2 一塊兒進行 merge 操做,最終在 Level 2 造成一個有序的 SST,而各個 SST 不會重疊。
上面僅僅是一個簡單的介紹,你們能夠參考 ScyllaDB 的兩篇文章 Write Amplification in Leveled Compaction,Space Amplification in Size-Tiered Compaction,裏面詳細的說明了這兩種 compaction 的區別。
Block Cache是RocksDB把數據緩存在內存中以提升讀性能的一種方法。開發者能夠建立一個cache對象並指明cache capacity,而後傳入引擎中。cache對象能夠在同一個進程中供多個DB Instance使用,這樣開發者就能夠經過配置控制全部的cache使用。Block cache存儲的是非壓縮的數據塊內容。用戶也能夠設置另一個block cache來存儲壓縮數據塊。讀數據時首先從非壓縮數據塊cache中讀數據、而後讀壓縮數據塊cache。當Direct-IO打開的話,壓縮數據庫能夠做爲系統頁緩存的替代。RocksDB中有兩種cache的實現方式,分別爲LRUCache和CLockCache。這兩種cache都會被分片,來下降鎖壓力。用戶設置的容量平均分配給每一個shard。默認狀況下,每一個cache都會被分片爲64塊,每塊大小不小於512K字節。
LRU Cache
默認狀況,RocksDB使用LRU Cache,默認大小爲8M。cache的每一個分片都有本身的LRU list和hash表來查找使用。每一個shard都有個mutex來控制數據併發訪問。無論是數據查找仍是數據寫入,線程都要獲取cache分片的鎖。開發中也能夠調用NewLRUCache()來建立一個LRU cache。這個函數提供了幾個有用的配置項來設置cache:
Capacity cache的總大小
num_shard_bits 去cache key的多少字節來選擇shard_id。cache將會被分片爲2^num_shard_bits
strict_capacity_limit 不多會出現block cache的size超過容量的狀況,這種狀況發生在持續不斷的read or iteration 訪問block cache,pinned blocks的總大小會超過容量。若是有更多的讀請求將block數據寫入block cache時,且strict_capacity_limit=false(default),cache服務會不遵循容量限制並容許寫入。若是host沒有足夠內存的話,就會致使DB instance OOM。若是將這個配置設置爲true,就能夠拒絕將更多的數據寫入cache,fail掉那些read or iterator。這個參數配置是以shard爲控制單元的,因此會出現某一個shard在capcity滿時拒絕繼續寫入cache,而另外一個shard仍然有extra unpinned space。
high_pri_pool_ratio 爲高優先級block預留的capacity 比例
Clock Cache
ClockCache實現了CLOCK算法。CLOCK CACHE的每一個shard都有一個cache entry的圓環list。算法會遍歷圓環的全部entry尋找unspined entry來回收,可是若是上次scan操做這個entry被使用的話,也會有繼續留在cache中的機會。尋找並回收entry使用tbb::concurrent_hash_map。
使用LRUCache的一個好處是有一把細粒度的鎖。在LRUCache中,即便是查找操做也須要獲取分片鎖,由於有可能會更改LRU-list。在CLock cache中查找並不須要獲取分片鎖,只須要查找當前hash_map就能夠了,只有在insert時須要獲取分片鎖。使用clock cache,相比於LRU cache,寫吞吐有必定提高。
當建立clock cache時,也有一些能夠配置的信息。
Capacity same as LRUCache
num_shard_bits same as LRUCache
strict_capacity_limit same as LRUCache
Simulated Cache
SimCache是當cache capacity或者shard num發生改變時預測cache hit的方法。SimCache封裝了真正的Cache 對象,運行一個shadow LRU cache模仿具備一樣capacity和shard num的cache服務,檢測cache hit和miss。這個工具在下面這種狀況頗有用,好比:開發者打開了一個DB 實例,配置了4G的cache size,如今想知道若是將cache size調整到64G時的cache hit。
SimCache的基本思想是根據要模擬的容量封裝正常的block cache,可是這個封裝後的block cache只有key,沒有value。當插入數據時,把key插入到兩個cache中,可是value只插入到normal cache。value的size會在兩種cache中都計算進去,可是SimCache中由於只有key,因此並無佔用那麼多的內存,可是以此卻能夠模擬block cache的一些行爲。
MemTable是一種在內存中保存數據的數據結構,而後再在合適的時機,MemTable中的數據會flush到SST file中。MemTable既能夠支持讀服務也能夠支持寫服務,寫操做會首先將數據寫入Memtable,讀操做在query SST files以前會首先從MemTable中query數據(由於MemTable中的數據一直是最新的)。
一旦MemTable滿了,就會轉換爲只讀的不可改變的,而後會建立一個新的MemTable來提供新的寫操做。後臺線程負責將MemTable中的數據flush到SST file,而後這個MemTable就會被銷燬。
重要的配置:
memtable_factory:memtable的工廠對象。經過這個工廠對象,用戶能夠改變memtable的底層實現並提供個性化的實現配置。
write_buff_size :單個內存表的大小限制
db_write_buff_size: 全部列族的內存表總大小。這個配置能夠管理內存表的總內存佔用。
write_buffer_manager : 這個配置不是管理全部memtable的總內存佔用,而是,提供用戶自定義的write buffer manager來管理總體的內存表內存使用。這個配置會覆蓋db_write_buffer_size。
max_write_buffer_number:內存表的最大個數
memtable的默認實現是skiplist。除了默認memtable實現外,用戶也可使用其餘類型的實現方法好比 HashLinkList、HashSkipList or Vector 來提升查詢性能。
Skiplist MemTable
基於Skiplist的memtable在支持讀、寫、隨機訪問和順序scan時提供了較好的性能。此外,還支持了一些其餘實現不能支持的feature好比concurrent insert和 insert with hint。
HashSkiplist MemTable
如其名,HashSkipList是在hash table中組織數據,hash table中的每一個bucket都是一個skip list,HashLinkList也是在hash table中組織數據,可是每個bucket是一個有序的單鏈表。這兩種結構實現目的都是在執行query操做時能夠減小比較次數。一種使用場景就是把這種memtable和PlainTable SST格式結合在一塊兒,而後將數據保存在RAMFS中。當執行檢索或者插入一個key時,key的前綴能夠經過Options.prefix_extractor來檢索,以後就找到了相應的hash bucket。進入到 hash bucket內部後,使用所有的key數據來進行比較操做。使用hash實現的memtable的最大限制是:當在多個key前綴上執行scan操做須要執行copy和sort操做,很是慢且很耗內存。
flush
在如下三種狀況下,內存表的flush操做會被觸發:
因此,內存表也能夠在未滿時執行flush操做。這也是產生的SST file比對應的內存表小的一個緣由,壓縮是是另外一個緣由(內存表總的數據是沒有壓縮的,SST file是壓縮過的)。
Concurrent Insert
若是不支持concurrent insert to memtable的話,來自多個線程的concurrent 寫會順序地寫入memtable。默認是打開concurrent insert to memtable,也能夠經過設置allow_concurrent_memtable_write來關閉。
對RocksDB的每一次update都會寫入兩個位置:1) 內存表(內存數據結構,後續會flush到SST file) 2)磁盤中的write ahead log(WAL)。在故障發生時,WAL能夠用來恢復內存表中的數據。默認狀況下,RocksDB經過在每次用戶寫時調用fflush WAL文件來保證一致性。
Write buffer mnager幫助開發者管理列族或者DB instance的內存表的內存使用。
Write buffer manager與rate_limiter和sst_file_manager相似。用戶建立一個write buffer manager對象,傳入 column family或者DBs的配置中。能夠參考write_buffer_manager.h的註釋部分來學習如何使用。
Limit total memory of memtables
在建立write buffer manager對象時,內存限制的閾值就已經肯定好了。RocksDB會按照這個閾值去管理總體的內存佔用。
在5.6或者更高版本中,若是總體內存表使用超過了閾值的90%,就會觸發正在寫入的某一個column family的數據執行flush動做。若是DB instance實際內存佔用超過了閾值,即便所有的內存表佔用低於90%,那也會觸發更加激進的flush動做。在5.6版本之前,只有在內存表內存佔用的total超過閾值時纔會觸發flush。
在5.6版本及更新版本中,內存是按照arena分配的total內存計數的,即便這些內存不是被內存表使用。在5.6以前版本中,內存使用是按照內存表實際使用的內存
Cost memory used in memtable to block cache
從5.6版本以後,用戶能夠將內存表的內存使用的佔用轉移到block cache。無論是否打開內存表的內存佔用,均可以這樣操做。
大部分狀況下,block cache中實際使用的blocks遠比block cache中的數據少不少,因此若是用戶打開了這個feature後,block cache的容量會覆蓋掉block cache和內存表的內存佔用。若是用戶打開了cache_index_and_filter_blocks的話,這三種內存佔用都在block cache中。
具體實現以下,針對內存表分配的每個1M內存,WriteBufferManager都會在block cache中put一個dummy 1M的entry,這樣block cache就能夠正確的計算內部佔用,並且能夠在須要時淘汰掉一些block以便騰出內存空間。若是內存表的內存佔用下降了,WriteBufferManager也不會立馬三除掉dummmy blocks,而是在後續慢慢地釋放掉。這是由於內存表空間佔用的up and down太正常不過了,RocksDB不須要對此太過敏感。
YCSB, 英文全稱:Yahoo! Cloud Serving Benchmark (YCSB) 。是 Yahoo 公司的一個用來對雲服務進行基礎測試的工具, 目標是促進新一代雲數據服務系統的性能比較。因爲它集成了大多數經常使用的數據庫的測試代碼,因此,它也是數據庫測試的一大利器.
1. 核心YCSB屬性
全部工做量文件能夠指定如下屬性:
workload:要使用的工做量類(例如com.yahoo.ycsb.workloads.CoreWorkload)
db:要使用的數據庫類。可選地,這在命令行能夠指定(默認:com.yahoo.ycsb.BasicDB)
exporter:要是用的測量結果的輸出類(默認:com.yahoo.ycsb.measurements.exporter.TextMeasurementsExporter)
exportfile:用於替代stdout的輸出文件路徑(默認:未定義/輸出到stdout)
threadcount:YCSB客戶端的線程數。可選地,這能夠在命令行指定(默認:1)
measurementtype:支持的測量結果類型有直方圖和時間序列(默認:直方圖)
2. 核心工做量包屬性
和核心工做量構造器一塊兒使用的屬性文件能夠指定如下屬性的值:
fieldcount:一條記錄中的字段數(默認:10) (字段的意義相似於關係數據庫中表的每一列)
fieldlength:每一個字段的大小(默認:100)
readallfields:是否應該讀取全部字段(true)或者只有一個字段(false)(默認:true)
readproportion:讀操做的比例(默認:0.95)
updateproportion:更新操做的比例(默認:0.05)
insertproportion:插入操做的比例(默認:0)
scanproportion:遍歷操做的比例(默認:0)
readmodifywriteproportion:讀-修改-寫一條記錄的操做的比例(默認:0)
requestdistribution:選擇要操做的記錄的分佈——均勻分佈(uniform)、Zipfian分佈(zipfian)或者最近分佈(latest)(默認:uniform)
maxscanlength:對於遍歷操做,最大的遍歷記錄數(默認:1000)
scanlengthdistribution:對於遍歷操做,要遍歷的記錄數的分佈,在1到maxscanlength之間(默認:uniform)
insertorder:記錄是否應該有序插入(ordered),或者是哈希順序(hashed)(默認:hashed)
operationcount:要進行的操做數數量
maxexecutiontime:最大的執行時間(單位爲秒)。當操做數達到規定值或者執行時間達到規定最大值時基準測試會中止。
table:表的名稱(默認:usertable)
recordcount:裝載進數據庫的初始記錄數(默認:0)
3. 測量結果屬性
這些屬性被應用於每個測量結果類型:
直方圖
histogram.buckets:直方圖輸出的區間數(默認:1000)
時間序列
timeseries.granularity:時間序列輸出的粒度(默認:1000)
另外還有兩個重要的option:
delayed_write_rate:參考https://github.com/facebook/rocksdb/wiki/Write-Stalls。
RocksDB has extensive system to slow down writes when flush or compaction can't keep up with the incoming write rate. Without such a system, if users keep writing more than the hardware can handle, the database will:
The idea is to slow down incoming writes to the speed that the database can handle.
Whenever stall conditions are triggered, RocksDB will reduce write rate to delayed_write_rate
, and could possiblely reduce write rate to even lower than delayed_write_rate
if estimated pending compaction bytes accumulates. One thing worth to note is that slowdown/stop triggers and pending compaction bytes limit are per-column family, and write stalls apply to the whole DB, which means if one column family triggers write stall, the whole DB will be stalled.
對於全是寫的workload,delayed_write_rate確定是越大越好。對於全是讀/讀寫混合的workload,應該是設置爲某個值比較好(由於有read amplification)
target_file_size_base:這個是在Level Style Compaction中會用到的。target_file_size_base and target_file_size_multiplier -- Files in level 1 will have target_file_size_base bytes. Each next level's file size will be target_file_size_multiplier bigger than previous one. However, by default target_file_size_multiplier is 1, so files in all L1..Lmax levels are equal. Increasing target_file_size_base will reduce total number of database files, which is generally a good thing. We recommend setting target_file_size_base to be max_bytes_for_level_base / 10, so that there are 10 files in level 1.
Ref:
Tuning RocksDB – Options https://www.jianshu.com/p/8e0018b6a8b6
https://www.jianshu.com/u/aa9cae571502
https://www.jianshu.com/p/9b7437b5ea5b
https://zhuanlan.zhihu.com/p/37193700
https://github.com/facebook/rocksdb/wiki/RocksDB-Tuning-Guide