漫談RocksDB(三)基本操作——動如脫兔,靜若處子

語言: CN / TW / HK

本文已參與「新人創作禮」活動,一起開啟掘金創作之路。

前言

上一篇文章已經講述了RocksDB的基礎概念,本文趁熱打鐵,將上文留下的作業以及RocksDB的基本操作集中說明一下,既幫助大家鞏固下上文的概念,又為後文的高階操作以及特性打下基礎。

總體來說,RocksDB的基礎操作和LSM tree的基礎操作基本相同,但是RocksDB在標準的LSM tree方案的基礎上進行了定製化的優化,以支援自身的各種功能,下面就一起來看一下RocksDB的各種基本操作。

正文

寫流程:

rocksdb寫入時,直接以append方式寫到WAL檔案以及memtable,隨即返回,因此非常快速。memtable達到一定閾值後切換為Immutable Memtable,只能讀不能寫。

Immutable memtable觸發閾值後,後臺Flush執行緒負責按照時間順序將Immutable Memtable刷盤 生成Level 0 SST,Level 0 SST觸發閾值後,經合併操作(compaction)生成level 1 SST,level 1 SST 合併操作生成level 2 SST,以此類推,生成level n SST。流程如下:

讀流程:

按照 memtable --> Level 0 SST–> Level 1 SST --> … -> Level n SST的順序讀取資料。這和記錄的新舊順序是一的。因此只要在當前級別找到記錄,就可以返回。流程如下:

Flush流程

上一章節講述了Flush的定義,下面來講一下Flush的具體流程:

簡單來說在RocksDB中,每一個ColumnFamily都有自己的Memtable,當Memtable超過固定大小之後(或者WAL檔案超過限制),它將會被設定為Immutable Memtable,然後會有後臺的執行緒啟動來重新整理這個Immutable Memtable到磁碟(SST)。

在下面這幾種條件下RocksDB會flush Memtable到磁碟:

  • 當某一個Memtable的大小超過write_buffer_size
  • 當所有的Memtable的大小超過db_write_buffer_size
  • 當WAL檔案的大小超過max_total_wal_size,我們需要清理WAL,因此此時我們需要將此WAL對應的資料都重新整理到磁碟,也是重新整理Memtable

Compaction操作

上一章節講述了Compaction操作的定義,下面來講一下Compaction操作的具體作用:

  • 資料合併遷移:首先當上層level的sst檔案數量或者大小達到閾值時,就會觸發指定的sst檔案的合併並遷移到下一層。每一層都會檢查是否需要進行compaction,直到資料遷移到最下層
  • 資料真刪:LSM樹的特點決定資料的修改和刪除都不是即時的真刪,而是寫入一條新的資料進行衝抵。這些資料只會在compaction時才會真正刪除(HBase是在major compaction中才會刪除,minor compaction不會進行真刪),所以compaction能釋放空間,減少LSM樹的空間放大,但是代價就是寫放大,需要根據業務場景要求通過引數來調整兩者之間的關係,平衡業務與效能。
  • 冷熱資料管理:RocksDB的資料流向是從Memtable到level 0 最後到 level N,所以資料查詢時也是通過這個路徑進行的。由於業務上查詢大多是熱資料的查詢,所以資料的冷熱管理就顯得很必要,能很大程度上提高查詢效率;而且由於各個level的sst檔案可以使用不同的壓縮演算法,所以可以根據自己的業務需求進行壓縮演算法的配置,達到最佳效能。

LSM tree相關的經典壓縮演算法有下面幾種:經典Leveled,Tiered,Tiered+Leveled,Leveled-N,FIFO。除了這些,RocksDB還額外實現了Tiered+Leveled和termed Level,Tiered termed Universal,FIFO。這些演算法各有各的特點,適用於不同的場景,不同的 compaction 演算法,可以在空間放大、讀放大和寫放大之間這三個LSM tree的"副作用"中進行取捨,以適應特定的業務場景。大家可以按需選擇,如果沒有特殊需求,開箱即用即可。至於每一種壓縮演算法的具體方式以及實現將會在高階特性篇進行講解。

Compression操作

上一章節講述了Compression操作的定義,下面來講一下Compression的具體操作:

使用options.compression來指定使用的壓縮方法。預設是Snappy。但是大多數情況下LZ4總是比Snappy好。之所以把Snappy作為預設的壓縮方法,是為了與舊版本保持相容。LZ4/Snappy是輕量壓縮,所以在CPU使用率和儲存空間之間能取得一個較好的平衡。

如果你想要進一步減少儲存的使用並且你有一些空閒的CPU,你可以嘗試設定options.bottommost_compression來使用一個更加重量級的壓縮。最底層會使用這個方式進行壓縮。通常最底層會儲存大部分的資料,所以使用者通常會選擇偏向空間的設定,而不是花費cpu在各個層壓縮所有資料。我們推薦使用ZSTD。如果沒有,Zlib是第二選擇。

如果你有大量空閒CPU並且希望同時減少空間和寫放大,把options.compression設定為重量級的壓縮方法,所有層級都生效。推薦使用ZSTD,如果沒有就用Zlib

當然也可以通過一個已經過期的遺留選項options.compression_per_level,你可以有更好的控制每一層的壓縮方式。當這個選項被使用的時候,options.compression不會再生效,但是正常情況下這個是不必要的,除非你有極特殊的需求或者效能優化要求。

最後可以通過把BlockBasedTableOptions.enable_index_compression設定為false來關閉索引的壓縮。

Ingest操作

上一章節講述了Ingest操作的定義,下面來講一下Ingest的具體操作。Ingest實現上主要是兩部分:建立SST檔案和匯入SST檔案:

建立SST檔案

建立SST檔案的過程比較簡單,建立了一個SstFileWriter物件之後,就可以開啟一個檔案,插入對應的資料,然後關閉檔案即可。 寫檔案的過程比較簡單,但是要注意下面三點:

  • 傳給SstFileWriter的Options會被用於指定表型別,壓縮選項等,用於建立sst檔案。
  • 傳入SstFileWriter的Comparator必須與之後匯入這個SST檔案的DB的Comparator絕對一致。
  • 行必須嚴格按照增序插入

匯入SST檔案

匯入SST檔案也非常簡單,需要做的只是呼叫IngestExternalFile()然後把檔案地址封裝成vector以及相關的options傳入引數即可。下面是IngestExternalFile()的實現邏輯:

  • 把檔案拷貝,或者連結到DB的目錄。
  • 阻塞DB的寫入(而不是跳過),因為我們必須保證db狀態的一致性,所以我們必須確保,我們能給即將匯入的檔案裡的所有key都安全地分配正確的序列號。
  • 如果檔案的key覆蓋了memtable的鍵範圍,把memtable進行flush操作。
  • 把檔案安排到LSM樹的最合適的層級。
  • 給檔案賦值一個全域性序列號。
  • 重新啟動DB的寫操作。

為了減少compaction的壓力,我們總是想將目標的sst檔案寫入最合適的層級,即合適寫入的最低層級,下面三個條件作為約束可以選出合適的層級:

  • 檔案可以安排在這個層
  • 這個檔案的key的範圍不會覆蓋上面任何一層的資料
  • 這個檔案的key的範圍不會覆蓋當前層正在進行壓縮

另外,5.5以後的新版本的RocksDB的IngestExternalFile載入一個外部SST檔案列表的時候,支援下層匯入,意思是如果ingest_behind為true,那麼重複的key會被跳過。在這種模式下,我們總是匯入到最底層。檔案中重複的key會被跳過,而不是覆蓋已經存在的key。

總結

上面的基礎操作中可以看出來,RocksDB的各種操作是圍繞著LSM tree的特性展開的,各種優化點也是為了平衡LSM tree的讀放大,寫放大以及空間放大三個副作用而設計的。

現在看來這三個放大更像資料庫的CAP,不能同時解決,只能根據業務場景來解決主要矛盾,儘量減少次要矛盾,至於具體的方法,會在後續的高階特性中詳細講述。