written by Alex Stocks on 2018/03/28,版權全部,無受權不得轉載html
近日工做中使用了 RocksDB。RocksDB 的優勢此處無需多說,它的一個 feature 是其有不少優化選項用於對 RocksDB 進行調優。欲熟悉這些參數,必須對其背後的原理有所瞭解,本文主要整理一些 RocksDB 的 wiki 文檔,以備本身參考之用。c++
先介紹一些 RocksDB 的基本操做和基本架構。git
參考文檔5提到RocksDB 是一個快速存儲系統,它會充分挖掘 Flash or RAM 硬件的讀寫特性,支持單個 KV 的讀寫以及批量讀寫。RocksDB 自身採用的一些數據結構如 LSM/SKIPLIST 等結構使得其有讀放大、寫放大和空間使用放大的問題。github
LSM 大體結構如上圖所示。LSM 樹並且經過批量存儲技術規避磁盤隨機寫入問題。 LSM 樹的設計思想很是樸素, 它的原理是把一顆大樹拆分紅N棵小樹, 它首先寫入到內存中(內存沒有尋道速度的問題,隨機寫的性能獲得大幅提高),在內存中構建一顆有序小樹,隨着小樹愈來愈大,內存的小樹會flush到磁盤上。磁盤中的樹按期能夠作 merge 操做,合併成一棵大樹,以優化讀性能【讀數據的過程可能須要從內存 memtable 到磁盤 sstfile 讀取屢次,稱之爲讀放大】。RocksDB 的 LSM 體如今多 level 文件格式上,最熱最新的數據盡在 L0 層,數據在內存中,最冷最老的數據盡在 LN 層,數據在磁盤或者固態盤上。RocksDB 還有一種日誌文件叫作 manifest,用於記錄對 sstfile 的更改,能夠認爲是 RocksDB 的 GIF,後面將會詳述。算法
LSM-Tree(Log-Structured-Merge-Tree)
LSM從命名上看,容易望文生義成一個具體的數據結構,一個tree。但LSM並非一個具體的數據結構,也不是一個tree。LSM是一個數據結構的概念,是一個數據結構的設計思想。實際上,要是給LSM的命名斷句,Log和Structured這兩個詞是合併在一塊兒的,LSM-Tree應該斷句成Log-Structured、Merge、Tree三個詞彙,這三個詞彙分別對應如下三點LSM的關鍵性質:數據庫
很明顯,LSM犧牲了一部分讀的性能和增長了合併的開銷,換取了高效的寫性能。那LSM爲何要這麼作?實際上,這就關係到對於磁盤寫已經沒有什麼優化手段了,而對於磁盤讀,不論硬件仍是軟件上都有優化的空間。經過多種優化後,讀性能雖然還是降低,但能夠控制在可接受範圍內。實際上,用於磁盤上的數據結構不一樣於用於內存上的數據結構,用於內存上的數據結構性能的瓶頸就在搜索複雜度,而用於磁盤上的數據結構性能的瓶頸在磁盤IO,甚至是磁盤IO的模式。express
以上三段摘抄自參考文檔20。我的覺得,除了將隨機寫合併以後轉化爲順寫以外,LSM 的另一個關鍵特性就在於其是一種自帶數據 Garbage Collect 的有序數據集合,對外只提供了 Add/Get 接口,其內部的 Compaction 就是其 GC 的關鍵,經過 Compaction 實現了對數據的刪除、附帶了 TTL 的過時數據地淘汰、同一個 Key 的多個版本 Value 地合併。RocksDB 基於 LSM 對外提供了 Add/Delete/Get 三個接口,用戶則基於 RocksDB 提供的 transaction 還能夠實現 Update 語義。編程
RocksDB的三種基本文件格式是 memtable/sstfile/logfile,memtable 是一種內存文件數據系統,新寫數據會被寫進 memtable,部分請求內容會被寫進 logfile。logfile 是一種有利於順序寫的文件系統。memtable 的內存空間被填滿以後,會有一部分老數據被轉移到 sstfile 裏面,這些數據對應的 logfile 裏的 log 就會被安全刪除。sstfile 中的內容是有序的。json
上圖所示,全部 Column Family 共享一個 WAL 文件,可是每一個 Column Family 有本身單獨的 memtable & ssttable(sstfile),即 log 共享而數據分離。後端
一個進程對一個 DB 同時只能建立一個 rocksdb::DB 對象,全部線程共享之。這個對象內部有鎖機制保證訪問安全,多個線程同時進行 Get/Put/Fetch Iteration 都沒有問題,可是若是直接 Iteration 或者 WriteBatch 則須要額外的鎖同步機制保護 Iterator 或者 WriteBatch 對象。
基於 RocksDB 設計存儲系統,要考慮到應用場景進行各類 tradeoff 設置相關參數。譬如,若是 RocksDB 進行 compaction 比較頻繁,雖然有利於空間和讀,可是會形成讀放大;compaction 太低則會形成讀放大和空間放大;增大每一個 level 的 comparession 難度能夠減少空間放大,可是會增長 cpu 負擔,是運算時間增長換取使用空間減少;增大 SSTfile 的 data block size,則是增大內存使用量來加快讀取數據的速度,減少讀放大。
單獨的 Get/Put/Delete 是原子操做,要麼成功要麼失敗,不存在中間狀態。
若是須要進行批量的 Get/Put/Delete 操做且須要操做保持原子屬性,則可使用 WriteBatch。
WriteBatch 還有一個好處是保持加快吞吐率。
默認狀況下,RocksDB 的寫是異步的:僅僅把數據寫進了操做系統的緩存區就返回了,而這些數據被寫進磁盤是一個異步的過程。若是爲了數據安全,能夠用以下代碼把寫過程改成同步寫:
rocksdb::WriteOptions write_options; write_options.sync = true; db->Put(write_options, …);
這個選項會啓用 Posix 系統的 fsync(...) or fdatasync(...) or msync(..., MS_SYNC)
等函數。
異步寫的吞吐率是同步寫的一千多倍。異步寫的缺點是機器或者操做系統崩潰時可能丟掉最近一批寫請求發出的由操做系統緩存的數據,可是 RocksDB 自身崩潰並不會致使數據丟失。而機器或者操做系統崩潰的機率比較低,因此大部分狀況下能夠認爲異步寫是安全的。
RocksDB 因爲有 WAL 機制保證,因此即便崩潰,其重啓後會進行寫重放保證數據一致性。若是不在意數據安全性,能夠把 write_option.disableWAL
設置爲 true,加快寫吞吐率。
RocksDB 調用 Posix API fdatasync()
對數據進行異步寫。若是想用 fsync()
進行同步寫,能夠設置 Options::use_fsync
爲 true。
RocksDB 可以保存某個版本的全部數據(可稱之爲一個 Snapshot)以方便讀取操做,建立並讀取 Snapshot 方法以下:
rocksdb::ReadOptions options; options.snapshot = db->GetSnapshot(); … apply some updates to db …. rocksdb::Iterator* iter = db->NewIterator(options); … read using iter to view the state when the snapshot was created …. delete iter; db->ReleaseSnapshot(options.snapshot);
若是 ReadOptions::snapshot 爲 null,則讀取的 snapshot 爲 RocksDB 當前版本數據的 snapshot。
無論是 it->key()
仍是 it->value()
,其值類型都是 rocksdb::Slice
。 Slice 自身由一個長度字段[ sizet size ]以及一個指向外部一個內存區域的指針[ const char* data_ ]構成,返回 Slice 比返回一個 string 廉價,並不存在內存拷貝的問題。RocksDB 自身會給 key 和 value 添加一個 C-style 的 ‘\0’,因此 slice 的指針指向的內存區域自身做爲字符串輸出沒有問題。
Slice 與 string 之間的轉換代碼以下:
rocksdb::Slice s1 = 「hello」; std::string str(「world」); rocksdb::Slice s2 = str; OR: std::string str = s1.ToString(); assert(str == std::string(「hello」));
可是請注意 Slice 的安全性,有代碼以下:
rocksdb::Slice slice; if (…) { std::string str = …; slice = str; } Use(slice);
當退出 if 語句塊後,slice 內部指針指向的內存區域已經不存在,此時再使用致使程序出問題。
Slice 自身雖然可以減小內存拷貝,可是在離開相應的 scope 以後,其值就會被釋放,rocksdb v5.4.5 版本引入一個 PinnableSlice,其繼承自 Slice,可替換以前 Get 接口的出參:
Status Get(const ReadOptions& options, ColumnFamilyHandle* column_family, const Slice& key, std::string* value) virtual Status Get(const ReadOptions& options, ColumnFamilyHandle* column_family, const Slice& key, PinnableSlice* value)
這裏的 PinnableSlice 如同 Slice 同樣能夠減小內存拷貝,提升讀性能,可是 PinnableSlice 內部有一個引用計數功能,能夠實現數據內存的延遲釋放,延長相關數據的生命週期,相關詳細分析詳見 參考文檔15。
參考文檔16 提到 PinnableSlice, as its name suggests, has the data pinned in memory. The pinned data are released when PinnableSlice object is destructed or when ::Reset is invoked explicitly on it.
。所謂的 pinned in memory 即爲引用計數之意,文中提到內存數據釋放是在 PinnableSlice 析構或者調用 ::Reset 以後。
用戶也能夠把一個 std::string 對象做爲 PinnableSlice 構造函數的參數, 把這個 std::string 指定爲 PinnableSlice 的初始內部 buffer [ rocksdb/slice.h:PinnableSlice::buf_ ],使用方法能夠參考 用新 Get 實現的舊版本的 Get:
virtual inline Status Get(const ReadOptions& options, ColumnFamilyHandle* column_family, const Slice& key, std::string* value) { assert(value != nullptr); PinnableSlice pinnable_val(value); assert(!pinnable_val.IsPinned()); auto s = Get(options, column_family, key, &pinnable_val); if (s.ok() && pinnable_val.IsPinned()) { value->assign(pinnable_val.data(), pinnable_val.size()); } // else value is already assigned return s; }
經過 rocksdb/slice.h:PinnableSlice::PinSlice 實現代碼能夠看出,只有在這個函數裏 PinnableSlice::pinned_ 被賦值爲 true, 同時內存區域存放在 PinnableSlice::data_ 指向的內存區域,故而 PinnableSlice::IsPinned 爲 true,則 內部 buffer [ rocksdb/slice.h:PinnableSlice::buf_ ] 一定爲空。
具體的編程用例可參考 pinnalble_slice.cc。
當使用 TransactionDB 或者 OptimisticTransactionDB 的時候,可使用 RocksDB 的 BEGIN/COMMIT/ROLLBACK 等事務 API。RocksDB 支持活鎖或者死等兩種事務。
WriteBatch 默認使用了事務,確保批量寫成功。
當打開一個 TransactionDB 的時候,若是 RocksDB 檢測到某個 key 已經被別的事務鎖住,則 RocksDB 會返回一個 error。若是打開成功,則全部相關 key 都會被 lock 住,直到事務結束。TransactionDB 的併發特性表現要比 OptimisticTransactionDB 好,可是 TransactionDB 的一個小問題就是無論寫發生在事務裏或者事務外,他都會進行寫衝突檢測。TransactionDB 使用示例代碼以下:
TransactionDB* txn_db; Status s = TransactionDB::Open(options, path, &txn_db); Transaction* txn = txn_db->BeginTransaction(write_options, txn_options); s = txn->Put(「key」, 「value」); s = txn->Delete(「key2」); s = txn->Merge(「key3」, 「value」); s = txn->Commit(); delete txn;
OptimisticTransactionDB 提供了一個更輕量的事務實現,它在進行寫以前不會進行寫衝突檢測,當對寫操做進行 commit 的時候若是發生了 lock 衝突致使寫操做失敗,則 RocksDB 會返回一個 error。這種事務使用了活鎖策略,適用於讀多寫少這種寫衝突機率比較低的場景下,使用示例代碼以下:
DB* db; OptimisticTransactionDB* txn_db; Status s = OptimisticTransactionDB::Open(options, path, &txn_db); db = txn_db->GetBaseDB(); OptimisticTransaction* txn = txn_db->BeginTransaction(write_options, txn_options); txn->Put(「key」, 「value」); txn->Delete(「key2」); txn->Merge(「key3」, 「value」); s = txn->Commit(); delete txn;
參考文檔 6 詳細描述了 RocksDB
的 Transactions。
CF 提供了對 DB 進行邏輯劃分開來的方法,用戶能夠經過 CF 同時對多個 CF 的 KV 進行並行讀寫的方法,提升了並行度。
RocksDB 內存中的數據格式是 skiplist,磁盤則是以 table 形式存儲的 SST 文件格式。
table 格式有兩種:繼承自 leveldb 的文件格式【詳見參考文檔2】和 PlainTable 格式【詳見參考文檔3】。PlainTable 格式是針對 低查詢延遲 或者低延遲存儲媒介如 SSD 特別別優化的一種文件格式。
RocksDB 把相鄰的 key 放到同一個 block 中,block 是數據存儲和傳遞的基本單元。默認 Block 的大小是 4096B,數據未經壓縮。
常常進行 bulk scan 操做的用戶可能但願增大 block size,而常常進行單 key 讀寫的用戶則可能但願減少其值,官方建議這個值減少不要低於 1KB 的下限,變大也不要超過 a few megabytes
。啓用壓縮也能夠起到增大 block size 的好處。
修改 Block size 的方法是修改 Options::block_size
。
Options::write_buffer_size
指定了一個寫內存 buffer 的大小,當這個 buffer 寫滿以後數據會被固化到磁盤上。這個值越大批量寫入的性能越好。
RocksDB 控制寫內存 buffer 數目的參數是 Options::max_write_buffer_number
。這個值默認是 2,當一個 buffer 的數據被 flush 到磁盤上的時候,RocksDB 就用另外一個 buffer 做爲數據讀寫緩衝區。
‘Options::minwritebuffernumberto_merge’ 設定了把寫 buffer 的數據固化到磁盤上時對多少個 buffer 的數據進行合併而後再固化到磁盤上。這個值若是爲 1,則 L0 層文件只有一個,這會致使讀放大,這個值過小會致使數據固化到磁盤上以前數據去重效果太差勁。
這兩個值並非越大越好,太大會延遲一個 DB 被從新打開時的數據加載時間。
在 1.8 章節裏提到 「block 是數據存儲和傳遞的基本單元」,RocksDB 的數據是一個 range 的 key-value 構成一個 Region,根據局部性原理每次訪問一個 Region 的 key 的時候,有不少機率會訪問其相鄰的 key,每一個 Region 的 keys 放在一個 block 裏,多個 Region 的 keys 放在多個 block 裏。
下面以文件系統做爲類比,詳細解釋下 RocksDB 的文件系統:
filename -> permission-bits, length, list of fileblockids
fileblockid -> data
以多個維度組織 key 的時候,咱們可能但願 filename 的前綴都是 ‘/‘, 而 fileblockid 的前綴都是 ‘0’,這樣能夠把他們分別放在不一樣的 block 裏,以方便快速查詢。
Rocksdb 對每一個 kv 以及總體數據文件都分別計算了 checksum,以進行數據正確性校驗。下面有兩個選項對 checksum 的行爲進行控制。
ReadOptions::verify_checksums
強制對每次從磁盤讀取的數據進行校驗,這個選項默認爲 true。Options::paranoid_checks
這個選項爲 true 的時候,若是 RocksDB 打開一個數據檢測到內部數據部分錯亂,立刻拋出一個錯誤。這個選擇默認爲 false。若是 RocksDB 的數據錯亂,RocksDB 會盡可能把它隔離出來,保證大部分數據的可用性和正確性。
GetApproximateSizes
方法能夠返回一個 key range 的磁盤佔用空間大體使用量,示例代碼以下:
rocksdb::Range ranges[2]; ranges[0] = rocksdb::Range(「a」, 「c」); ranges[1] = rocksdb::Range(「x」, 「z」); uint64_t sizes[2]; rocksdb::Status s = db->GetApproximateSizes(ranges, 2, sizes);
上面的 sizes[0]
返回 [a..c)
key range 的磁盤使用量,而 sizes[1]
返回 [x..z)
key range 的磁盤使用量。
通常狀況下,RocksDB 會刪除一些過期的 WAL 文件,所謂過期就是 WAL 文件裏面對應的一些 key 的數據已經被固化到磁盤了。可是 RocksDB 提供了兩個選項以實讓用戶控制 WAL 什麼時候刪除:Options::WAL_ttl_seconds
和 Options::WAL_size_limit_MB
,這兩個參數分別控制 WAL 文件的超時時間 和 最大文件 size。
若是這兩個值都被設置爲 0,則 log 不會被固化到文件系統上。
若是 Options::WAL_ttl_seconds
爲 0 而 Options::WAL_size_limit_MB
不爲 0, RocksDB 會每 10 分鐘檢測全部的 WAL 文件,若是其整體 size 超過 Options::WAL_size_limit_MB
,則 RocksDB 會刪除最先的日誌直到知足這個值位置。一切空文件都會被刪除。
若是 Options::WAL_ttl_seconds
不爲 0 而 Options::WAL_size_limit_MB
爲 0,RocksDB 會每 Options::WAL_ttl_seconds
/ 2 檢測一次 WAL 文件, 全部 TTL 超過 Options::WAL_ttl_seconds
的 WAL 文件都會被刪除。
若是兩個值都不爲 0,RocksDB 會每 10 分鐘檢測全部的 WAL 文件,全部不知足條件的 WAL 文件都會被刪除,其中 ttl 參數優先。
許多 LSM 引擎不支持高效的 RangeScan 操做,由於 Range 操做須要掃描全部的數據文件。通常狀況下常規的技術手段是給 key 創建索引,只用遍歷 key 就能夠了。應用能夠經過確認 prefix_extractor
指定一個能夠的前綴,RocksDB 能夠爲這些 key prefix 創建 Bloom 索引,以加快查詢速度。
參考文檔 5 的 Compaction Styles
一節提到,若是啓用 Level Style Compaction
, L0 存儲着 RocksDB 最新的數據,Lmax 存儲着比較老的數據,L0 裏可能存着重複 keys,可是其餘層文件則不可能存在重複 key。每一個 compaction 任務都會選擇 Ln 層的一個文件以及與其相鄰的 Ln+1 層的多個文件進行合併,刪除過時 或者 標記爲刪除 或者 重複 的 key,而後把合併後的文件放入 Ln+1 層。Compaction 過程會致使寫放大【如寫qps是10MB/s,可是實際磁盤io是50MB/s】效應,可是能夠節省空間並減小讀放大。
若是啓用 Universal Style Compaction
,則只壓縮 L0 的全部文件,合併後再放入 L0 層裏。
RocksDB 的 compaction 任務線程不宜過多,過多容易致使寫請求被 hang 住。
RocksDB 的 API GetUpdatesSince
可讓調用者從 transaction log 獲知最近被更新的 key(原文意爲用 tail 方式讀取 transaction log),經過這個 API 能夠進行數據的增量備份。
RocksDB 在進行數據備份時候,能夠調用 API DisableFileDeletions
中止刪除文件操做,調用 API GetLiveFiles/GetSortedWalFiles
以檢索活躍文件列表,而後進行數據備份。備份工做完成之後在調用 API EnableFileDeletions
讓 RocksDB 再啓動過時文件淘汰工做。
RocksDB 會建立一個 thread pool 與 Env 對象進行關聯,線程池中線程的數目能夠經過 Env::SetBackgroundThreads()
設定。經過這個線程池能夠執行 compaction 與 memtable flush 任務。
當 memtable flush 和 compaction 兩個任務同時執行的時候,會致使寫請求被 hang 住。RocksDB 建議建立兩個線程池,分別指定 HIGH 和 LOW 兩個優先級。默認狀況下 HIGH 線程池執行 memtable flush 任務,LOW 線程池執行 compaction 任務。
相關代碼示例以下:
#include 「rocksdb/env.h」 #include 「rocksdb/db.h」 auto env = rocksdb::Env::Default(); env->SetBackgroundThreads(2, rocksdb::Env::LOW); env->SetBackgroundThreads(1, rocksdb::Env::HIGH); rocksdb::DB* db; rocksdb::Options options; options.env = env; options.max_background_compactions = 2; options.max_background_flushes = 1; rocksdb::Status status = rocksdb::DB::Open(options, 「/tmp/testdb」, &db); assert(status.ok());
還有其餘一些參數,可詳細閱讀參考文檔4。
RocksDB 的每一個 SST 文件都包含一個 Bloom filter。Bloom Filter 只對特定的一組 keys 有效,因此只有新的 SST 文件建立的時候纔會生成這個 filter。當兩個 SST 文件合併的時候,會生成新的 filter 數據。
當 SST 文件加載進內存的時候,filter 也會被加載進內存,當關閉 SST 文件的時候,filter 也會被關閉。若是想讓 filter 常駐內存,能夠用以下代碼設置:
BlockBasedTableOptions::cache_index_and_filter_blocks=true
通常狀況下不要修改 filter 相關參數。若是須要修改,相關設置上面已經說過,此處再也不多談,詳細內容見參考文檔 7。
RocksDB 在進行 compact 的時候,會刪除被標記爲刪除的數據,會刪除重複 key 的老版本的數據,也會刪除過時的數據。數據過時時間由 API DBWithTTL::Open(const Options& options, const std::string& name, StackableDB** dbptr, int32_t ttl = 0, bool read_only = false)
的 ttl 參數設定。
TTL 的使用有如下注意事項:
infinity
,即永不過時;(int32_t)Timestamp
時間值;Timestamp+ttl<time_now
,則會被淘汰掉;DBWithTTL::Open
可能會帶上不一樣的 TTL 值,此時 kv 以最大的 TTL 值爲準;DBWithTTL::Open
的參數 read_only
爲 true,則不會觸發 compact 任務,不會有過時數據被刪除。RocksDB的內存大體有以下四個區:
第三節詳述了 Block Cache,這裏只給出總結性描述:它存儲一些讀緩存數據,它的下一層是操做系統的 Page Cache。
Index 由 key、offset 和 size 三部分構成,當 Block Cache 增大 Block Size 時,block 個數必會減少,index 個數也會隨之下降,若是減少 key size,index 佔用內存空間的量也會隨之下降。
filter是 bloom filter 的實現,若是假陽率是 1%,每一個key佔用 10 bits,則總佔用空間就是 num_of_keys * 10 bits
,若是縮小 bloom 佔用的空間,能夠設置 options.optimize_filters_for_hits = true
,則最後一個 level 的 filter 會被關閉,bloom 佔用率只會用到原來的 10% 。
結合 block cache 所述,index & filter 有以下優化選項:
cache_index_and_filter_blocks
這個 option 若是爲 true,則 index & filter 會被存入 block cache,而 block cache 中的內容會隨着 page cache 被交換到磁盤上,這就會大大下降 RocksDB的性能,把這個 option 設爲 true 的同時也把 pin_l0_filter_and_index_blocks_in_cache
設爲 true,以減少對性能的影響。若是 cache_index_and_filter_blocks
被設置爲 false (其值默認就是 false),index/filter 個數就會受 max_open_files
影響,官方建議把這個選項設置爲 -1,以方便 RocksDB 加載全部的 index 和 filter 文件,最大化程序性能。
能夠經過以下代碼獲取 index & filter 內存量大小:
c++
std::string out;
db->GetProperty(「rocksdb.estimate-table-readers-mem」, &out);
block cache、index & filter 都是讀 buffer,而 memtable 則是寫 buffer,全部 kv 首先都會被寫進 memtable,其 size 是 write_buffer_size
。 memtable 佔用的空間越大,則寫放大效應越小,由於數據在內存被整理好,磁盤上就越少的內容會被 compaction。若是 memtable 磁盤空間增大,則 L1 size 也就隨之增大,L1 空間大小受 max_bytes_for_level_base
option 控制。
能夠經過以下代碼獲取 memtable 內存量大小:
std::string out; db->GetProperty(「rocksdb.cur-size-all-mem-tables」, &out);
這部份內存空間通常佔用總量很少,可是若是有 100k 之多的transactions 發生,每一個 iterator 與一個 data block 外加一個 L1 的 data block,因此內存使用量大約爲 num_iterators * block_size * ((num_levels-1) + num_l0_files)
。
能夠經過以下代碼獲取 Pin Blocks 內存量大小:
c++
table_options.block_cache->GetPinnedUsage();
RocksDB 的讀流程分爲邏輯讀(logical read)和物理讀(physical read)。邏輯讀一般是對 cache【Block Cache & Table Cache】進行讀取,物理讀就是直接讀磁盤。
參考文檔 12 詳細描述了 LeveDB(RocksDB)的讀流程,轉述以下:
在第0層SSTable中查找,沒法命中轉到下一流程;
對於L0 的文件,RocksDB 採用遍歷的方法查找,因此爲了查找效率 RocksDB 會控制 L0 的文件個數。
在剩餘SSTable中查找。
對於 L1 層以及 L1 層以上層級的文件,每一個 SSTable 沒有交疊,可使用二分查找快速找到 key 所在的 Level 以及 SSTfile。
至於寫流程,請參閱 ### 5 Flush & Compaction 章節內容。
無論 RocksDB 有多少 column family,一個 DB 只有一個 WriteController,一旦 DB 中一個 column family 發生堵塞,那麼就會阻塞其餘 column family 的寫。RocksDB 寫入時間長了之後,可能會不定時出現較大的寫毛刺,可能有兩個地方致使 RocksDB 會出現較大的寫延時:獲取 mutex 時可能出現幾十毫秒延遲 和 將數據寫入 memtable 時候可能出現幾百毫秒延時。
獲取 mutex 出現的延遲是由於 flush/compact 線程與讀寫線程競爭致使的,能夠經過調整線程數量下降毛刺時間。
至於寫入 memtable 時候出現的寫毛刺時間,解決方法一就是使用大的 page cache,禁用系統 swap 以及配置 min_free_kbytes、dirty_ratio、dirty_background_ratio 等參數來調整系統的內存回收策略,更基礎的方法是使用內存池。
採用內存池時,memtable 的內存分配和回收流程圖以下:
使用內存池時,RocksDB 的內容分配代碼模塊以下:
Block Cache 是 RocksDB 的數據的緩存,這個緩存能夠在多個 RocksDB 的實例下緩存。通常默認的Block Cache 中存儲的值是未壓縮的,而用戶能夠再指定一個 Block Cache,裏面的數據能夠是壓縮的。用戶訪問數據先訪問默認的 Block Cache,待沒法獲取後再訪問用戶 Cache,用戶 Cache 的數據能夠直接存入 page cache 中。
Cache 有兩種:LRUCache 和 BlockCache。Block 分爲不少 Shard,以減少競爭,因此 shard 大小均勻一致相等,默認 Cache 最多有 64 個 shards,每一個 shard 的 最小 size 爲 512k,總大小是 8M,類別是 LRU。
std::shared_ptr<Cache> cache = NewLRUCache(capacity); BlockedBasedTableOptions table_options; table_options.block_cache = cache; Options options; options.table_factory.reset(new BlockedBasedTableFactory(table_options));
這個 Cache 是不壓縮數據的,用戶能夠設置壓縮數據 BlockCache,方法以下:
table_options.block_cache_compressed = cache;
若是 Cache 爲 nullptr,則RocksDB會建立一個,若是想禁用 Cache,能夠設置以下 Option:
table_options.no_block_cache = true;
默認狀況下RocksDB用的是 LRUCache,大小是 8MB, 每一個 shard 單獨維護本身的 LRU list 和獨立的 hash table,以及本身的 Mutex。
RocksDB還提供了一個 ClockCache,每一個 shard 有本身的一個 circular list,有一個 clock handle 會輪詢這個 circular list,尋找過期的 kv,若是 entry 中的 kv 已經被訪問過則能夠繼續存留,相對於 LRU 好處是無 mutex lock,circular list 本質是 tbb::concurrenthashmap,從 benchmark 來看,兩者命中率類似,但吞吐率 Clock 比 LRU 稍高。
Block Cache初始化之時相關參數:
默認狀況下 index 和filter block 與 block cache 是獨立的,用戶不能設定兩者的內存空間使用量,但爲了控制 RocksDB 的內存空間使用量,能夠用以下代碼把 index 和 filter 也放在 block cache 中:
BlockBasedTableOptions table_options; table_options.cache_index_and_filter_blocks = true;
index 與 filter 通常訪問頻次比 data 高,因此把他們放到一塊兒會致使內存空間與 cpu 資源競爭,進而致使 cache 性能抖動厲害。有以下兩個參數須要注意:cacheindexfilterblockswithhighpriority 和 highpripoolratio 同樣,這個參數只對 LRU Cache 有效,二者須同時生效。這個選項會把 LRU Cache 劃分爲高 prio 和低 prio 區,data 放在 low 區,index 和 filter 放在 high 區,若是高區佔用的內存空間超過了 capacity * highpripoolratio,則會侵佔 low 區的尾部數據空間。
SimCache 用於評測 Cache 的命中率,它封裝了一個真正的 Cache,而後用給定的 capacity 進行 LRU 測算,代碼以下:
c++
// This cache is the actual cache use by the DB.
std::shared_ptr<Cache> cache = NewLRUCache(capacity);
// This is the simulated cache.
std::shared_ptr<Cache> sim_cache = NewSimCache(cache, sim_capacity, sim_num_shard_bits);
BlockBasedTableOptions table_options;
table_options.block_cache = sim_cache;
大概只有容量的 2% 會被用於測算。
RocksDB 3.0 之後添加了一個 Column Family【後面簡稱 CF】 的feature,每一個 kv 存儲之時都必須指定其所在的 CF。RocksDB爲了兼容以往版本,默認建立一個 「default」 的CF。存儲 kv 時若是不指定 CF,RocksDB 會把其存入 「default」 CF 中。
RocksDB 的 Option 有 Options, ColumnFamilyOptions, DBOptions 三種。
ColumnFamilyOptions 是 table 級的,而 Options 是 DB 級的,Options 繼承自 ColumnFamilyOptions 和 DBOptions,它通常影響只有一個 CF 的 DB,如 「default」。
每一個 CF 都有一個 Handle:ColumnFamilyHandle,在 DB 指針被 delete 前,應該先 delete ColumnFamilyHandle。若是 ColumnFamilyHandle 指向的 CF 被別的使用者經過 DropColumnFamily 刪除掉,這個 CF 仍然能夠被訪問,由於其引用計數不爲 0.
在以 Read/Write 方式打開一個 DB 的時候,須要指定一個由全部將要用到的 CF string name 構成的 ColumnFamilyDescriptor array。無論 「default」 CF 使用與否,都必須被帶上。
CF 存在的意義是全部 table 共享 WAL,但不共享 memtable 和 table 文件,經過 WAL 保證原子寫,經過分離 table 可快讀快寫快刪除。每次 flush 一個 CF 後,都會新建一個 WAL,都這並不意味着舊的 WAL 會被刪除,由於別的 CF 數據可能尚未落盤,只有全部的 CF 數據都被 flush 且全部的 WAL 有關的 data 都落盤,相關的 WAL 纔會被刪除。RocksDB 會定時執行 CF flush 任務,能夠經過 Options::max_total_wal_size
查看已有多少舊的 CF 文件已經被 flush 了。
RocksDB 會在磁盤上依據 LSM 算法對多級磁盤文件進行 compaction,這會影響寫性能,拖慢程序性能,能夠經過 WriteOptions.low_pri = true
下降 compaction 的優先級。
RocksDB 有不少選項以專門的目的進行設置,可是大部分狀況下不須要進行特殊的優化。這裏只列出一個經常使用的優化選項。
cf_options.write_buffer_size
CF 的 write buffer 的最大 size。最差狀況下 RocksDB 使用的內存量會翻倍,因此通常狀況下不要輕易修改其值。
這個值通常設置爲 RocksDB 想要使用的內存總量的 1/3,其他的留給 OS 的 page cache。
BlockBasedTableOptions table_options; … \\ set options in table_options options.table_factory.reset(new std::shared_ptr<Cache> cache = NewLRUCache(<your_cache_size>); table_options.block_cache = cache; BlockBasedTableFactory(table_options));
本進程的全部的 DB 全部的 CF 全部的 table_options 都必須使用同一個 cahce 對象,或者讓全部的 DB 全部的 CF 使用同一個 table_options。
cf_options.compression, cf_options.bottonmost_compression
選擇壓縮方法跟你的機器、CPU 能力以及內存磁盤空間大小有關,官方推薦 cf_options.compression
使用 kLZ4Compression,cf_options.bottonmost_compression
使用 kZSTD,選用的時候要確認你的機器有這兩個庫,這兩個選項也能夠分別使用 Snappy 和 Zlib。
官方真正建議修改的參數只有這個 filter 參數。若是大量使用迭代方法,不要修改這個參數,若是大量調用 Get() 接口,建議修改這個參數。修改方法以下:
table_options.filter_policy.reset(NewBloomFilterPolicy(10, false));
一個可能的優化設定以下:
cf_options.level_compaction_dynamic_level_bytes = true; options.max_background_compactions = 4; options.max_background_flushes = 2; options.bytes_per_sync = 1048576; table_options.block_size = 16 * 1024; table_options.cache_index_and_filter_blocks = true; table_options.pin_l0_filter_and_index_blocks_in_cache = true;
上面只是羅列了一些優化選項,這些選項也只能在進程啓動的時候設定。更多的選項請詳細閱讀參考文檔1。
參考文檔 5 的 Persistence 一節提到,RocksDB 每次接收寫請求的時候,請求內容會先被寫入 WAL transaction log,而後再把數據寫入 memfile 裏面。
Put 函數的參數 WriteOptions 裏有一個選項能夠指明是否須要把寫請求的內容寫入 WAL log 裏面。
RocksDB 內部有一個 batch-commit 機制,經過一次 commit 批量地在一次 sync 操做裏把全部 transactions log 寫入磁盤。
RocksDB 的內存數據在 memtable 中存着,有 active-memtable 和 immutable-memtable 兩種。active-memtable 是當前被寫操做使用的 memtable,當 active-memtable 空間寫滿以後( Options.writebuffersize 控制其內存空間大小 )這個空間會被標記爲 readonly 成爲 immutable-memtable。memtable 實質上是一種有序 SkipList,因此寫過程實際上是寫 WAL 日誌和數據插入 SkipList 的過程。
RocksDB 的數據刪除過程跟寫過程相同,只不過 插入的數據是 「key:刪除標記」。
immutable-memtable 被寫入 L0 的過程被稱爲 flush 或者 minor compaction。flush 的觸發條件是 immutable memtable數量超過 minwritebuffernumberto_merge。flush 過程以 column family 爲單位,一個 column family 會使用一個或者多個 immutable-memtable,flush 會一次把全部這些文件合併後寫入磁盤的 L0 sstfile 中。
在 compaction 過程當中若是某個被標記爲刪除的 key 在某個 snapshot 中存在,則會被一直保留,直到 snapshot 不存在纔會被刪除。
RocksDB 的 compaction 策略分爲 Universal Compaction
和 Leveled Compaction
兩種。兩種策略分別有不一樣的使用場景,下面分兩個章節詳述。綜述就是 Leveled Compaction
有利於減少空間放大卻會增長讀放大,Universal Compaction
有利於減小讀放大卻會增大空間放大。
compaction 的觸發條件是文件個數和文件大小。L0 的觸發條件是 sst 文件個數(level0filenumcompactiontrigger 控制),觸發 compaction score 是 L0 sst 文件個數與 level0filenumcompactiontrigger 的比值或者全部文件的 size 超過 maxbytesforlevelbase。L1 ~ LN 的觸發條件則是 sst 文件的大小。
若是 level_compaction_dynamic_level_bytes
爲 false,L1 ~ LN 每一個 level 的最大容量由 max_bytes_for_level_base
和 max_bytes_for_level_multiplier
決定,其 compaction score 就是當前總容量與設定的最大容量之比,若是某 level 知足 compaction 的條件則會被加入 compaction 隊列。
若是 level_compaction_dynamic_level_bytes
爲 true,則 Target_Size(Ln-1) = Target_Size(Ln) / max_bytes_for_level_multiplier
,此時若是某 level 計算出來的 target 值小於 max_bytes_for_level_base / max_bytes_for_level_multiplier
,則 RocksDB 不會再這個 level 存儲任何 sst 數據。
5.1.1 Compaction Score
compact 流程的 Compaction Score,不一樣 level 的計算方法不同,下面先列出 L0 的計算方法。其中 num 表明未 compact 文件的數目。
Param | Value | Description | Score |
---|---|---|---|
level0filenumcompactiontrigger | 4 | num 爲 4 時,達到 compact 條件 | num < 20 時 Score = num / 4 |
level0slowdownwrites_trigger | 20 | num 爲 20 時,RocksDB 會減慢寫入速度 | 20 <= num && num < 24 時 Score = 10000 |
level0stopwrites_trigger | 24 | num 爲 24 時,RocksDB 中止寫入文件,儘快對 L0 進行 compact | 24 <= num 時 Score = 1000000 |
對於 L1+ 層,score = LevelBytes / TargetSize。
5.1.2 Level Max Bytes
每一個 level 容量總大小的計算前文已經提過,
Param | Value | Description |
---|---|---|
maxbytesforlevelbase | 10485760 | L1 總大小 |
maxbytesforlevelmultiplier | 10 | 最大乘法因子 |
maxbytesforlevelmultiplier_addtl[2…6] | 1 | L2 ~ L6 總大小調整參數 |
每一個 level 的總大小計算公式爲 Level_max_bytes[N] = Level_max_bytes[N-1] * max_bytes_for_level_multiplier^(N-1)*max_bytes_for_level_multiplier_additional[N-1]
。
5.1.3 compact file
上面詳述了 compact level 的選擇,可是每一個 level 具體的 compact 文件對象,
L0 層全部文件會被選作 compact 對象,由於它們有很高的機率全部文件的 key range 發生重疊。
對於 L1+ 層的文件,先對全部文件的大小進行排序以選出最大文件。
LevelDB 的文件選取過程以下:
LN 中每一個文件都一個 seek 數值,其默認值非零,每次訪問後這個數值減 1,其值越小說明訪問越頻繁。sst 文件的策略以下:
5.1.4 compaction
大體的 compaction 流程大體爲:
5.1.5 並行 Compact 與 sub-compact
參數 maxbackgroundcompactions 大於 1 時,RocksDB 會進行並行 Compact,但 L0 和 L1 層的 Compaction 任務不能進行並行。
一次 compaction 只能 compact 一個或者多個文件,這會約束總體 compaction 速度。用戶能夠設置 max_subcompactions 參數大於 1,RocksDB 如上圖同樣嘗試把一個文件拆爲多個 sub,而後啓動多個線程執行 sub-compact。
Univesal Compaction 主要針對 L0。當 L0 中的文件個數多於 level0_file_num_compaction_trigger
,則啓動 compact。
L0 中全部的 sst 文件均可能存在重疊的 key range,假設全部的 sst 文件組成了文件隊列 R1,R2,R3,...,Rn,R1 文件的數據是最新的,R2 其次,Rn 則包含了最老的數據,其 compact 流程以下:
max_size_amplification_percent
,則對全部的 sst 進行 compaction(就是所謂的 full compaction);Universal Compaction
主要針對低寫放大場景,跟 Leveled Compaction
相比一次合併文件較多但由於一次只處理 L0 因此寫放大總體較低,可是空間放大效應比較大。
RocksDB 還支持一種 FIFO 的 compaction。FIFO 顧名思義就是先進先出,這種模式週期性地刪除舊數據。在 FIFO 模式下,全部文件都在 L0,當 sst 文件總大小超過閥值 maxtablefiles_size,則刪除最老的 sst 文件。參考文檔21中提到能夠基於 FIFO compaction 機制把 RocksDB 當作一個時序數據庫:對於 FIFO 來講,它的策略很是的簡單,全部的 SST 都在 Level 0,若是超過了閾值,就從最老的 SST 開始刪除,其實能夠看到,這套機制很是適合於存儲時序數據
。
整個 compaction 是 LSM-tree 數據結構的核心,也是rocksDB的核心,詳細內容請閱讀 參考文檔8 和 參考文檔9。
RocksDB 自身之提供了 Put/Delete/Get 等接口,若須要在現有值上進行修改操做【或者成爲增量更新】,能夠藉助這三個操做進行如下操做實現之:
若是但願整個過程是原子操做,就須要藉助 RocksDB 的 Merge 接口了。參考文檔14 給出了 RocksDB Merge 接口定義以下:
RocksDB 提供了一個 MergeOperator 做爲 Merge 接口,其中一個子類 AssociativeMergeOperator 可在大部分場景下使用,其定義以下:
// The simpler, associative merge operator. class AssociativeMergeOperator : public MergeOperator { public: virtual ~AssociativeMergeOperator() {} // Gives the client a way to express the read -> modify -> write semantics // key: (IN) 操做對象 KV 的 key // existing_value:(IN) 操做對象 KV 的 value,若是爲 null 則意味着 KV 不存在 // value: (IN) 新值,用於替換/更新 @existing_value // new_value: (OUT) 客戶端負責把 merge 後的新值填入這個變量 // logger: (IN) Client could use this to log errors during merge. // // Return true on success. // All values passed in will be client-specific values. So if this method // returns false, it is because client specified bad data or there was // internal corruption. The client should assume that this will be treated // as an error by the library. virtual bool Merge(const Slice& key, const Slice* existing_value, const Slice& value, std::string* new_value, Logger* logger) const = 0; private: // Default implementations of the MergeOperator functions virtual bool FullMergeV2(const MergeOperationInput& merge_in, MergeOperationOutput* merge_out) const override; virtual bool PartialMerge(const Slice& key, const Slice& left_operand, const Slice& right_operand, std::string* new_value, Logger* logger) const override; };
RocksDB AssociativeMergeOperator 被稱爲關聯性 Merge Operator,參考文檔14 給出了關聯性的定義:
**MergeOperator還能夠用於非關聯型數據類型的更新。** 例如,在RocksDB中保存json字符串,即Put接口寫入data的格式爲合法的json字符串。而Merge接口只但願更新json中的某個字段。因此代碼多是這樣
:
// Put/store the json string into to the database db_->Put(put_option_, "json_obj_key", "{ employees: [ {first_name: john, last_name: doe}, {first_name: adam, last_name: smith}] }"); // Use a pre-defined "merge operator" to incrementally update the value of the json string db_->Merge(merge_option_, "json_obj_key", "employees[1].first_name = lucy"); db_->Merge(merge_option_, "json_obj_key", "employees[0].last_name = dow"); `AssociativeMergeOperator沒法處理這種場景,由於它假設Put和Merge的數據格式是關聯的。咱們須要區分Put和Merge的數據格式,也沒法把多個merge操做數合併成一個。這時候就須要Generic MergeOperator。` // The Merge Operator // // Essentially, a MergeOperator specifies the SEMANTICS of a merge, which only // client knows. It could be numeric addition, list append, string // concatenation, edit data structure, ... , anything. // The library, on the other hand, is concerned with the exercise of this // interface, at the right time (during get, iteration, compaction...) class MergeOperator { public: virtual ~MergeOperator() {} // Gives the client a way to express the read -> modify -> write semantics // key: (IN) The key that's associated with this merge operation. // existing: (IN) null indicates that the key does not exist before this op // operand_list:(IN) the sequence of merge operations to apply, front() first. // new_value: (OUT) Client is responsible for filling the merge result here // logger: (IN) Client could use this to log errors during merge. // // Return true on success. Return false failure / error / corruption. // 用於對已有的值作Put或Delete操做 virtual bool FullMerge(const Slice& key, const Slice* existing_value, const std::deque<std::string>& operand_list, std::string* new_value, Logger* logger) const = 0; // This function performs merge(left_op, right_op) // when both the operands are themselves merge operation types. // Save the result in *new_value and return true. If it is impossible // or infeasible to combine the two operations, return false instead. // 若是連續屢次對一個 key 進行操做,則能夠能夠藉助 PartialMerge 將兩個操做數合併. virtual bool PartialMerge(const Slice& key, const Slice& left_operand, const Slice& right_operand, std::string* new_value, Logger* logger) const = 0; // The name of the MergeOperator. Used to check for MergeOperator // mismatches (i.e., a DB created with one MergeOperator is // accessed using a different MergeOperator) virtual const char* Name() const = 0; };
當調用DB::Put()和DB:Merge()接口時, 並不須要馬上計算最後的結果. RocksDB將計算的動做延後觸發, 例如在下一次用戶調用Get, 或者RocksDB決定作Compaction時. 因此, 當merge的動做真正開始作的時候, 可能積壓(stack)了多個操做數須要處理. 這種狀況就須要MergeOperator::FullMerge來對existing_value和一個操做數序列進行計算, 獲得最終的值.
有時候, 在調用FullMerge以前, 能夠先對某些merge操做數進行合併處理, 而不是將它們保存起來, 這就是PartialMerge的做用: 將兩個操做數合併爲一個, 減小FullMerge的工做量.
當遇到兩個merge操做數時, RocksDB老是先會嘗試調用用戶的PartialMerge方法來作合併, 若是PartialMerge返回false纔會保存操做數. 當遇到Put/Delete操做, 就會調用FullMerge將已存在的值和操做數序列傳入, 計算出最終的值.
merge 操做數的格式和Put相同
多個順序的merge操做數能夠合併成一個
merge 操做數的格式和Put不一樣
當多個merge操做數能夠合併時,PartialMerge()方法返回true
*!!!: 本節文字摘抄自 參考文檔14 。
參考文檔 12 列舉了 RocksDB 磁盤上數據文件的種類:
* db的操做日誌 * 存儲實際數據的 SSTable 文件 * DB的元信息 Manifest 文件 * 記錄當前正在使用的 Manifest 文件,它的內容就是當前的 manifest 文件名 * 系統的運行日誌,記錄系統的運行信息或者錯誤日誌。 * 臨時數據庫文件,repair 時臨時生成的。
manifest 文件記載了全部 SSTable 文件的 key 的範圍、level 級別等數據。
上面是 leveldb 的架構圖,能夠做爲參考,明白各類文件的做用。
log 文件就是 WAL。
如上圖,log 文件的邏輯單位是 Record,物理單位是 block,每一個 Record 能夠存在於一個 block 中,也能夠佔用多個 block。Record 的詳細結構見上圖文字部分,其 type 字段的意義見下圖。
從上圖可見 Record type的意義:若是某 KV 過長則能夠用多 Record 存儲。
RocksDB 整個 LSM 樹的信息須要常駐內存,以讓 RocksDB 快速進行 kv 查找或者進行 compaction 任務,RocksDB 會用文件把這些信息固化下來,這個文件就是 Manifest 文件。RocksDB 稱 Manifest 文件記錄了 DB 狀態變化的事務性日誌,也就是說它記錄了全部改變 DB 狀態的操做。主要內容有事務性日誌和數據庫狀態的變化。
RocksDB 的函數 VersionSet::LogAndApply 是對 Manifest 文件的更新操做,因此能夠經過定位這個函數出現的位置來跟蹤 Manifest 的記錄內容。
Manifest 文件做爲事務性日誌文件,只要數據庫有變化,Manifest都會記錄。其內容 size 超過設定值後會被 VersionSet::WriteSnapShot 重寫。
RocksDB 進程 Crash 後 Reboot 的過程當中,會首先讀取 Manifest 文件在內存中重建 LSM 樹,而後根據 WAL 日誌文件恢復 memtable 內容。
上圖是 leveldb 的 Manifest 文件結構,這個 Manifest 文件有如下文件內容:
RocksDB MANIFEST文件所保存的數據基本是來自於VersionEdit這個結構,MANIFEST包含了兩個文件,一個log文件一個包含最新MANIFEST文件名的文件,Manifest的log文件名是這樣 MANIFEST-(seqnumber),這個seq會一直增加,只有當 超過了指定的大小以後,MANIFEST會刷新一個新的文件,當新的文件刷新到磁盤(而且文件名更新)以後,老的文件會被刪除掉,這裏能夠認爲每一次MANIFEST的更新都表明一次snapshot,其結構描述以下:
MANIFEST = { CURRENT, MANIFEST-<seq-no>* } CURRENT = File pointer to the latest manifest log MANIFEST-<seq no> = Contains snapshot of RocksDB state and subsequent modifications
在RocksDB中任意時間存儲引擎的狀態都會保存爲一個Version(也就是SST的集合),而每次對Version的修改都是一個VersionEdit,而最終這些VersionEdit就是 組成manifest-log文件的內容。
下面就是MANIFEST的log文件的基本構成:
version-edit = Any RocksDB state change version = { version-edit* } manifest-log-file = { version, version-edit* } = { version-edit* }
關於 VersionSet 相關代碼分析見參考文檔13。
SSTfile 結構以下:
<beginning_of_file> [data block 1] [data block 2] … [data block N] [meta block 1: filter block] [meta block 2: stats block] [meta block 3: compression dictionary block] … [meta block K: future extended block] [metaindex block] [index block] [Footer] <end_of_file>
LevelDB 的 SSTfile 結構以下:
見參考文檔12,SSTtable 大體分爲幾個部分:
block 結構以下圖:
record 結構以下圖:
Footer 結構以下圖:
memtable 中存儲了一些 metadata 和 data,data 在 skiplist 中存儲。metadata 數據以下(源自參考文檔 12):
RocksDB 的 Version 表示一個版本的 metadata,其主要內容是 FileMetaData 指針的二維數組,分層記錄了全部的SST文件信息。
FileMetaData 數據結構用來維護一個文件的元信息,包括文件大小,文件編號,最大最小值,引用計數等信息,其中引用計數記錄了被不一樣的Version引用的個數,保證被引用中的文件不會被刪除。
Version中還記錄了觸發 Compaction 相關的狀態信息,這些信息會在讀寫請求或 Compaction 過程當中被更新。在 CompactMemTable 和 BackgroundCompaction 過程當中會致使新文件的產生和舊文件的刪除,每當這個時候都會有一個新的對應的Version生成,並插入 VersionSet 鏈表頭部,LevelDB 用 VersionEdit 來表示這種相鄰 Version 的差值。
VersionSet 結構如上圖所示,它是一個 Version 構成的雙向鏈表,這些Version按時間順序前後產生,記錄了當時的元信息,鏈表頭指向當前最新的Version,同時維護了每一個Version的引用計數,被引用中的Version不會被刪除,其對應的SST文件也所以得以保留,經過這種方式,使得LevelDB能夠在一個穩定的快照視圖上訪問文件。
VersionSet中除了Version的雙向鏈表外還會記錄一些如LogNumber,Sequence,下一個SST文件編號的狀態信息。
本節內容節選自參考文檔 12。
爲了不進程崩潰或機器宕機致使的數據丟失,LevelDB 須要將元信息數據持久化到磁盤,承擔這個任務的就是 Manifest 文件,每當有新的Version產生都須要更新 Manifest。
新增數據正好對應於VersionEdit內容,也就是說Manifest文件記錄的是一組VersionEdit值,在Manifest中的一次增量內容稱做一個Block。
Manifest Block 的詳細結構如上圖所示。
上圖最上面的流程顯示了恢復元信息的過程,也就是一次應用 VersionEdit 的過程,這個過程會有大量的臨時 Version 產生,但這種方法顯然太過於耗費資源,LevelDB 引入 VersionSet::Builder 來避免這種中間變量,方法是先將全部的VersoinEdit內容整理到VersionBuilder中,而後一次應用產生最終的Version,詳細流程如上圖下邊流程所示。
數據恢復的詳細流程以下:
RocksDB 每次進行更新操做就會把更新內容寫入 Manifest 文件,同時它會更新版本號。
版本號是一個 8 字節的證書,每一個 key 更新時,除了新數據被寫入數據文件,同時記錄下 RocksDB 的版本號。RocksDB 的 Snapshot 數據僅僅是邏輯數據,並無對應的真實存在的物理數據,僅僅對應一下當前 RocksDB 的全局版本號而已,只要 Snapshot 存在,每一個 key 對應版本號的數據在後面的更新、刪除、合併時會一併存在,不會被刪除,以保證數據一致性。
6.7.1 Checkpoints
Checkpoints 是 RocksDB 提供的一種 snapshot,獨立的存在一個單獨的不一樣於 RocksDB 自身數據目錄的目錄中,既能夠 ReadOnly 模式打開,也能夠 Read-Write 模式打開。Checkpoints 被用於全量或者增量 Backup 機制中。
若是 Checkpoints 目錄和 RocksDB 數據目錄在同一個文件系統上,則 Checkpoints 目錄下的 SST 是一個 hard link【SST 文件是 Read-Only的】,而 manifest 和 CURRENT 兩個文件則會被拷貝出來。若是 DB 有多個 Column Family,wal 文件也會被複制,其時間範圍足以覆蓋 Checkpoints 的起始和結束,以保證數據一致性。
若是以 Read-Write 模式打開 Checkpoints 文件,則其中過期的 SST 文件會被刪除掉。
RocksDB 提供了 point-of-time 數據備份功能,能夠調用 BackupEngine::CreateNewBackup(db, flush_before_backup = false)
接口進行數據備份, 其大體流程以下:
GetLiveFiles()
獲取當前的有效文件,如 table files, current, options and manifest file;將 RocksDB 中的全部的 sst/Manifest/配置/CURRENT 等有效文件備份到指定目錄;
GetLiveFiles() 接口返回的 SST 文件若是已經被備份過,則這個文件不會被從新複製到目標備份目錄,可是 BackupEngine
會對這個文件進行 checksum 校驗,若是校驗失敗則會停止備份過程。
若是 flush_before_backup
爲 false,則BackupEngine
會調用 GetSortedWalFiles()
接口把當前有效的 wal 文件也拷貝到備份目錄;
從新容許刪除文件。
sst 文件只有在 compact 時纔會被刪除,因此禁止刪除就至關於禁止了 compaction。別的 RocksDB 在獲取這些備份數據文件後會依據 Manifest 文件重構 LSM 結構的同時,也能恢復出 WAL 文件,進而重構出當時的 memtable 文件。
在進行 Backup 的過程當中,寫操做是不會被阻塞的,因此 WAL 文件內容會在 backup 過程當中發生改變。RocksDB 的 flushbeforebackup 選項用來控制在 backup 時是否也拷貝 WAL,其值爲 true 則不拷貝。
6.8.1 Backup 編程接口
RocksDB 提供的 Backup 接口使用方法詳見 參考文檔17。include/rocksdb/utilities/backupable_db.h 主要提供了 BackupEngine
和 BackupEngineReadOnly
,分別用於備份數據和恢復數據。
BackupEngine
備份數據是增量式備份【設置選項 BackupableDBOptions::share_table_files
】,調用 BackupEngine::CreateNewBackup()
接口進行備份後,能夠調用接口 BackupEngine::GetBackupInfo()
獲取備份文件的信息:ID、timestamp、size、file number 和 metadata【用戶自定義數據】。
備份 DB 目錄見上圖,各個備份文件的 size 是其 private 目錄下數據與 shared 目錄下數據之和,shared 下面存儲的數據是各個備份公共的數據,因此全部備份文件的 size 之和可能大於實際佔用的磁盤空間大小。meta 目錄下各個文件的格式詳見 utilities/backupable/backupable_db.cc,上圖中 meta/1
內容以下:
1536821592 # checksum 1 # backup ID 4 # private file number private/1/MANIFEST-000008 crc32 272357318 private/1/OPTIONS-000011 crc32 3039312718 private/1/CURRENT crc32 1581506767 private/1/000009.log crc32 3494278128
Private 目錄則包含一些非 SST 文件:options, current, manifest, WALs。若是 Options::share_table_files
爲false,則 private 目錄會存儲 SST 文件。若是 Options::share_table_files
爲 true 且 Options::share_files_with_checksum
爲 false,shared 目錄包含一些 SST 文件,SST 文件命名與原 RocksDB 目錄下命名一致,因此在一個備份目錄下只能備份一個 RocksDB 實例的數據。
接口 BackupEngine::VerifyBackups()
用於對備份數據進行校驗,可是僅僅根據 meta 目錄下各個 ID 文件記錄的文件 size 與 相應的 private 目錄下的文件的 size 是否相等,並不會進行 checksum 校驗, 校驗 checksum 須要讀取數據文件,比較費時。另外須要注意的是,這個接口相應的 BackupEngine
句柄只能由BackupEngine::CreateNewBackup()
建立,也即只能在進行文件備份且句柄未失效前進行數據校驗,由於校驗時所依據的數據是在備份過程當中產生的。
接口 BackupEngineReadOnly::RestoreDBFromBackup(backup_id, db_dir, wal_dir,restore_options)
用於備份數據恢復,參數 db_dir
和wal_dir
大部分場景下都是同一個目錄,但在 參考文檔18 所提供的把 RocksDB 當作純內存數據庫的使用場景下, db_dir
在內存虛擬文件系統上,而 wal_dir
則是一個磁盤文件目錄。進行數據恢復時,這個接口還會根據 meta 下相應 ID 記錄的 備份數據 checksum 對 private 目錄下的數據進行校驗,發錯錯誤時返回 Status::Corruption
錯誤。
6.8.2 Backup 性能優化
BackupEngine::Open()
啓用時須要進行一些初始化工做,因此它會消耗一些時間。例如須要把本地 RocksDB 數據備份到遠端的 HDFS 上,這個過程就可能消耗多個 network round-trip,因此在實際使用中不要頻繁建立 BackupEngine
對象。
加快 BackupEngine 對象的方式之一是經過調用 PurgeOldBackups(N)
來刪除非必要的備份文件,接口 PurgeOldBackups(N)
自己之意就是隻保留最近的 N 個備份,多餘的會被刪除掉。也能夠經過調用 DeleteBackup(id)
接口根據備份 ID 刪除某個肯定的備份。
初始化 BackupEngine 對象事後,備份的速度就取決於本地與遠端的媒介運行速度了。例如,若是本地媒介是 HDD,在其自身飽和運轉以後就算是打開再多的線程也無濟於事。若是媒介是一個小的 HDFS 集羣,其表現也不會很好。若是本地是 SSD 而遠端是一個大的 HDFS 集羣,則相較於單線程, 16 個備份線程會被備份時間縮短 2/3。
6.8.3 高級編程接口
BackupEngine::CreateNewBackupWithMetadata()
用於設置 metadata,例如設置你能辨識的備份 ID,metadata 能夠經過 BackupEngine::GetBackupInfo()
獲取;rocksdb::LoadLatestOptions()
or rocksdb:: LoadOptionsFromFile()
RocksDB 如今也能對 RocksDB 的 options 進行備份,能夠經過這兩個接口獲取相應備份的 Options;BackupableDBOptions::backup_env
用於設置備份目錄的 ENV;BackupableDBOptions::backup_dir
用於設置備份文件的根目錄;BackupableDBOptions::share_table_files
若是這個選項爲 true,則 BackupEngine
會進行增量備份,把全部的 SST 文件存儲到 shared/ 子目錄,其危險是 SST 文件名字可能相同【在多個 RocksDB 對象共用同一備份目錄的場景下】;BackupableDBOptions::share_files_with_checksum
在多個 RocksDB 對象共用同一備份目錄的場景下,SST 文件名字可能相同,把這個選項設置爲 true 能夠處理這個衝突,SST 文件會被 BackupEngine
經過 checksum/size/seqnum 三個參數進行校驗;BackupableDBOptions::max_background_operations
這個參數用於設置備份和恢復數據的線程數,在使用分佈式文件系統如 HDFS 場景下,這個參數會大大提升備份和恢復的效率;BackupableDBOptions::info_log
用於設置 LOG 對象,能夠在備份和恢復數據時進行日誌輸出;BackupableDBOptions::sync
若是設置爲 true,BackupEngine
會調用 fsync
系統接口進行文件數據和 metadata 的數據寫入,以防備系統重啓或者崩潰時的數據不一致現象,大部分狀況下若是爲追求性能,這個參數能夠設置爲 false;BackupableDBOptions::destroy_old_data
若是這個選項爲 true,新的 BackupEngine
被建立出來以後備份目錄下舊的備份數據會被清空;BackupEngine::CreateNewBackup(db, flush_before_backup = false)
flushbeforebackup 被設置爲 true 時,BackupEngine
首先 flush memtable,而後再進行數據複製,而 WAL log 文件不會被複制,由於 flush 時候它會被刪掉,若是這個爲 false 則相應的 WAL 日誌文件也會被複制以保證備份數據與當前 RocksDB 狀態一致;官方 wiki 【參考文檔 11】提供了一份 FAQ,下面節選一些比較有意義的建議,其餘內容請移步官方文檔。
DB::SyncWAL()
以前的數據 或者已經被寫入 L0 的 memtable 的數據都是安全的;GetIntProperty(cf_handle, 「rocksdb.estimate-num-keys")
獲取一個 column family 中大概的 key 的個數;GetAggregatedIntProperty(「rocksdb.estimate-num-keys", &num_keys)
獲取整個 RocksDB 中大概的 key 的總數,之因此只能獲取一個大概數值是由於 RocksDB 的磁盤文件有重複 key,並且 compact 的時候會進行 key 的淘汰,因此沒法精確獲取;DB::OpenForReadOnly()
對 RocksDB 進行只讀訪問;options.max_background_flushes
最少爲 4;插入數據以前設置關閉自動 compact,把 options.level0_file_num_compaction_trigger/options.level0_slowdown_writes_trigger/options.level0_stop_writes_trigger
三個值放大,數據插入後再啓動調用 compact 函數進行 compaction 操做。 若是調用了Options::PrepareForBulkLoad()
,後面三個方法會被自動啓用;DBOptions::db_paths/DBOptions::db_log_dir/DBOptions::wal_dir
三個參數分別存儲 RocksDB 的數據,這種狀況下若是要釋放 RocksDB 的數據能夠經過 DestroyDB() 這個 API 去執行刪除任務;BackupOptions::backup_log_files
或者 flush_before_backup
的值爲 true 的時候,若是程序調用 CreateNewBackup()
則 RocksDB 會建立 point-in-time snapshot
,RocksDB進行數據備份的時候不會影響正常的讀寫邏輯;prefix extractor
;ColumnFamilyOptions::bottommost_compression
使用不一樣的壓縮的方法;prefix iterating
;rocksdb.estimate-live-data-size
能夠估算 RocksDB 使用的磁盤空間;