LSM存儲引擎基本原理

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

1 什麼是LSM?

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

目前全部基於該思想實現的存儲引擎,咱們均可以稱之爲「LSM存儲引擎」,例如:分佈式

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

不一樣於傳統的基於B+樹的數據庫存儲引擎,基於LSM的引擎尤爲適合於寫多讀少的場景。oop

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

2 最基本的存儲引擎

一個最基本的存儲引擎,須要支持下面兩個操做:優化

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

加快寫操做

咱們知道,磁盤特別是機械硬盤,其隨機寫的速度是很是慘不忍睹的。可是,若是咱們是順序寫磁盤的話,那速度跟寫內存是至關的:由於減小了尋道時間和旋轉時間。並且順序寫的狀況下,還能將數據先放到buffer,等待數量達到磁盤的一頁時,再落盤,能進一步減小磁盤IO次數。ui

因此,這裏咱們規定,每一次寫數據都追加到數據文件的末尾。spa

#!/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.

相關文章
相關標籤/搜索