LSM儲存引擎基本原理

shuxiaow發表於2019-03-29

本文是閱讀了DDIA的第三章後整理的讀書筆記,總結了什麼是LSM儲存引擎,以及在實現上的一些細節優化。

1 什麼是LSM?

LSM一詞最早來自於Partrick O'Neil et al.發表的文章[1],全稱為"Log-Structured Merged-Tree"。後來,Google發表的Bigtable論文[2]將其發揚光大。

目前所有基於該思想實現的儲存引擎,我們都可以稱之為“LSM儲存引擎”,例如:

  • LevelDB。Google開源的單機版Bigtable實現,使用C++編寫。
  • RocksDB。來自Facebook,基於LevelDB改良而來,使用C++編寫。
  • HBase。最接近的BigTable開源實現,使用Java編寫,是Hadoop的重要組成部分。
  • Cassandra。來自Facebook的分散式資料庫系統,同樣借鑑了BigTable設計思想,使用Java編寫。
  • 其它等等。

不同於傳統的基於B+樹的資料庫儲存引擎,基於LSM的引擎尤其適合於寫多讀少的場景。

我們先從一個最簡單的儲存引擎示例出發,然後再描述LSM引擎的基本原理。

2 最基本的儲存引擎

一個最基本的儲存引擎,需要支援下面兩個操作:

  • Put(Key, Value)
  • Get(Key)

加快寫操作

我們知道,磁碟特別是機械硬碟,其隨機寫的速度是非常慘不忍睹的。但是,如果我們是順序寫磁碟的話,那速度跟寫記憶體是相當的:因為減少了尋道時間和旋轉時間。而且順序寫的情況下,還能將資料先放到buffer,等待數量達到磁碟的一頁時,再落盤,能進一步減少磁碟IO次數。

所以,這裡我們規定,每一次寫資料都追加到資料檔案的末尾。

#!/bin/sh
# usage: ./Put key value
echo "$1:$2" >> simpledb.data
複製程式碼

注:如果對同一個key寫多次,最終以最後一次的值為準(即對於讀請求,應返回最後一次寫入的值)。

加快讀操作

要從資料檔案裡面查詢:

# usage: ./Get key
grep "^$1:" simpledb.data | sed -e 's/^://g' | tail -n 1
複製程式碼

直接從資料檔案查詢的效率是很低的:我們需要遍歷整個資料檔案。

這時候就需要“index”來加快讀操作了。我們可以在記憶體中儲存一個key到檔案偏移量的對映關係(雜湊表索引):在查詢時直接根據雜湊表得到偏移量,再去讀檔案即可。

當然,加了索引表也相應地增加了寫操作的複雜度:寫資料時,在追加寫資料檔案的同時,也要更新索引表。

這樣的建索引的方式有個缺點:因為索引map必須常駐記憶體,所以它沒法處理資料量很大的情況。當記憶體無法載入完整索引資料時,就無法工作了。

防止資料檔案無止盡的增長

我們再來看看另外一個問題:傳統的B+樹,每個key只會存一份值,佔用的磁碟空間是跟資料量嚴格對應的。但是在追加寫的方案中,磁碟空間是永無止盡的,只要這個系統線上上執行,產生寫請求,檔案體積就會增加。

解決檔案無限增長的方法就是 compaction

  • 將資料檔案分段(segment)。每個segment檔案最大不超過多少位元組,當segment滿時建立一個新的segment。當前寫入的segment稱為活躍segment。同一時刻只有一個活躍segment。
  • 後臺定期將舊的segment檔案合併壓縮:每個key只保留最新的值。

以上就是一個簡單的基於記憶體索引+檔案分段並定期壓縮的儲存引擎。可以看到,它能夠提供很好的寫入效能,但是無法應對資料量過大的場景。

3 LSM儲存引擎

首先看看怎麼解決記憶體索引過大的問題。

假定現在我們要儲存Nkey-value,那麼我們同樣需要在索引裡面儲存Nkey-offset。但是,如果資料檔案本身是按序存放的,我們就沒必要對每個key建索引了。我們可以將key劃分成若干個block,只索引每個blockstart_key。對於其它key,根據大小關係找到它存在的block,然後在block內部做順序搜尋即可。

在LSM裡面,我們把按序組織的資料檔案稱為SSTableSorted String Table)。只儲存block起始key的offset的索引,我們稱為“稀疏索引”(Sparse Index)。

sstable_sparse_index

而且,有了block的概念之後,我們可以以block為單位將資料進行壓縮,以達到減少磁碟IO吞吐量。

那怎麼生成和維護SSTable呢?

我們可以在記憶體裡面維護一個平衡二叉樹(例如AVL樹或者紅黑樹)。每當有Put(Key, Value)請求時,先將資料寫入二叉樹,保證其順序性。當二叉樹達到既定規模時,我們將其按序寫入到磁碟,轉換成SSTable儲存下來。

在LSM裡面,我們把記憶體裡的二叉樹稱為memtable

注意,這裡的memtable雖然也是存在記憶體中的,但是它跟上面說的稀釋索引不一樣。對每一個SSTable,我們都會為它維護一個稀疏的記憶體索引;但是memtable只是用來生成新的SSTable

再來看看如何compaction

SSTable,我們同樣是通過 segment + compaction 來解決磁碟佔用的問題。

segmentmemtableSSTable的時候就已經做了。

compaction則依賴後臺執行緒定期執行了。但是對於有序的SSTable,我們可以使用歸併排序的思路來合併和壓縮檔案:

sstable_merge

故障恢復

如果在將memtable轉存SSTable時,程式掛掉了,怎麼保證未寫入SSTable的資料不丟失呢?

參考資料庫的redo log,我們也可以搞一個log記錄當前memtable的寫操作。在有Put請求過來時,除了寫入memtable,還將操作追加到log。當memtable成功轉成SSTable之後,它對應的log檔案就可以刪除了。在下次啟動時,如果發現有殘留的log檔案,先通過它恢復上次的memtable

Bloom Filter

對於查詢那些不存在的key,我們需要搜尋完memtable和所有的SSTable,才能確定地說它不存在。

在資料量不大的情況下,這不是個問題。但是當資料量達到一定的量級後,這會對系統效能造成非常嚴重的問題。

我們可以藉助Bloom Filter(布隆過濾器)來快速判斷一個key是否存在。

布隆過濾器的特點是,它可能會把一個不存在的key判定為存在;但是它絕不會把一個存在的key判定為不存在。這是可以接受的,因為對於極少數誤判為存在的key,只是多幾次搜尋而已,只要不會將存在的key誤判為不存在就行。而且它帶來的好處是顯而易見的:可以節省大量的對不存在的key的搜尋時間。

合併策略

上文已經提到,我們需要對SSTables做合併:將多個SSTable檔案合併成一個SSTable檔案,並對同一個key,只保留最新的值。

那這裡討論的合併策略(Compaction Strategy)又是什麼呢?

A compaction strategy is what determines which of the sstables will be compacted, and when.

也就是說,合併策略是指:1)選擇什麼時候做合併;2)哪些SSTable會合併成一個SSTable

目前廣泛應用的策略有兩種:size-tiered策略和leveled策略。

  • HBase採用的是size-tiered策略。
  • LevelDB和RocksDB採用的是leveled策略。
  • Cassandra兩種策略都支援。

這裡簡要介紹下兩種策略的基本原理。後面研究LevelDB原始碼時再詳細描述leveled策略。

size-tiered策略

簡稱STCS(Size-Tiered Compaction Strategy)。其基本原理是,每當某個尺寸的SSTable數量達到既定個數時,合併成一個大的SSTable,如下圖所示:

stcs

它的優點是比較直觀,實現簡單,但是缺點是合併時的空間放大效應(Space Amplification)比較嚴重,具體請參考Scylla’s Compaction Strategies Series: Space Amplification in Size-Tiered Compaction

空間放大效應,比如說資料本身只佔用2GB,但是在合併時需要有額外的8G空間才能完成合並,那空間放大就是4倍。

leveled策略

STCS策略之所以有嚴重的空間放大問題,主要是因為它需要將所有SSTable檔案合併成一個檔案,只有在合併完成後才能刪除小的SSTable檔案。那如果我們可以每次只處理小部分SSTable檔案,就可以大大改善空間放大問題了。

leveled策略,簡稱LCS(Leveled Compaction Strategy),核心思想就是將資料分成互不重疊的一系列固定大小(例如 2 MB)的SSTable檔案,再將其分層(level)管理。對每個Level,我們都有一份清單檔案記錄著當前Level內每個SSTable檔案儲存的key的範圍。

Level和Level的區別在於它所儲存的SSTable檔案的最大數量:Level-L最多隻能儲存 10 LSSTable檔案(但是Level 0是個例外,後面再說)。

lcs

注:上圖中,"run of"就表示一個系列,這些檔案互不重疊,共同組成該level的所有資料。Level 1有10個檔案;Level 2有100個檔案;依此類推。

下面對照著上圖再詳細描述下LCS壓縮策略:

先來看一下當Level >= 1時的合併策略。以Level 1為例,當Level 1SSTable數量超過10個時,我們將多餘的SSTable轉存到Level-2。為了不破壞Level-2本身的互不重疊性,我們需要將Level-2內與這些待轉存的SSTable有重疊的SSTable挑出來,然後將這些SSTable檔案重新合併去重,形成新的一組SSTable檔案。如果這組新的SSTable檔案導致Level-2的總檔案數量超過100個,再將多餘的檔案按照同樣的規則轉存到Level-3

再來看看Level 0Level 0SSTable檔案是直接從memtable轉化來的:你沒法保證這些SSTable互不重疊。所以,我們規定Level 0數量不能超過4個:當達到4個時,我們將這4個檔案一起處理:合併去重,形成一組互不重疊的SSTable檔案,再將其按照上一段描述的策略轉存到Level 1

4 引用

[1] Patrick O’Neil, Edward Cheng, Dieter Gawlick, and Elizabeth O’Neil: “The Log- Structured Merge-Tree (LSM-Tree),” Acta Informatica, volume 33, number 4, pages 351–385, June 1996. doi:10.1007/s002360050048

[2] Fay Chang, Jeffrey Dean, Sanjay Ghemawat, et al.: “Bigtable: A Distributed Storage System for Structured Data,” at 7th USENIX Symposium on Operating System Design and Implementation (OSDI), November 2006.

相關文章