最近項目中用到這個nb的玩意,所以就花時間研究了下,同時整理下助自己記憶。這個猛虎上山的logo就是rocksdb官方的logo。 同時借用官網的一句話來綜述下: 一個持久性的key-value存儲,為了更快速的存儲環境而生。
rocks特性
rocksdb是一個可嵌入的C++庫, 可用于存儲任意大小字節流的key, value 結構。支持的方法有Put, Get, Delete, 傳統的數據庫的常用方法CRUD.其中update這個方法對rocksdb來說也是一種Put方法。同時因為rocksdb是按key的順序存儲數據,所以還支持Iterator迭代的方法,即就是定位到數據庫中的一個key后,可以從這個key開始正向的掃描,也可以逆向的掃描。同時支持PrefixExtractor前綴迭代,假設我們存儲的key有20位,前10位為時間信息,那么我們可以根據前綴迭代把某個時間段的數據拉出來,這個需要我們在創建數據庫時配置PrefixExtractor的位數為10,默認是0。同時rocksdb支持快照Snapshot, 每次以只讀方式Get數據時都會創建一個快照,在這個時間點之前的數據對Get可見,之后不可見。同時rocksdb還提供一些用戶可定義的filter, 比如提供bloomfilter可以優化讀性能;提供DB級別的ttlfilter, 使得超過過期時間的數據在compaction過程中被刪除。
Rocksdb還支持寫入的原子性,數據的持久化,數據校驗,數據壓縮,數據備份,數據緩存等一些特性。
寫邏輯
我們從一條數據對寫入來了解rocksdb的存儲結構吧。
首先當一條數據寫入rocksdb時, 會將這條記錄封裝成一個batch, 也可以是多條記錄一個batch,由batch來保證原子操作。就是一個batch里的數據要么全部成功要么全部失敗。
第一步先以日志的形式落地磁盤,記write ahead log, 落地成功后再寫入memtable。這里記錄wal的原因就是防止重啟時內存中的數據丟失。所以在db重新打開時會先從wal恢復內存中的mentable. 可配置WAL保存在可靠的存儲里。
這里的memtable是在內存中的一個跳表結構(skiplist)。每一個節點都是存儲著一個key, value. 跳表可使查找的復雜度為logn, 同時插入數據非常簡單。每個batch獨占memtable的寫鎖。這個是為了避免多線程寫造成的數據錯亂。
當memtable的數據大小超過閾值(write_buffer_size)后,會新生成一個memtable繼續寫,將前一個memtable保存為只讀memtable.
當只讀memtable的數量超過閾值后,會將所有的只讀memtable合并并flush到磁盤生成一個SST文件。這里的SST屬于level0, level0中的每個SST有序,整個level0不一定有序。
當level0的sst文件數超過閾值或者總大小超過閾值,會觸發compaction操作,將level0中的數據合并到level1中。同樣level1的文件數超過閾值或者總大小超過閾值,也會觸發compaction操作, 這時候隨機選擇一個sst合并到更高層的level中。這里有兩點比較重要,1:level1 及其以上的level都整體有序。每個sst存儲一個范圍的數據互不交叉互不重合;2: level1 以上的 compaction操作可以多線程執行,前提是每個線程所操作的數據互不交叉。
那么這樣數據就由內存流入到高層的level。我們理想狀態下,所有的數據都存在非level0的一層level上。這樣可以保證最高效的查詢速度。所以對rocksdb來說, 每次寫都保證著原子性,所有數據都會落地到磁盤,保證著數據的可靠性和持久性。
讀邏輯
同樣,我們看一條數據是如何被讀取的。
首先RocksDB中的每一條記錄(KeyValue)都有一個LogSequenceNumber,從最初的0開始,每次寫入加1。lsn在memtable中單調遞增。之前提到的snapshot即就是基于lsn實現,每次以只讀模式打開時,記錄一個lsn, 大于該lsn的key不可見。
首先讀操作先訪問memtable。跳表的時間復雜度可達到logn, 如果不存在會訪問level0, 而level0整體不是有序的, 所以會按創建時間由新到老依次訪問每一個sst文件。所以時間復雜度為m*logn。如果仍不存在,則繼續訪問level1,由于level1及其以上的level都整體有序,所以只需要訪問一個sst文件即可。 直到查找到最高層或者找到這個key。所以讀操作可能會被放大好多倍。
rocksdb做了幾點優化,一點是為每個SST提供一個可配置的bloomfilter. 每個level的配置不一樣。這樣可以快速的確認一個key在不在某個SST中,這點以犧牲磁盤空間來換取時間。另一點是提供可配置的cache, 用于保存訪問過的key在內存中, 這里有一點就是它緩存的是某個key在SST文件中的整個block里的記錄。
存儲結構之memtable
rocksdb的memtable, 默認是skiplist跳表結構, 但它也同時支持hash-skiplist, hash-linklist結構。在創建數據庫時,可配置選擇存儲合適結構。什么時候選擇合適的結構類型,這塊還值得研究。如果將所有的數據都保存到內存中,這時候用hash-linklist是不是更合適呢。
skiplist 結構:
hash-skiplist結構:
hash-linklist結構:
存儲結構之SST
SST(Sorted Sequence Table)
SST作為rocksdb第二個存儲方式, 它的數據主要以block塊的方式存儲,默認是4k大小。ps之所以默認設定4k是因為我們一般的操作系統存儲的每個頁剛好是4k, 這樣每次加載一份數據一個block剛好就是一頁。這樣就不會造成,block過大需要分頁存儲或者block過小一頁存儲一個block造成內存浪費。
其中每個block以最后的crc碼來效驗數據的正確性。
根據block存儲的不同數據可以分為多種類型。其中Datablock里存儲著壓縮的key, value 記錄,是數據層。Metablock存儲著filter的數據,其中bloomfilter, prefix_bloomfilter, 還有用戶自定義的一些filter的數據存在這里。 Metablock Index記錄著filter的大小偏移量等信息。Indexblock記錄著每一個Datablock的最大key和最小key的偏移量等信息。 Footer記錄著Metablockindex塊的偏移量和大小以及Indexblock的偏移量和大小等信息。
Flush操作
* 首先當只讀memtable的數量大于閾值,如果沒有其它線程flush,則將該次操作加入隊列。
* 遍歷skiplist,通過迭代器逐一掃描key-value,將key-value寫入到data-block,
* 如果data block大小已經超過閾值,或者key-value對是最后的一對,則觸發一次block-flush, 同時根據壓縮算法對block進行壓縮,并生成對應的index block記錄;
* 所有數據更新完后, 寫入index block,meta block,metaindex block以及footer信息到文件尾;
* 并將變化sst文件的元信息寫入manifest文件。 同時清理wal日志文件。
Compaction操作
* rocksdb會定期的檢測每個level的狀態, 并為每個level計算score. score通過level實際的size/base_size來計算;level0的score是通過實際sst數/ level0 的sst文件數閾值計算。
* 如果score>1 則會觸發compaction, 找score最大的level,根據一定策略從中選擇一個sst文件進行compaction.?
* 根據這個sst文件的minkey, maxkey找到leveln+1層中有重疊的sst文件。 多個sst文件進行歸并排序,生成新的有序sst文件。
* 將變化的sst文件的信息寫入manifest文件。
數據庫文件結構
SST
rocksdb的數據文件, 嚴格說包含SST文件,還有內存中的memtable數據。
memtable寫入到一定閾值,可能是這個參數max_total_wal_size(待驗證),會flush memtable里的數據到SST level0文件。level0文件的key不一定是有序的,但leveln(n>0)的key必然是有序的.
可通過ldb工具來查看SST的內容
```
# ../ldb --db=./data/fansnum_tail scan
1162218773 : 34
1162222073 : 4433
1162231091 : 1025
1162231094 : 4
1162231403 : 72
1162233913 : 66
1162241104 : 35
./db_bench --db=/data1/xiaodong28/PacketServer/PacketComputation/offline_update/fansnum/ --benchmarks=fillrandom --num=10000 --compression_type=none
```
MANIFEST
*MANIFEST*: 記錄rocksdb最近的狀態變化日志。其中包含manifest日志 和最新的文件指針 *CURRENT*, 記錄最近的MANIFEST
manifest日志: *MANIFEST-(seq number)*, 記錄一系列版本更新記錄。
在RocksDB中任意時間存儲引擎的狀態都會保存為一個Version(也就是SST的集合),而每次對Version的修改都是一個VersionEdit,而最終這些VersionEdit就是 組成manifest-log文件的內容
```
/data1/rocksdb/rocksdb/bin/ldb --path=./fansnum/MANIFEST-000014? manifest_dump
```
log
WAL文件。 rocksdb在寫數據時, 先會寫WAL,再寫memtable。為了避免crash時, memtable的數據丟失。服務重啟時會從該文件恢復memtable。
OPTIONS
rocksdb的配置文件, 配置參數說明可參考下一小節。
IDENTITY
存放當前rocksdb的唯一標識
LOCK
LOCK 進程的全局鎖,DB一旦被open, 其他進程將無法修改,報類似以下錯誤。
```
Open rocksdb ../data/fansnum_rocksdb/ failed,? reason: IO error: while open a file for lock: ../data/fansnum_rocksdb//LOCK: Permission deniedCommand init,? costtime: 0.946000 ms
```
LOG
rocksdb的操作日志文件, 可配置定期的統計信息寫入LOG. 可通過info_log_level調整日志輸出級別; 通過keep_log_file_num限制文件數量 等等。
寫數據測試
對于rocksdb的寫操作做了個測試數據分析:
* 測試機器是20CPU, 32G的內存, STAF磁盤
* 測試數據庫的總key量為3.9億,大小為5.2G。
* 最終生成的數據文件大小為2.63G, 數據壓縮比為50.6%,
* 生成的level結構為Level0沒有數據,Level1 5個sst文件,總大小290.22M; Level2 43個文件,總大小2.35G. 我們從level文件結構可以看出結果還是比較好的,比如查一個key,只需讀level1和level2中各一個sst文件即可。
* 總compaction次數255次,寫磁盤總數據量為22.9G, 寫放大為8.7,這就相當于在這次測試中平均每條記錄寫了8.7次磁盤。而且這個值會隨著數據量的增大而增大。
* 總耗時1140s, 平均每秒寫34.2w條數據。
讀性能測試
針對LRUcache, 做了一個讀性能的測試的數據分析
測試機器是20CPU, 32G的內存, STAF磁盤, 測試數據庫的總key量為3.9億, 數據庫大小為2.63G,測試數據的key量為1kw。根據創建不同大小的LRUcache, 主要關注了兩個性能指標,平均耗時和cache命中率。其中db默認的cache是8M的LRUcache。上圖可以看出隨著cache大小的增加, 平均耗時再遞減,命中率在遞增。16GB的cache可以將所有數據全部存在cache中。那時查詢一個key的平均耗時需要3.3微妙。
從上圖可以看出不同比例的數據的最大耗時, 比如在配置cache的db中,95%數據的都可以在14微秒之內返回, 99%的數據都可以在25微秒之內返回。配置最大cache時單線程訪問可以達到30w的qps。
可優化的點
寫放大
由于存在數據的compaction操作,所以rocksdb實際總的寫磁盤數據量并不是等同于輸入的數據量, 我們看下我寫一次全量粉絲數的數據量5.2G, 經過壓縮后2.63G, 直到所有數據更新完成后寫磁盤的量達到22.9G, 近8.7倍的寫放大。
讀放大
同時rocksdb也存在讀放大,基于之前的讀操作內容,我們可以知道一次get操作可能包含多次讀磁盤操作。而且每次讀一個記錄會將該記錄所在的block都讀入內存。
空間放大
空間放大主要表現在數據的更新和刪除實際都在compaction操作中執行,這樣在compaction之前一個key在數據庫中會存好多份記錄。這樣造成的空間的浪費。
應用場景
通過以上對rocksdb的了解,rocksdb比較適合小規模數據存儲,因為數據量越大,其寫放大,讀放大,空間放大就會越嚴重, 但具體到什么規模這個還定義不了,當前我們本地存儲10G左右的數據,性能杠杠的。適用于對寫性能要求高,同時有大量內存來緩存SST數據以達到快速讀取的場景。適用于存儲變長key,value的場景。