歡迎光臨
每天分享高質量文章

LSM-Tree 與 RocksDB

(點選上方公眾號,可快速關註)


來源:MRRiddler ,

blog.mrriddler.com/2017/05/30/LSM-Tree%E4%B8%8ERocksDB/

冥冥之中,接觸到了不同於關係資料庫的NoSQL Key-Value儲存引擎RocksDB,懵懵懂懂、充滿好奇,google一點,滿眼皆是LSM-Tree,頭暈眼花、若即若離,便有了這篇文章,一起與大家分享這趟探險之旅。

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的樣式。

LSM近年來已被廣泛使用起來,還有將B家族樹和LSM結合起來使用的,像HBase、SQLite4、MongoDB、Cassandra、LevelDB,還有接下來的主角RocksDB,這些當家資料儲存花旦,都或多或少支援、使用起LSM了。

RocksDB

RocksDB是Facebook在LevelDB基礎上用C++寫的Key-Value儲存引擎。其Key和Value都是二進位制流。並對快閃記憶體(flash)有更友好的最佳化。先來聊一聊RocksDB的整體結構,然後再聊一聊RocksDB中的一些有意思的抽象,最後聊一聊RocksDB記憶體上、磁碟上的具體結構。在RocksDB中,將記憶體結構中的資料寫入磁碟結構叫做flush,而不同file、level之間merge叫做compaction。

Architecture

RocksDB如上文所說是基於LSM做儲存。RocksDB在記憶體中的結構叫做memtable,用於形成Log-Structured的file叫做logfile,磁碟上的file結構叫做sstfile,用於記錄對file更改的log叫做manifest。

Column-Family

為儲存的資料邏輯分族,將不同family互相隔離,分開配置、儲存。column family共享logfile,而不共享memtable和sstfile,這使得column family有以下兩點特點:

  • 多個column family仍然能保持事務的原子性。

  • 單獨增加、修改、刪除一個column family內的資料效能提升。

Filter

RocksDB有一些奇思妙想的filter,這些filter根據特定條件生成,透過資料的key就可以判斷資料是否確定或可能被特定條件排除掉。有些filter可以用來對讀最佳化,有些也可以用來管理資料的生命週期等。

Bloom-Filter

bloom filter就是一個能提高讀效能的filter。透過演演算法可以生成一個key set對應的bloom filter。這個bloom filter可以判斷出任意key可能存在或者肯定不存在key set中。每個sstfile在生成的時候,都會建立一個或多個對應的bloom filter,當在讀資料的時候,可以快速判斷出資料是否可能在sstfile中。在做range scan和prefix查詢的時候,bloom filter也能幫上忙。

Time-To-Live(TTL)

RocksDB還支援為存入的資料設定上最長生命週期。RocksDB會為有TTL的資料建立一個filter。這種filter叫做compaction filter,當進行compaction的時候,生命週期結束的資料將會被排除。很明顯,這個TTL不是絕對時間,RocksDB會在絕對時間過後的一段時間內刪除資料。

Memtable

RocksDB在記憶體上的結構由memtable組成,具體預設使用SkipList,當然,外部也可以指定其他資料結構。不過,SkipList做為用多個線性結構模仿出層級結構的“樹狀“資料結構,能提供常數級順序訪問和對數級隨機訪問,並且在併發環境相對於其他樹狀資料結構,減輕了鎖開銷,運用在這裡再合適不過了。

Flush

為了減小鎖開銷,將寫入memtable和flush分離,RocksDB會使用多個memtable,並且把flush這件事抽象為task-pipeline,也就是job、job queue、worker抽象。將要flush的memtable先轉換成immutable memtable,代表其不可寫了,並將immutable memtable加入flush pipeline,等待後臺執行緒去flush。而同時新的memtable生成取代immutable memtable等待資料寫入。在將immutable memtable flush的過程中,值得一提的是會做inline-compaction,這是將immutbale memtable提前進行資料合併的最佳化,如果在這個memtable建立到flush的週期中,資料被加入然後刪除,在這一步就可以compact掉,這對短生命週期的資料有很大的寫最佳化。

SSTFile(SSTTable)

RocksDB在磁碟上的file結構sstfile由block作為基本單位組成,一個sstfile結構如下:

[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]                              

其中data block就是資料物體block,meta block為元資料block。metaindex block和index block分別是對meta block和data block的索引block。metaindex block和index block都以blockhandle結構作為指向所索引block的指標。blockhandle包含所索引block的offset和size。metaindex block將所索引的meta block的名字作為key。而index block將所索引的data block前一block的最大key值與所索引data block的最小key值的中間任意值作為key。

filter block記錄的就是針對此sstfile生效的一系列filter。stats block也就是properties block,它以key-value形式記錄了此sstfile的一些屬性。sstfile組成的block有可能被壓縮(compression),不同level也可能使用不同的compression方式。compression dictionary block記錄了適用於compression此sstfile block的庫。footer添加了一些位元組填充,和記錄了指向metaindex block、index block的blockhandle結構指標。這說明sstfile如果要遍歷block,會逆序遍歷,從footer開始。

Compaction

RocksDB會在後臺多執行緒compact。RocksDB有兩種compact style:

  • Universal Style Compaction:這種style去compact就如上文LSM所聊的,將磁碟上資料完全按照寫入時間線去組織,只將相鄰時間段內寫入磁碟上的資料compact,很像在合併互不重疊(overlapping)的時間段。這種style可以compact出新的file或者level。很明顯,不同時間段內的資料會有重疊的key,會有冗餘空間。

  • Level Style Compaction:這種style去compact不會將磁碟上資料完全按照寫入時間線去組織。而為了防止同一level多個file中相同key資料的冗餘,這種style要保證同一level不存在有相同key的資料。若將Ln level中的file1 要compact到Ln+1level,要將Ln+1level中所有包含重疊file1資料key的檔案一起compact,最後將這些file compact成Ln+1level中的一個新file。這種style還會造成一種效果,就是同一level的file按key值排序。雖然Level Style Compaction減少了冗餘空間,但是,對於讀資料來說,Universal Style Compaction更加遵守區域性性,所以Level Style Compaction讀效能更差一點。

也就說,Universal Style Compaction重讀輕空間,Level Style Compaction重空間輕讀。RocksDB將後者作為default style,關於這兩種style如何選擇(pick)level、file去compact,這裡就不整體敘述了,基本是file、level的數量或大小達到一個ratio的threshold就會triger compact,關於Universal Style的選擇在這裡,關於Level Style的選擇在這裡。挑個有趣的聊下,Level Style如何pick file去compact?RocksDB還不能提供一個universal策略去pick,不過以下幾個option可以選擇:

  • 優先選擇key分佈範圍集中的file去compact:file中資料key如果分佈集中,就會有更少的重疊file在下一level,就會有更少的compact開銷。用kOldestSmallestSeqFirst設定優先compactkey分佈範圍集中的file。

  • 優先選擇冷資料file去compact:經常修改的熱資料如果經常compact,只會浪費開銷。設想下,剛剛插入的一組資料,馬上又刪除,如果插入後就進行compact,本應該刪除的資料又佔了兩倍的冗餘空間。用kOldestLargestSeqFirst設定優先compact冷資料file。

  • 優先選擇被刪除資料多的file去compact:被刪除的資料如果早被compact的話,可以減少冗餘空間,用kByCompensatedSize設定優先compact被刪除資料多的file。

SSTFile-Indexing

如果RocksDB使用的是Level Style Compaction,那麼還可以在查詢資料時做更多最佳化。Level Style Compaction保證同一level不存在有相同key的資料,且file按key值排序。首先,對於有序結構搜尋,可以使用二分查詢。其次,如果在某一level中都查詢不到key,那麼在下一level中查詢的時候,可以用查詢到的最後一個file的key範圍排除下一level的很多file,比如如下結構:

                                         file 1                                file 2

                                      +———-+                          +———-+

level 1:                              | 100, 200 |                          | 300, 400 |

                                      +———-+                          +———-+

           file 1     file 2      file 3      file 4       file 5       file 6       file 7      

         +——–+ +——–+ +———+ +———-+ +———-+ +———-+ +———-+

level 2: | 40, 50 | | 60, 70 | | 95, 110 | | 150, 160 | | 210, 230 | | 290, 300 | | 310, 320 | 

         +——–+ +——–+ +———+ +———-+ +———-+ +———-+ +———-+

如果要查詢80,在level 1中file1沒有查詢到,那麼在level2中可以排除file3以後的file。這種排除和level結構有點像區間樹。基於此思想,在level compact的時候,可以為file新增一系列指向下一level file的指標,加快查詢速度。

Manifest

因為檔案系統的操作不能保證原子性,所以RocksDB對file的更改也會記錄log,就記錄在manifest裡。實際上,可以將manifest看做,記錄的是RocksDB的狀態。manifest記錄的內容就成了RocksDB按時間排序的一系列狀態,如果,將每個狀態看做RocksDB某個時間點的快照,manifest就是RocksDB的動圖GIF。

Cache

沒有cache的儲存引擎是不完整的,RocksDB有兩種cache,block cache和table cache。先來聊聊block cache。block cache快取的單位就是sstfile的data block,這種cache有兩種演演算法,經典的LRU和clock,任由你選擇。除了data block,用於索引和提高讀效能的index block和filter block更是重點快取物件,但RocksDB並不會把這倆放在block cache中,RocksDB會單獨照顧好這倆,基本不用外部操心。而table cache快取的是sstfile的file descriptor,實際上,作業系統透過file descriptor用取用計數的方式來管理file結構體和其背後的資源。也就是說,table cache快取的是作業系統用來操作sstfile的file結構體和其背後資源,更多關於file descriptor結構,推薦這篇文章

http://cuckootan.me/2015/04/18/Linux/行程開啟及讀寫檔案的過程/

Tips

以下是使用RocksDB的一些建議:

  • RocksDB執行緒模型是單控制代碼對多執行緒。

  • Get、Put、Delete等操作不需要操心資料同步問題,而Iterator和WriteBatch需要同步控制。

  • WriteBatch類似於事務,提供將一個或多個資料庫操作抽象為單位的能力,此單位是原子性的,但並不會互相隔離從而併發控制,也不會檢測出異常並恢復。

本篇聊了很多RocksDB的設計思想。然而畢竟大多數工程師都是面嚮應用,不會去搞個儲存引擎。這裡就總結幾條從LSM、RocksDB等儲存引擎中學到的有關讀寫IO的技巧,理解了大有裨益。

  • 相對於計算、記憶體讀取,磁碟IO慢不知多少數量級。

  • 相對於執行一次磁碟IO,磁碟IO的資料量多少不重要,也就說開始、結束一次磁碟IO更費時。儘量集中對磁碟進行IO。

  • 相對於磁碟隨機讀寫,磁碟順序讀寫快上3個數量級。

  • 讀寫分離,減少併發鎖的效能開銷。

取用

  • LSM

    https://www.zhihu.com/question/19887265

  • LSM論文

    http://www.cs.umb.edu/~poneil/lsmtree.pdf

  • RocksDB Blog

    http://rocksdb.org/blog/

  • RocksDB Wiki

    https://github.com/facebook/rocksdb/wiki

看完本文有收穫?請轉發分享給更多人

關註「ImportNew」,看技術乾貨

贊(0)

分享創造快樂