RocksDB關鍵設計詳解

0 說明


近日工作中使用了 RocksDB。RocksDB 的優點此處無需多說,它的一個 feature 是其有很多優化選項用于對 RocksDB 進行調優。欲熟悉這些參數,必須對其背后的原理有所了解,本文主要整理一些 RocksDB 的 wiki 文檔,以備自己參考之用。

1?Basic Operations


先介紹一些 RocksDB 的基本操作和基本架構。

1.1 LSM 與 WriteBatch

參考文檔5提到RocksDB 是一個快速存儲系統,它會充分挖掘 Flash or RAM 硬件的讀寫特性,支持單個 KV 的讀寫以及批量讀寫。RocksDB 自身采用的一些數據結構如 LSM/SKIPLIST 等結構使得其有讀放大、寫放大和空間使用放大的問題。

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的關鍵性質:

  • 將數據形成Log-Structured:在將數據寫入LSM內存結構之前,先記錄log。這樣LSM就可以將有易失性的內存看做永久性存儲器。并且信任內存上的數據,等到內存容量達到threshold再集體寫入磁盤。將數據形成Log-Structured,也是將整體存儲結構轉換成了“內存(in-memory)”存儲結構。
  • 將所有磁盤上數據不組織成一個整體索引結構,而組織成有序的文件集:因為磁盤隨機讀寫比順序讀寫慢3個數量級,LSM盡量將磁盤讀寫轉換成順序讀寫。將磁盤上的數據組織成B樹這樣的一個整體索引結構,雖然查找很高效,但是面對隨機讀寫,由于大量尋道導致其性能不佳。而LSM用了一種很有趣的方法,將所有數據不組織成一個整體索引結構,而組織成有序的文件集。每次LSM面對磁盤寫,將數據寫入一個或幾個新生成的文件,順序寫入且不能修改其他文件,這樣就將隨機讀寫轉換成了順序讀寫。LSM將一次性集體寫入的文件作為一個level,磁盤上劃分多level,level與level之間互相隔離。這就形成了,以寫入數據時間線形成的邏輯上、而非物理上的層級結構,這也就是為什么LSM被命名為”tree“,但不是“tree”。
  • 將數據按key排序,在合并不同file、level上的數據時類似merge-join:如果一直保持生成新的文件,不僅寫入會造成冗余空間,而且也會大量降低讀的性能。所以要高效的、周期性合并不同file、level。而如果數據是亂序的,根本做不到高效合并。所以LSM要將數據按key排序,在合并不同file、level上的數據時類似 merge-join。

很明顯,LSM犧牲了一部分讀的性能和增加了合并的開銷,換取了高效的寫性能。那LSM為什么要這么做?實際上,這就關系到對于磁盤寫已經沒有什么優化手段了,而對于磁盤讀,不論硬件還是軟件上都有優化的空間。通過多種優化后,讀性能雖然仍是下降,但可以控制在可接受范圍內。實際上,用于磁盤上的數據結構不同于用于內存上的數據結構,用于內存上的數據結構性能的瓶頸就在搜索復雜度,而用于磁盤上的數據結構性能的瓶頸在磁盤IO,甚至是磁盤IO的模式。

以上三段摘抄自參考文檔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 中的內容是有序的。

上圖所示,所有 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 還有一個好處是保持加快吞吐率。

1.2 同步寫 與 異步寫

默認情況下,RocksDB 的寫是異步的:僅僅把數據寫進了操作系統的緩存區就返回了,而這些數據被寫進磁盤是一個異步的過程。如果為了數據安全,可以用如下代碼把寫過程改為同步寫:

<span style="color:#000000"><span style="background-color:#f5f2f0"><span style="color:black"><code class="language-C">    rocksdb::WriteOptions write_options;write_options.sync = true;db->Put(write_options, …);</code></span></span></span>

這個選項會啟用 Posix 系統的?fsync(...) or fdatasync(...) or msync(..., MS_SYNC)?等函數。

異步寫的吞吐率是同步寫的一千多倍。異步寫的缺點是機器或者操作系統崩潰時可能丟掉最近一批寫請求發出的由操作系統緩存的數據,但是 RocksDB 自身崩潰并不會導致數據丟失。而機器或者操作系統崩潰的概率比較低,所以大部分情況下可以認為異步寫是安全的。

RocksDB 由于有 WAL 機制保證,所以即使崩潰,其重啟后會進行寫重放保證數據一致性。如果不在乎數據安全性,可以把?write_option.disableWAL?設置為 true,加快寫吞吐率。

RocksDB 調用 Posix API?fdatasync()?對數據進行異步寫。如果想用?fsync()?進行同步寫,可以設置?Options::use_fsync?為 true。

1.3 Snapshots

RocksDB 能夠保存某個版本的所有數據(可稱之為一個 Snapshot)以方便讀取操作,創建并讀取 Snapshot 方法如下:

<span style="color:#000000"><span style="background-color:#f5f2f0"><span style="color:black"><code class="language-C">    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);</code></span></span></span>

如果 ReadOptions::snapshot 為 null,則讀取的 snapshot 為 RocksDB 當前版本數據的 snapshot。

1.4 Slice & PinnableSlice

不管是?it->key()?還是?it->value(),其值類型都是?rocksdb::Slice。 Slice 自身由一個長度字段[ sizet size?]以及一個指向外部一個內存區域的指針[ const char* data_ ]構成,返回 Slice 比返回一個 string 廉價,并不存在內存拷貝的問題。RocksDB 自身會給 key 和 value 添加一個 C-style 的 ‘\0’,所以 slice 的指針指向的內存區域自身作為字符串輸出沒有問題。

Slice 與 string 之間的轉換代碼如下:

<span style="color:#000000"><span style="background-color:#f5f2f0"><span style="color:black"><code class="language-C">    rocksdb::Slice s1 = “hello”;std::string str(“world”);rocksdb::Slice s2 = str;OR:std::string str = s1.ToString();assert(str == std::string(“hello”));</code></span></span></span>

但是請注意 Slice 的安全性,有代碼如下:

<span style="color:#000000"><span style="background-color:#f5f2f0"><span style="color:black"><code class="language-C">    rocksdb::Slice slice;if (…) {std::string str = …;slice = str;}Use(slice);   </code></span></span></span>

當退出 if 語句塊后,slice 內部指針指向的內存區域已經不存在,此時再使用導致程序出問題。

Slice 自身雖然能夠減少內存拷貝,但是在離開相應的 scope 之后,其值就會被釋放,rocksdb v5.4.5 版本引入一個 PinnableSlice,其繼承自 Slice,可替換之前 Get 接口的出參:

<span style="color:#000000"><span style="background-color:#f5f2f0"><span style="color:black"><code class="language-cpp">Status <span style="color:#dd4a68">Get</span><span style="color:#999999">(</span><span style="color:#0077aa">const</span> ReadOptions<span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">&</span></span> options<span style="color:#999999">,</span>ColumnFamilyHandle<span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">*</span></span> column_family<span style="color:#999999">,</span> <span style="color:#0077aa">const</span> Slice<span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">&</span></span> key<span style="color:#999999">,</span>std<span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">::</span></span>string<span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">*</span></span> value<span style="color:#999999">)</span><span style="color:#0077aa">virtual</span> Status <span style="color:#dd4a68">Get</span><span style="color:#999999">(</span><span style="color:#0077aa">const</span> ReadOptions<span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">&</span></span> options<span style="color:#999999">,</span>ColumnFamilyHandle<span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">*</span></span> column_family<span style="color:#999999">,</span> <span style="color:#0077aa">const</span> Slice<span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">&</span></span> key<span style="color:#999999">,</span>PinnableSlice<span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">*</span></span> value<span style="color:#999999">)</span></code></span></span></span>

這里的 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:

<span style="color:#000000"><span style="background-color:#f5f2f0"><span style="color:black"><code class="language-cpp"> <span style="color:#0077aa">virtual</span> <span style="color:#0077aa">inline</span> Status <span style="color:#dd4a68">Get</span><span style="color:#999999">(</span><span style="color:#0077aa">const</span> ReadOptions<span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">&</span></span> options<span style="color:#999999">,</span>ColumnFamilyHandle<span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">*</span></span> column_family<span style="color:#999999">,</span> <span style="color:#0077aa">const</span> Slice<span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">&</span></span> key<span style="color:#999999">,</span>std<span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">::</span></span>string<span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">*</span></span> value<span style="color:#999999">)</span> <span style="color:#999999">{</span><span style="color:#dd4a68">assert</span><span style="color:#999999">(</span>value <span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">!=</span></span> <span style="color:#0077aa">nullptr</span><span style="color:#999999">)</span><span style="color:#999999">;</span>PinnableSlice <span style="color:#dd4a68">pinnable_val</span><span style="color:#999999">(</span>value<span style="color:#999999">)</span><span style="color:#999999">;</span><span style="color:#dd4a68">assert</span><span style="color:#999999">(</span><span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">!</span></span>pinnable_val<span style="color:#999999">.</span><span style="color:#dd4a68">IsPinned</span><span style="color:#999999">(</span><span style="color:#999999">)</span><span style="color:#999999">)</span><span style="color:#999999">;</span><span style="color:#0077aa">auto</span> s <span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">=</span></span> <span style="color:#dd4a68">Get</span><span style="color:#999999">(</span>options<span style="color:#999999">,</span> column_family<span style="color:#999999">,</span> key<span style="color:#999999">,</span> <span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">&</span></span>pinnable_val<span style="color:#999999">)</span><span style="color:#999999">;</span><span style="color:#0077aa">if</span> <span style="color:#999999">(</span>s<span style="color:#999999">.</span><span style="color:#dd4a68">ok</span><span style="color:#999999">(</span><span style="color:#999999">)</span> <span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">&&</span></span> pinnable_val<span style="color:#999999">.</span><span style="color:#dd4a68">IsPinned</span><span style="color:#999999">(</span><span style="color:#999999">)</span><span style="color:#999999">)</span> <span style="color:#999999">{</span>value<span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">-</span></span><span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">></span></span><span style="color:#dd4a68">assign</span><span style="color:#999999">(</span>pinnable_val<span style="color:#999999">.</span><span style="color:#dd4a68">data</span><span style="color:#999999">(</span><span style="color:#999999">)</span><span style="color:#999999">,</span> pinnable_val<span style="color:#999999">.</span><span style="color:#dd4a68">size</span><span style="color:#999999">(</span><span style="color:#999999">)</span><span style="color:#999999">)</span><span style="color:#999999">;</span><span style="color:#999999">}</span>  <span style="color:slategray">// else value is already assigned</span><span style="color:#0077aa">return</span> s<span style="color:#999999">;</span><span style="color:#999999">}</span></code></span></span></span>

通過 rocksdb/slice.h:PinnableSlice::PinSlice 實現代碼可以看出,只有在這個函數里 PinnableSlice::pinned_ 被賦值為 true, 同時內存區域存放在 PinnableSlice::data_ 指向的內存區域,故而 PinnableSlice::IsPinned 為 true,則 內部 buffer [ rocksdb/slice.h:PinnableSlice::buf_ ] 必定為空。

具體的編程用例可參考?pinnalble_slice.cc。

1.5 Transactions

當使用 TransactionDB 或者 OptimisticTransactionDB 的時候,可以使用 RocksDB 的 BEGIN/COMMIT/ROLLBACK 等事務 API。RocksDB 支持活鎖或者死等兩種事務。

WriteBatch 默認使用了事務,確保批量寫成功。

當打開一個 TransactionDB 的時候,如果 RocksDB 檢測到某個 key 已經被別的事務鎖住,則 RocksDB 會返回一個 error。如果打開成功,則所有相關 key 都會被 lock 住,直到事務結束。TransactionDB 的并發特性表現要比 OptimisticTransactionDB 好,但是 TransactionDB 的一個小問題就是不管寫發生在事務里或者事務外,他都會進行寫沖突檢測。TransactionDB 使用示例代碼如下:

<span style="color:#000000"><span style="background-color:#f5f2f0"><span style="color:black"><code class="language-C">    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;</code></span></span></span>

OptimisticTransactionDB 提供了一個更輕量的事務實現,它在進行寫之前不會進行寫沖突檢測,當對寫操作進行 commit 的時候如果發生了 lock 沖突導致寫操作失敗,則 RocksDB 會返回一個 error。這種事務使用了活鎖策略,適用于讀多寫少這種寫沖突概率比較低的場景下,使用示例代碼如下:

<span style="color:#000000"><span style="background-color:#f5f2f0"><span style="color:black"><code class="language-C">    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;</code></span></span></span>

參考文檔 6 詳細描述了?RocksDB?的 Transactions。

1.6 Column Family

CF 提供了對 DB 進行邏輯劃分開來的方法,用戶可以通過 CF 同時對多個 CF 的 KV 進行并行讀寫的方法,提高了并行度。

1.7 MemTable and Table factories

RocksDB 內存中的數據格式是 skiplist,磁盤則是以 table 形式存儲的 SST 文件格式。

table 格式有兩種:繼承自 leveldb 的文件格式【詳見參考文檔2】和 PlainTable 格式【詳見參考文檔3】。PlainTable 格式是針對 低查詢延遲 或者低延遲存儲媒介如 SSD 特別別優化的一種文件格式。

1.8 Block size

RocksDB 把相鄰的 key 放到同一個 block 中,block 是數據存儲和傳遞的基本單元。默認 Block 的大小是 4096B,數據未經壓縮。

經常進行 bulk scan 操作的用戶可能希望增大 block size,而經常進行單 key 讀寫的用戶則可能希望減小其值,官方建議這個值減小不要低于 1KB 的下限,變大也不要超過?a few megabytes。啟用壓縮也可以起到增大 block size 的好處。

修改 Block size 的方法是修改?Options::block_size

1.9 Writer Buffer

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.10 Key Layout

在?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 里,以方便快速查詢。

1.11 Checksums

Rocksdb 對每個 kv 以及整體數據文件都分別計算了 checksum,以進行數據正確性校驗。下面有兩個選項對 checksum 的行為進行控制。

  • ReadOptions::verify_checksums?強制對每次從磁盤讀取的數據進行校驗,這個選項默認為 true。
  • Options::paranoid_checks?這個選項為 true 的時候,如果 RocksDB 打開一個數據檢測到內部數據部分錯亂,馬上拋出一個錯誤。這個選擇默認為 false。

如果 RocksDB 的數據錯亂,RocksDB 會盡量把它隔離出來,保證大部分數據的可用性和正確性。

1.12 Approximate Sizes

GetApproximateSizes?方法可以返回一個 key range 的磁盤占用空間大致使用量,示例代碼如下:

<span style="color:#000000"><span style="background-color:#f5f2f0"><span style="color:black"><code class="language-C">    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);  </code></span></span></span>

上面的?sizes[0]?返回?[a..c)?key range 的磁盤使用量,而?sizes[1]?返回?[x..z)?key range 的磁盤使用量。

1.13 Purging WAL files

一般情況下,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 參數優先。

1.14 Prefix Iterators

許多 LSM 引擎不支持高效的 RangeScan 操作,因為 Range 操作需要掃描所有的數據文件。一般情況下常規的技術手段是給 key 建立索引,只用遍歷 key 就可以了。應用可以通過確認?prefix_extractor?指定一個可以的前綴,RocksDB 可以為這些 key prefix 建立 Bloom 索引,以加快查詢速度。

1.15 Multi-Threaded Compactions

參考文檔 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 住。

1.16 Incremental Backups and Replication

RocksDB 的 API?GetUpdatesSince?可以讓調用者從 transaction log 獲知最近被更新的 key(原文意為用 tail 方式讀取 transaction log),通過這個 API 可以進行數據的增量備份。

RocksDB 在進行數據備份時候,可以調用 API?DisableFileDeletions?停止刪除文件操作,調用 API?GetLiveFiles/GetSortedWalFiles?以檢索活躍文件列表,然后進行數據備份。備份工作完成以后在調用 API?EnableFileDeletions?讓 RocksDB 再啟動過期文件淘汰工作。

1.17 Thread Pool

RocksDB 會創建一個 thread pool 與 Env 對象進行關聯,線程池中線程的數目可以通過?Env::SetBackgroundThreads()?設定。通過這個線程池可以執行 compaction 與 memtable flush 任務。

當 memtable flush 和 compaction 兩個任務同時執行的時候,會導致寫請求被 hang 住。RocksDB 建議創建兩個線程池,分別指定 HIGH 和 LOW 兩個優先級。默認情況下 HIGH 線程池執行 memtable flush 任務,LOW 線程池執行 compaction 任務。

相關代碼示例如下:

<span style="color:#000000"><span style="background-color:#f5f2f0"><span style="color:black"><code class="language-cpp"><span style="color:#990055">#<span style="color:#0077aa">include</span> “rocksdb/env.h”</span>
<span style="color:#990055">#<span style="color:#0077aa">include</span> “rocksdb/db.h”</span><span style="color:#0077aa">auto</span> env <span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">=</span></span> rocksdb<span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">::</span></span>Env<span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">::</span></span><span style="color:#dd4a68">Default</span><span style="color:#999999">(</span><span style="color:#999999">)</span><span style="color:#999999">;</span>
env<span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">-</span></span><span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">></span></span><span style="color:#dd4a68">SetBackgroundThreads</span><span style="color:#999999">(</span><span style="color:#990055">2</span><span style="color:#999999">,</span> rocksdb<span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">::</span></span>Env<span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">::</span></span>LOW<span style="color:#999999">)</span><span style="color:#999999">;</span>
env<span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">-</span></span><span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">></span></span><span style="color:#dd4a68">SetBackgroundThreads</span><span style="color:#999999">(</span><span style="color:#990055">1</span><span style="color:#999999">,</span> rocksdb<span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">::</span></span>Env<span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">::</span></span>HIGH<span style="color:#999999">)</span><span style="color:#999999">;</span>
rocksdb<span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">::</span></span>DB<span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">*</span></span> db<span style="color:#999999">;</span>
rocksdb<span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">::</span></span>Options options<span style="color:#999999">;</span>
options<span style="color:#999999">.</span>env <span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">=</span></span> env<span style="color:#999999">;</span>
options<span style="color:#999999">.</span>max_background_compactions <span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">=</span></span> <span style="color:#990055">2</span><span style="color:#999999">;</span>
options<span style="color:#999999">.</span>max_background_flushes <span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">=</span></span> <span style="color:#990055">1</span><span style="color:#999999">;</span>
rocksdb<span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">::</span></span>Status status <span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">=</span></span> rocksdb<span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">::</span></span>DB<span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">::</span></span><span style="color:#dd4a68">Open</span><span style="color:#999999">(</span>options<span style="color:#999999">,</span> “<span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">/</span></span>tmp<span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">/</span></span>testdb”<span style="color:#999999">,</span> <span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">&</span></span>db<span style="color:#999999">)</span><span style="color:#999999">;</span>
<span style="color:#dd4a68">assert</span><span style="color:#999999">(</span>status<span style="color:#999999">.</span><span style="color:#dd4a68">ok</span><span style="color:#999999">(</span><span style="color:#999999">)</span><span style="color:#999999">)</span><span style="color:#999999">;</span></code></span></span></span>

還有其他一些參數,可詳細閱讀參考文檔4。
?

1.18 Bloom Filter

RocksDB 的每個 SST 文件都包含一個 Bloom filter。Bloom Filter 只對特定的一組 keys 有效,所以只有新的 SST 文件創建的時候才會生成這個 filter。當兩個 SST 文件合并的時候,會生成新的 filter 數據。

當 SST 文件加載進內存的時候,filter 也會被加載進內存,當關閉 SST 文件的時候,filter 也會被關閉。如果想讓 filter 常駐內存,可以用如下代碼設置:

<span style="color:#000000"><span style="background-color:#f5f2f0"><span style="color:black"><code class="language-cpp">BlockBasedTableOptions<span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">::</span></span>cache_index_and_filter_blocks<span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">=</span></span><span style="color:#990055">true</span></code></span></span></span>

一般情況下不要修改 filter 相關參數。如果需要修改,相關設置上面已經說過,此處不再多談,詳細內容見參考文檔 7。

1.19 Time to Live

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 的使用有以下注意事項:

  • 1 TTL 其值單位是秒,如果 TTL 值為 0 或者負數,則 TTL 值為?infinity,即永不過期;
  • 2 每個 kv 被插入數據文件中的時候會帶上創建時的機器?(int32_t)Timestamp?時間值;
  • 3 compaction 時如果 kv 滿足條件?Timestamp+ttl<time_now,則會被淘汰掉;
  • 4 Get/Iterator 的時候可能返回過期的 kv(compact 任務還未執行);
  • 5 不同的?DBWithTTL::Open?可能會帶上不同的 TTL 值,此時 kv 以最大的 TTL 值為準;
  • 6 如果?DBWithTTL::Open?的參數?read_only?為 true,則不會觸發 compact 任務,不會有過期數據被刪除。

2?RocksDB Memory


RocksDB的內存大致有如下四個區:

  • Block Cache
  • Indexes and bloom filters
  • Memtables
  • Blocked pinned by iterators
2.1 Block Cache

第三節詳述了 Block Cache,這里只給出總結性描述:它存儲一些讀緩存數據,它的下一層是操作系統的 Page Cache。

2.2 Indexes and bloom filters

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);

2.3 Indexes and bloom filters

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 內存量大小:

<span style="color:#000000"><span style="background-color:#f5f2f0"><span style="color:black"><code class="language-cpp">    std<span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">::</span></span>string out<span style="color:#999999">;</span>db<span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">-</span></span><span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">></span></span><span style="color:#dd4a68">GetProperty</span><span style="color:#999999">(</span>“rocksdb<span style="color:#999999">.</span>cur<span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">-</span></span>size<span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">-</span></span>all<span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">-</span></span>mem<span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">-</span></span>tables”<span style="color:#999999">,</span> <span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">&</span></span>out<span style="color:#999999">)</span><span style="color:#999999">;</span></code></span></span></span>
2.4 Blocks pinned by iterators

這部分內存空間一般占用總量不多,但是如果有 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();

2.5 讀流程

RocksDB 的讀流程分為邏輯讀(logical read)和物理讀(physical read)。邏輯讀通常是對 cache【Block Cache & Table Cache】進行讀取,物理讀就是直接讀磁盤。

參考文檔 12 詳細描述了 LeveDB(RocksDB)的讀流程,轉述如下:

  • 在MemTable中查找,無法命中轉到下一流程;
  • 在immutable_memtable中查找,查找不中轉到下一流程;
  • 在第0層SSTable中查找,無法命中轉到下一流程;

    對于L0 的文件,RocksDB 采用遍歷的方法查找,所以為了查找效率 RocksDB 會控制 L0 的文件個數。

  • 在剩余SSTable中查找。

    對于 L1 層以及 L1 層以上層級的文件,每個 SSTable 沒有交疊,可以使用二分查找快速找到 key 所在的 Level 以及 SSTfile。

至于寫流程,請參閱 ### 5 Flush & Compaction 章節內容。

2.6 memory pool

不管 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 的內容分配代碼模塊如下:

3?Block Cache


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。

<span style="color:#000000"><span style="background-color:#f5f2f0"><span style="color:black"><code class="language-cpp">    std<span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">::</span></span>shared_ptr<span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59"><</span></span>Cache<span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">></span></span> cache <span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">=</span></span> <span style="color:#dd4a68">NewLRUCache</span><span style="color:#999999">(</span>capacity<span style="color:#999999">)</span><span style="color:#999999">;</span>BlockedBasedTableOptions table_options<span style="color:#999999">;</span>table_options<span style="color:#999999">.</span>block_cache <span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">=</span></span> cache<span style="color:#999999">;</span>Options options<span style="color:#999999">;</span>options<span style="color:#999999">.</span>table_factory<span style="color:#999999">.</span><span style="color:#dd4a68">reset</span><span style="color:#999999">(</span><span style="color:#0077aa">new</span> <span style="color:#dd4a68">BlockedBasedTableFactory</span><span style="color:#999999">(</span>table_options<span style="color:#999999">)</span><span style="color:#999999">)</span><span style="color:#999999">;</span>  </code></span></span></span>

這個 Cache 是不壓縮數據的,用戶可以設置壓縮數據 BlockCache,方法如下:

<span style="color:#000000"><span style="background-color:#f5f2f0"><span style="color:black"><code class="language-cpp">table_options<span style="color:#999999">.</span>block_cache_compressed <span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">=</span></span> cache<span style="color:#999999">;</span></code></span></span></span>

如果 Cache 為 nullptr,則RocksDB會創建一個,如果想禁用 Cache,可以設置如下 Option:

<span style="color:#000000"><span style="background-color:#f5f2f0"><span style="color:black"><code class="language-cpp">table_options<span style="color:#999999">.</span>no_block_cache <span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">=</span></span> <span style="color:#990055">true</span><span style="color:#999999">;</span></code></span></span></span>

默認情況下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初始化之時相關參數:

  • capacity 總的內存使用量
  • num_shards_bits 把 key 的前 n bits 作為 shard id,則總 shard 的數目為 2 ^ num_shards_bits;
  • strict_capacity_limit 在一些極端情況下 block cache 的總體使用量可能超過 capacity,如在對 block 進行讀或者迭代讀取的時候可能有插入數據的操作,此時可能因為加鎖導致有些數據無法及時淘汰,使得總體capacity超標。如果這個選項設置為 true,則此時插入操作是被允許的,但有可能導致進程 OOM。如果設置為 false,則插入操作會被 refuse,同時讀取以及遍歷操作有可能失敗。這個選項對每個 shard 都有效,這就意味著有的 shard 可能內存已滿, 別的 shard 卻有很多空閑。
  • high_pri_pool_ratio block中為高優先級的 block 保留多少比例的空間,這個選項只有 LRU Cache 有。

默認情況下 index 和filter block 與 block cache 是獨立的,用戶不能設定二者的內存空間使用量,但為了控制 RocksDB 的內存空間使用量,可以用如下代碼把 index 和 filter 也放在 block cache 中:

<span style="color:#000000"><span style="background-color:#f5f2f0"><span style="color:black"><code class="language-cpp">BlockBasedTableOptions table_options<span style="color:#999999">;</span>
table_options<span style="color:#999999">.</span>cache_index_and_filter_blocks <span style="background-color:rgba(255, 255, 255, 0.5)"><span style="color:#a67f59">=</span></span> <span style="color:#990055">true</span><span style="color:#999999">;</span></code></span></span></span>

index 與 filter 一般訪問頻次比 data 高,所以把他們放到一起會導致內存空間與 cpu 資源競爭,進而導致 cache 性能抖動厲害。有如下兩個參數需要注意:cacheindexfilterblockswithhighpriority 和 highpripoolratio 一樣,這個參數只對 LRU Cache 有效,兩者須同時生效。這個選項會把 LRU Cache 劃分為高 prio 和低 prio 區,data 放在 low 區,index 和 filter 放在 high 區,如果高區占用的內存空間超過了 capacity * highpripoolratio,則會侵占 low 區的尾部數據空間。

  • pin_l0_filter_and_index_blocks_in_cache 把 level0 的 index 以及 filter block 放到 Block Cache 中,因為 l0 訪問頻次最高,一般內存容量不大,占用不了多大內存空間。

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% 會被用于測算。

4?Column Families


RocksDB 3.0 以后添加了一個 Column Family【后面簡稱 CF】 的feature,每個 kv 存儲之時都必須指定其所在的 CF。RocksDB為了兼容以往版本,默認創建一個 “default” 的CF。存儲 kv 時如果不指定 CF,RocksDB 會把其存入 “default” CF 中。

4.1 Option

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 的優先級。

4.2?Set Up Option

RocksDB 有很多選項以專門的目的進行設置,但是大部分情況下不需要進行特殊的優化。這里只列出一個常用的優化選項。

  • cf_options.write_buffer_size

CF 的 write buffer 的最大 size。最差情況下 RocksDB 使用的內存量會翻倍,所以一般情況下不要輕易修改其值。

  • Set block cache size

這個值一般設置為 RocksDB 想要使用的內存總量的 1/3,其余的留給 OS 的 page cache。

<span style="color:#000000"><span style="background-color:#f5f2f0"><span style="color:black"><code class="language-C">    BlockBasedTableOptions table_options;… \\ set options in table_optionsoptions.table_factory.reset(newstd::shared_ptr<Cache> cache = NewLRUCache(<your_cache_size>);table_options.block_cache = cache;BlockBasedTableFactory(table_options));</code></span></span></span>

本進程的所有的 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。

  • Bloom filter

官方真正建議修改的參數只有這個 filter 參數。如果大量使用迭代方法,不要修改這個參數,如果大量調用 Get() 接口,建議修改這個參數。修改方法如下:

<span style="color:#000000"><span style="background-color:#f5f2f0"><span style="color:black"><code class="language-C">    table_options.filter_policy.reset(NewBloomFilterPolicy(10, false));</code></span></span></span>

一個可能的優化設定如下:

<span style="color:#000000"><span style="background-color:#f5f2f0"><span style="color:black"><code class="language-none">    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;</code></span></span></span>

上面只是羅列了一些優化選項,這些選項也只能在進程啟動的時候設定。更多的選項請詳細閱讀參考文檔1。

4.3 WriteOption & Persistence

參考文檔 5 的 Persistence 一節提到,RocksDB 每次接收寫請求的時候,請求內容會先被寫入 WAL transaction log,然后再把數據寫入 memfile 里面。

Put 函數的參數 WriteOptions 里有一個選項可以指明是否需要把寫請求的內容寫入 WAL log 里面。

RocksDB 內部有一個 batch-commit 機制,通過一次 commit 批量地在一次 sync 操作里把所有 transactions log 寫入磁盤。

5 Flush & Compaction & Merge


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?有利于減少讀放大卻會增大空間放大。

5.1 Leveled 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 文件的數目。

ParamValueDescriptionScore
level0filenumcompactiontrigger4num 為 4 時,達到 compact 條件num < 20 時 Score = num / 4
level0slowdownwrites_trigger20num 為 20 時,RocksDB 會減慢寫入速度20 <= num && num < 24 時 Score = 10000
level0stopwrites_trigger24num 為 24 時,RocksDB 停止寫入文件,盡快對 L0 進行 compact24 <= num 時 Score = 1000000

對于 L1+ 層,score = LevelBytes / TargetSize。

5.1.2 Level Max Bytes

每個 level 容量總大小的計算前文已經提過,

ParamValueDescription
maxbytesforlevelbase10485760L1 總大小
maxbytesforlevelmultiplier10最大乘法因子
maxbytesforlevelmultiplier_addtl[2…6]1L2 ~ 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 文件的策略如下:

  • 1 選擇 seek 次數為 0 的文件進行 merge,如果沒有 seek 次數為 0 的文件,則從第一個文件開始進行 compact;
  • 2 一次 compact 后記錄本次結束的 key,下次 compact 開始時以這個 key 為起始繼續進行 compact。
5.1.4 compaction

大致的 compaction 流程大致為:

  • 1 找到 score 最高的 level;
  • 2 根據一定策略從 level 中選擇一個 sst 文件進行 compact,L0 的各個 sst 文件之間 key range 【minkey, maxkey】 有重疊,所以可能一次選取多個;
  • 3 獲取 sst 文件的 minkey 和 maxkey;
  • 4 從 level + 1 中選取出于 (minkey, maxkey) 用重疊的 sst 文件,有重疊的文件則把文件與 level 中的文件進行合并(merge - sort)作為目標文件,沒有重疊文件則把原始文件作為目標文件;
  • 5 對目標文件進行壓縮后放入 level + 1 中。
5.1.5 并行 Compact 與 sub-compact

參數 maxbackgroundcompactions 大于 1 時,RocksDB 會進行并行 Compact,但 L0 和 L1 層的 Compaction 任務不能進行并行。

一次 compaction 只能 compact 一個或者多個文件,這會約束整體 compaction 速度。用戶可以設置 max_subcompactions 參數大于 1,RocksDB 如上圖一樣嘗試把一個文件拆為多個 sub,然后啟動多個線程執行 sub-compact。

5.2 Universal Compaction

Univesal Compaction 主要針對 L0。當 L0 中的文件個數多于?level0_file_num_compaction_trigger,則啟動 compact。

L0 中所有的 sst 文件都可能存在重疊的 key range,假設所有的 sst 文件組成了文件隊列 R1,R2,R3,...,Rn,R1 文件的數據是最新的,R2 其次,Rn 則包含了最老的數據,其 compact 流程如下:

  • 1 如果空間放大超過?max_size_amplification_percent,則對所有的 sst 進行 compaction(就是所謂的 full compaction);
  • 2 如果前size(R1)小于size(R2)在一定比例,默認1%,則與R1與R2一起進行compaction,如果(R1+R2)*(100+ratio)%100<R3,則將R3也加入到compaction任務中,依次順序加入sst文件;
  • 如果第1和第2種情況都沒有compaction,則強制選擇前N個文件進行合并。

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。

5.4 Merge

RocksDB 自身之提供了 Put/Delete/Get 等接口,若需要在現有值上進行修改操作【或者成為增量更新】,可以借助這三個操作進行以下操作實現之:

  • 調用 Get 接口然后獲取其值;
  • 在內存中修改這個值;
  • 調用 Put 接口寫回 RocksDB。

如果希望整個過程是原子操作,就需要借助 RocksDB 的 Merge 接口了。參考文檔14?給出了 RocksDB Merge 接口定義如下:

  • 1 封裝了read - modify - write語義,對外統一提供簡單的抽象接口;
  • 2 減少用戶重復觸發Get操作引入的性能損耗;
  • 3 通過決定合并操作的時間和方式,來優化后端性能,并達到并不改變底層更新的語義;
  • 4 漸進式的更新,來均攤更新帶來帶來的性能損耗,以得到漸進式的性能提升。

RocksDB 提供了一個 MergeOperator 作為 Merge 接口,其中一個子類 AssociativeMergeOperator 可在大部分場景下使用,其定義如下:

<span style="color:#000000"><span style="background-color:#f5f2f0"><span style="color:black"><code class="language-C">    // 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 functionsvirtual 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;};</code></span></span></span>

RocksDB AssociativeMergeOperator 被稱為關聯性 Merge Operator,參考文檔14?給出了關聯性的定義:

  • 調用Put接口寫入RocksDB的數據的格式和Merge接口是相同的
  • 用用戶自定義的merge操作,可以將多個merge操作數合并成一個

**MergeOperator還可以用于非關聯型數據類型的更新。** 例如,在RocksDB中保存json字符串,即Put接口寫入data的格式為合法的json字符串。而Merge接口只希望更新json中的某個字段。所以代碼可能是這樣

<span style="color:#000000"><span style="background-color:#f5f2f0"><span style="color:black"><code class="language-C">    // Put/store the json string into to the databasedb_->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 stringdb_->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;};</code></span></span></span>
  • 工作原理

當調用DB::Put()和DB:Merge()接口時, 并不需要立刻計算最后的結果. RocksDB將計算的動作延后觸發, 例如在下一次用戶調用Get, 或者RocksDB決定做Compaction時. 所以, 當merge的動作真正開始做的時候, 可能積壓(stack)了多個操作數需要處理. 這種情況就需要MergeOperator::FullMerge來對existing_value和一個操作數序列進行計算, 得到最終的值.

  • PartialMerge 和 Stacking

有時候, 在調用FullMerge之前, 可以先對某些merge操作數進行合并處理, 而不是將它們保存起來, 這就是PartialMerge的作用: 將兩個操作數合并為一個, 減少FullMerge的工作量.
當遇到兩個merge操作數時, RocksDB總是先會嘗試調用用戶的PartialMerge方法來做合并, 如果PartialMerge返回false才會保存操作數. 當遇到Put/Delete操作, 就會調用FullMerge將已存在的值和操作數序列傳入, 計算出最終的值.

  • 使用Associative Merge的場景

merge 操作數的格式和Put相同
多個順序的merge操作數可以合并成一個

  • 使用Generic Merge的場景

merge 操作數的格式和Put不同
當多個merge操作數可以合并時,PartialMerge()方法返回true

*!!!: 本節文字摘抄自?參考文檔14?。

6 磁盤文件


參考文檔 12 列舉了 RocksDB 磁盤上數據文件的種類:

<span style="color:#000000"><span style="background-color:#f5f2f0"><span style="color:black"><code class="language-none">* db的操作日志
* 存儲實際數據的 SSTable 文件
* DB的元信息 Manifest 文件
* 記錄當前正在使用的 Manifest 文件,它的內容就是當前的 manifest 文件名
* 系統的運行日志,記錄系統的運行信息或者錯誤日志。
* 臨時數據庫文件,repair 時臨時生成的。</code></span></span></span>

manifest 文件記載了所有 SSTable 文件的 key 的范圍、level 級別等數據。

上面是 leveldb 的架構圖,可以作為參考,明白各種文件的作用。

6.1 log 文件

log 文件就是 WAL。

如上圖,log 文件的邏輯單位是 Record,物理單位是 block,每個 Record 可以存在于一個 block 中,也可以占用多個 block。Record 的詳細結構見上圖文字部分,其 type 字段的意義見下圖。

從上圖可見 Record type的意義:如果某 KV 過長則可以用多 Record 存儲。

6.2 Manifest 文件

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 文件有以下文件內容:

  • coparator名、log編號、前一個log編號、下一個文件編號、上一個序列號,這些都是日志、sstable文件使用到的重要信息,這些字段不一定必然存在;
  • 其次是compact點,可能有多個,寫入格式為{kCompactPointer, level, internal key}
  • 其后是刪除文件,可能有多個,格式為{kDeletedFile, level, file number}。
  • 最后是新文件,可能有多個,格式為{kNewFile, level, file number, file size, min key, max key}。

RocksDB MANIFEST文件所保存的數據基本是來自于VersionEdit這個結構,MANIFEST包含了兩個文件,一個log文件一個包含最新MANIFEST文件名的文件,Manifest的log文件名是這樣 MANIFEST-(seqnumber),這個seq會一直增長,只有當 超過了指定的大小之后,MANIFEST會刷新一個新的文件,當新的文件刷新到磁盤(并且文件名更新)之后,老的文件會被刪除掉,這里可以認為每一次MANIFEST的更新都代表一次snapshot,其結構描述如下:

<span style="color:#000000"><span style="background-color:#f5f2f0"><span style="color:black"><code class="language-none">MANIFEST = { CURRENT, MANIFEST-<seq-no>* }
CURRENT = File pointer to the latest manifest log
MANIFEST-<seq no> = Contains snapshot of RocksDB state and subsequent modifications</code></span></span></span>

在RocksDB中任意時間存儲引擎的狀態都會保存為一個Version(也就是SST的集合),而每次對Version的修改都是一個VersionEdit,而最終這些VersionEdit就是 組成manifest-log文件的內容。

下面就是MANIFEST的log文件的基本構成:

<span style="color:#000000"><span style="background-color:#f5f2f0"><span style="color:black"><code class="language-none">version-edit      = Any RocksDB state change
version           = { version-edit* }
manifest-log-file = { version, version-edit* }= { version-edit* }</code></span></span></span>

關于 VersionSet 相關代碼分析見參考文檔13。

6.3 SSTfile

SSTfile 結構如下:

<span style="color:#000000"><span style="background-color:#f5f2f0"><span style="color:black"><code class="language-none"><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></code></span></span></span>

LevelDB 的 SSTfile 結構如下:

見參考文檔12,SSTtable 大致分為幾個部分:

  • 數據塊 Data Block,直接存儲有序鍵值對,是 SSTfile 的數據實體;
  • Meta Block,存儲Filter相關信息;
  • Meta Index Block,對Meta Block的索引,它只有一條記錄,key是meta index的名字(也就是Filter的名字),value為指向meta index的位置;
  • Index Block,是對Data Block的索引,對于其中的每個記錄,其key >=Data Block最后一條記錄的key,同時<其后Data Block的第一條記錄的key;value是指向data index的位置信息;
  • Footer,指向各個分區的位置和大小。

block 結構如下圖:

record 結構如下圖:

Footer 結構如下圖:

6.4 memtable

memtable 中存儲了一些 metadata 和 data,data 在 skiplist 中存儲。metadata 數據如下(源自參考文檔 12):

  • 當前日志句柄;
  • 版本管理器、當前的版本信息(對應 compaction)和對應的持久化文件標示;
  • 當前的全部db配置信息比如 comparator 及其對應的 memtable 指針;
  • 當前的狀態信息以決定是否需要持久化 memtable 和合并 sstable;
  • sstable 文件集合的信息。
6.5 VersionSet

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文件編號的狀態信息。

6.6 MetaData Restore

本節內容節選自參考文檔 12。

為了避免進程崩潰或機器宕機導致的數據丟失,LevelDB 需要將元信息數據持久化到磁盤,承擔這個任務的就是 Manifest 文件,每當有新的Version產生都需要更新 Manifest。

新增數據正好對應于VersionEdit內容,也就是說Manifest文件記錄的是一組VersionEdit值,在Manifest中的一次增量內容稱作一個Block。

Manifest Block 的詳細結構如上圖所示。

上圖最上面的流程顯示了恢復元信息的過程,也就是一次應用 VersionEdit 的過程,這個過程會有大量的臨時 Version 產生,但這種方法顯然太過于耗費資源,LevelDB 引入 VersionSet::Builder 來避免這種中間變量,方法是先將所有的VersoinEdit內容整理到VersionBuilder中,然后一次應用產生最終的Version,詳細流程如上圖下邊流程所示。

數據恢復的詳細流程如下:

  • 依次讀取Manifest文件中的每一個Block, 將從文件中讀出的Record反序列化為VersionEdit;
  • 將每一個的VersionEdit Apply到VersionSet::Builder中,之后從VersionSet::Builder的信息中生成Version;
  • 計算compactionlevel、compactionscore
  • 將新生成的Version掛到VersionSet中,并初始化VersionSet的manifestfilenumber, nextfilenumber,lastsequence,lognumber,prevlognumber_ 信息;
6.7 Snapshot

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 文件會被刪除掉。

6.8 Backup

RocksDB 提供了 point-of-time 數據備份功能,可以調用?BackupEngine::CreateNewBackup(db, flush_before_backup = false)?接口進行數據備份, 其大致流程如下:

  • 禁止刪除文件(sst 文件和 log 文件);
  • 調用?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內容如下:

<span style="color:#000000"><span style="background-color:#f5f2f0"><span style="color:black"><code class="language-tex">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</code></span></span></span>

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 狀態一致;

7 FAQ


官方 wiki 【參考文檔 11】提供了一份 FAQ,下面節選一些比較有意義的建議,其他內容請移步官方文檔。

  • 1 如果機器崩潰后重啟,則 RocksDB 能夠恢復的數據是同步寫【WriteOptions.sync=true】調用?DB::SyncWAL()?之前的數據 或者已經被寫入 L0 的 memtable 的數據都是安全的;
  • 2 可以通過?GetIntProperty(cf_handle, “rocksdb.estimate-num-keys")?獲取一個 column family 中大概的 key 的個數;
  • 3 可以通過?GetAggregatedIntProperty(“rocksdb.estimate-num-keys", &num_keys)?獲取整個 RocksDB 中大概的 key 的總數,之所以只能獲取一個大概數值是因為 RocksDB 的磁盤文件有重復 key,而且 compact 的時候會進行 key 的淘汰,所以無法精確獲取;
  • 4 Put()/Write()/Get()/NewIterator() 這幾個 API 都是線程安全的;
  • 5 多個進程可以同時打開同一個 RocksDB 文件,但是其中只能有一個寫進程,其他的都只能通過?DB::OpenForReadOnly()?對 RocksDB 進行只讀訪問;
  • 6 當進程中還有線程在對 RocksDB 進行 讀、寫或者手工 compaction 的時候,不能強行關閉它;
  • 7 RocksDB 本身不建議使用大 key,但是它支持的 key 的最大長度是 8MB,value 的最大長度是 3GB;
  • 8 RocksDB 最佳實踐:一個線程進行寫,且以順序方式寫;以 batch 方式把數據寫入 RocksDB;使用 vector memtable;確保?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(),后面三個方法會被自動啟用;
  • 9 關閉 RocksDB 對象時,如果是通過 DestroyDB() 去關閉時,這個 RocksDB 還正被另一個進程訪問,則會造成不可預料的后果;
  • 10 可以通過?DBOptions::db_paths/DBOptions::db_log_dir/DBOptions::wal_dir?三個參數分別存儲 RocksDB 的數據,這種情況下如果要釋放 RocksDB 的數據可以通過 DestroyDB() 這個 API 去執行刪除任務;
  • 11 當?BackupOptions::backup_log_files?或者?flush_before_backup?的值為 true 的時候,如果程序調用?CreateNewBackup()?則 RocksDB 會創建?point-in-time snapshot,RocksDB進行數據備份的時候不會影響正常的讀寫邏輯;
  • 12 RocksDB 啟動之后不能修改?prefix extractor
  • 13 SstFileWriter API 可以用來創建 SST 文件,如果這些 SST 文件被添加到別的 RocksDB 數據庫發生 key range 重疊,則會導致數據錯亂;
  • 14 編譯 RocksDB 的 gcc 版本 最低是 4.7,推薦 4.8 以上;
  • 15 單個文件系統如 ext3 或者 xfs 可以使用多個磁盤,然后讓 RocksDB 在這個文件系統上運行進而使用多個磁盤;
  • 16 使用多磁盤時,RAID 的 stripe size 不能小于 64kb,推薦使用1MB;
  • 17 RocksDB 可以針對每個 SST 文件通過?ColumnFamilyOptions::bottommost_compression?使用不同的壓縮的方法;
  • 18 當多個 Handle 指向同一個 Column Family 時,其中一個線程通過 DropColumnFamily() 刪除一個 CF 的時候,其引用計數會減一,直至為 0 時整個 CF 會被刪除;
  • 19 RocksDB 接受一個寫請求的時候,可能因為 compact 會導致 RocksDB 多次讀取數據文件進行數據合并操作;
  • 20 RocksDB 不直接支持數據的復制,但是提供了 API GetUpdatesSince() 供用戶調用以獲取某個時間點以后更新的 kv;
  • 21 Options 的 block_size 是指 block 的內存空間大小,與數據是否壓縮無關;
  • 22 options.prefixextractor 一旦啟用,就無法繼續使用 Prev() 和 SeekToLast() 兩個 API,可以把 ReadOptions.totalorder_seek 設置為 true,以禁用?prefix iterating
  • 23 當 BlockBaseTableOptions::cacheindexandfilterblocks 的值為 true 時,在進行 Get() 調用的時候相應數據的 bloom filter 和 index block 會被放進 LRU cache 中,如果這個參數為 false 則只有 memtable 的 index 和 bloom filter 會被放進內存中;
  • 24 當調用 Put() 或者 Write() 時參數 WriteOptions.sync 的值為 true,則本次寫以前的所有 WriteOptions.disableWAL 為 false 的寫的內容都會被固化到磁盤上;
  • 25 禁用 WAL 時,DB::Flush() 只對單個 Column Family 的數據固化操作是原子的,對多個 Column Family 的數據操作則不是原子的,官方考慮將來會支持這個 feature;
  • 26 當使用自定義的 comparators 或者 merge operators 時,ldb 工具就無法讀取 sst 文件數據;
  • 27 RocksDB 執行前臺的 Get() 和 Write() 任務遇到錯誤時,它會返回 rocksdb::IOError 具體值;
  • 28 RocksDB 執行后臺任務遇到錯誤時 且 options.paranoid_checks 值為 true,則 RocksDB 會自動進入只讀模式;
  • 29 RocksDB 一個線程執行 compact 的時候,這個任務是不可取消的;
  • 30 RocksDB 一個線程執行 compact 任務的時候,可以在另一個線程調用 CancelAllBackgroundWork(db, true) 以中斷正在執行的 compact 任務;
  • 31 當多個進程打開一個 RocksDB 時,如果指定的 compact 方式不一樣,則后面的進程會打開失敗;
  • 32 Column Family 使用場景:(1) 不同的 Column Family 可以使用不同的 setting/comparators/compression types/merge operators/compaction filters;(2) 對數據進行邏輯隔離,方便分別刪除;(3) 一個 Column Family 存儲 metadata,另一個存儲 data;
  • 33 使用一個 RocksDB 就是使用一個物理存儲系統,使用一個 Column Family 則是使用一個邏輯存儲系統,二者主要區別體現在 數據備份、原子寫以及寫性能表現上。DB 是數據備份和復制以及 checkpoint 的基本單位,但是 Column Family 則利用 BatchWrite,因為這個操作是可以跨 Column Family 的,而且多個 Column Family 可以共享同一個 WAL,多個 DB 則無法做到這一點;
  • 34 RocksDB 不支持多列;
  • 35 RocksDB 的讀并不是無鎖的,有如下情況:(1) 訪問 sharded block cache (2) 如果 table cache options.maxopenfiles 不等于 -1 (3) 如果 flush 或者 compaction 剛剛完成,RocksDB 此時會使用全局 mutex lock 以獲取 LSM 樹的最新 metadata (4) RocksDB 使用的內存分配器如 jemalloc 有時也會加鎖,這四種情況總體很少發生,總體可以認為讀是無鎖的;
  • 36 如果想高效的對所有數據進行 iterate,則可以創建 snapshot 然后再遍歷;
  • 37 如果一個 key space range (minkey, maxkey) 很大,則使用 Column Family 對其進行 sharding,如果這個 range 不大則不要單獨使用一個 Column Family;
  • 38 RocksDB 沒有進行 compaction 的時候,可以通過?rocksdb.estimate-live-data-size?可以估算 RocksDB 使用的磁盤空間;
  • 39 Snapshot 僅僅存在于邏輯概念上,其對應的實際物理文件可能正被 compaction 線程執行寫任務;Checkpoint 則是實際物理文件的一個鏡像,或者說是一個硬鏈接,而出處于同樣的 Env 下【都是 RocksDB 數據文件格式】;而 backup 雖然也是物理數據的鏡像,但是與原始數據處于不同的 Env 下【如 backup 可能在 HDFS 上】;
  • 40 推薦使用壓縮算法 LZ4,Snappy 次之,壓縮之后如果為了更好的壓縮效果可以使用 Zlib;
  • 41 即使沒有被標記為刪除的 key,也沒有數據過期,RocksDB 仍然會執行 compaction,以提高讀性能;
  • 42 RocksDB 的 key 和 value 是存在一起的,遍歷一個 key 的時候,RocksDB 已經把其 value 讀入 cache 中;
  • 43 對于一個離線 DB 可以通過?sstdump --showproperties --command=none?命令獲取特定 sst 文件的 index & filter 的 size,對于正在運行的 DB 可以通過讀取 DB 的屬性?kAggregatedTableProperties?或者調用 DB::GetPropertiesOfAllTables() 獲取 DB 的 index & filter 的 size。

參考文檔


  • 1?RocksDB Tuning Guide
  • 2?Rocksdb BlockBasedTable Format
  • 3?PlainTable Format
  • 4?Thread Pool
  • 5?RocksDB Basics
  • 6?Transactions
  • 7?Bloom Filter
  • 8?Universal Compaction
  • 9?Leveled Compaction
  • 10?Time to Live
  • 11?RocksDB-FAQ
  • 12?設計思路和主要知識點
  • 13?RocksDB · MANIFEST文件介紹
  • 14?RocksDB. Merge Operator
  • 15?使用PinnableSlice減少Get時的內存拷貝
  • 16?PinnableSlice; less memcpy with point lookups
  • 17?How to backup RocksDB?
  • 18?RocksDB系列十三:How to persist in memory RocksDB database?
  • 19?Checkpoints
  • 20?LSM-Tree與RocksDB
  • 21?自動調優 RocksDB

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/91315.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/91315.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/91315.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Kotlin -> 普通Lambda vs 掛起Lambda

1. 普通Lambda vs 掛起Lambda的本質區別 1.1 普通Lambda&#xff08;同步執行&#xff09; val lambda: (Int) -> String { it.toString() }// 編譯器生成&#xff1a; class Lambda$1 : Function1<Int, String> {override fun invoke(p1: Int): String {return p1.t…

Apache Ignite 中如何配置和啟用各類監控指標

這段文檔是關于 Apache Ignite 中如何配置和啟用各類監控指標&#xff08;Metrics&#xff09; 的詳細說明。核心思想是&#xff1a;“指標收集有性能開銷&#xff0c;因此默認不開啟所有指標&#xff0c;需要你按需手動開啟。” 下面我們來逐層拆解、通俗易懂地理解這些內容。…

uniapp x swiper/image組件mode=“aspectFit“ 圖片有的閃現后黑屏

部分安卓機針對大寫.JPG 有的豎圖正常&#xff0c;橫圖/正方形不對。解決方案&#xff1a;加border-radius: 1rpx;就行<!-- 圖片預覽彈出框 --><fui-backdrop v-model:visible"imgPreviewVisible" :closable"true" onclick"imgPreviewVisibl…

conda安裝jupter

conda自帶的jupter本來在base里沒有在pytorch環境中 安裝jupter conda install nb_conda 此擴展程序在 Jupyter 文件瀏覽器中添加了一個 Conda 選項卡。選擇 Conda 選項卡將顯示&#xff1a; 當前存在的 Conda 環境列表當前配置的通道中可用的 Conda 包列表&#xff08;htt…

嵌入式操作系統快速入門(1):快速入門操作系統常見基礎概念

快速體會操作系統常見基礎概念 1 初識基本概念 1.1 操作系統 一個軟件程序&#xff1b;用于解決計算機多任務執行時的資源爭搶問題&#xff1b;管理計算機中的各種資源&#xff0c;確保計算機正常完成各種工作&#xff08;任務&#xff09;&#xff0c;解決多任務環境中任務的調…

網絡安全-同形異義字攻擊:眼見并非為實(附案例詳解)

什么是同形異義字攻擊&#xff1f;對人眼而言&#xff0c;一切看起來完全正常。但實際上&#xff0c;例如單詞 Ηоmоgraph 并不完全等同于單詞 Homograph。它們之間的差異非常細微&#xff0c;難以察覺。Ηоmоgraph 實際上包含了幾個非拉丁字母。在本例中&#xff0c;我們將…

windows服務器 maven 配置環境變量,驗證maven環境變量是否配置成功

前置條件&#xff1a;先確認對應版本的jdk已安裝配置好&#xff0c;可使用java -version檢測; 我使用的apache-maven-3.6.3是對應jdk1.8 1.找到系統變量配置窗口 以windows server2019為例&#xff0c;右鍵計算機屬性&#xff0c; 高級系統設置–》環境變量–》系統變量2.新建M…

安裝 docker compose v2版 筆記250731

安裝 docker compose v2版 筆記250731 簡述 v2版是插件形式 確認系統要求, 已安裝 Docker Engine&#xff08;版本 20.10.5 或更高&#xff09; 安裝方式可分為 apt 或 yum 安裝 (能自動升級) apt install docker-compose-pluginyum install docker-compose-plugin 手動二…

PHP 5.5 Action Management with Parameters (English Version)

PHP 5.5 Action Management with Parameters (English Version) Here’s a PHP 5.5 compatible script that uses URL parameters instead of paths for all operations: <?php // Start session for persistent storage session_start();// Initialize the stored actio…

GR-3(4B) 技術報告--2025.7.23--字節跳動 Seed

0. 前言 前兩天字節發布了GR-3&#xff0c;粗略的看了一下&#xff0c;在某些方面超過了SOTA pi0&#xff0c;雖然不開源&#xff0c;但是也可以來看一看。 官方項目頁 1. GR-3模型 1.1 背景 在機器人研究領域&#xff0c;一直以來的目標就是打造能夠幫助人類完成日常任務…

Linux網絡編程:UDP 的echo server

目錄 前言&#xff1a; 一、服務端的實現 1、創建socket套接字 2、綁定地址信息 3、執行啟動程序 二、用戶端的實現 總結&#xff1a; 前言&#xff1a; 大家好啊&#xff0c;前面我們介紹了一些在網絡編程中的一些基本的概念知識。 今天我們就借著上節課提到的&#…

AI+金融,如何跨越大模型和場景鴻溝?

文&#xff5c;白 鴿編&#xff5c;王一粟當AI大模型已開始走向千行百業之時&#xff0c;備受看好的金融行業&#xff0c;卻似乎陷入了落地瓶頸。打開手機銀行想查下貸款額度&#xff0c;對著屏幕說了半天&#xff0c;AI客服卻只回復 “請點擊首頁貸款按鈕”&#xff1b;客戶經…

深度解析:從零構建跨平臺對象樹管理系統(YongYong框架——QT對象樹機制的現代化替代方案)

一、技術背景與核心價值 1.1 QT對象樹的局限性 在Qt框架中&#xff0c;QObject通過對象樹機制實現了革命性的對象管理&#xff1a; #mermaid-svg-SvqKmpFjg76R02oL {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-Sv…

力扣46:全排列

力扣46:全排列題目思路代碼題目 給定一個不含重復數字的數組 nums &#xff0c;返回其 所有可能的全排列 。你可以 按任意順序 返回答案。 思路 看到所有可能首先想到的就是回溯。 回溯的結束條件也很好寫&#xff0c;用數組的長度來判斷即可。這道題的難點主要是如何進行判…

mac環境配置rust

rustup 是一個命令行工具&#xff0c;用于管理 Rust 編譯器和相關工具鏈 sh 體驗AI代碼助手 代碼解讀復制代碼curl --proto ‘https’ --tlsv1.2 -sSf https://sh.rustup.rs | sh使得 Rust 的安裝在當前 shell 環境中生效 如果你使用的是 bash, zsh 或其他類似的 shell&#xf…

腳手架搭建React項目

腳手架搭建項目 1. 認識腳手架工具 1.1. 前端工程的復雜化 1.1.1. 如果只是開發幾個小的demo程序&#xff0c;那么永遠不要考慮一些復雜的問題&#xff1a; 比如目錄結構如何組織劃分&#xff1b;比如如何關鍵文件之間的相互依賴&#xff1b;比如管理第三方模塊的依賴&#xff…

Golang 調試技巧:在 Goland 中查看 Beego 控制器接收的前端字段參數

&#x1f41b; Golang 調試技巧&#xff1a;在 Goland 中查看 Beego 控制器接收的前端字段參數 在使用 Beego 開發 Web 項目時&#xff0c;我們常常會在控制器中通過 c.GetString()、c.GetInt() 等方法獲取前端頁面傳過來的字段值。而在調試過程中&#xff0c;如何在 Goland 中…

sqli-labs:Less-2關卡詳細解析

1. 思路&#x1f680; 本關的SQL語句為&#xff1a; $sql"SELECT * FROM users WHERE id$id LIMIT 0,1";注入類型&#xff1a;數值型提示&#xff1a;參數id無需考慮閉合問題&#xff0c;相對簡單 2. 手工注入步驟&#x1f3af; 我的地址欄是&#xff1a;http://l…

TRAE 軟件使用攻略

摘要TRAE 是一款集成了人工智能技術的開發工具&#xff0c;旨在為開發者提供高效、智能的編程體驗。它包括三個主要組件&#xff1a;TRAE IDE、TRAE SOLO 和 TRAE 插件。無論是編程新手還是經驗豐富的開發者&#xff0c;都可以通過 TRAE 提高工作效率和代碼質量。標題一&#x…

將開發的軟件安裝到手機:環境配置、android studio設置、命令行操作

將開發的軟件安裝到手機環境配置android studio4.1.2安裝命令行操作環境配置 注意&#xff1a;所有的工具的版本都需要根據當下自己的軟件需要的。 Node&#xff1a;14.16.0 &#xff08;如果安裝了npm&#xff0c;可以使用npm進行當前使用node版本的更改&#xff09; &#x…