HBase/TiDB都在用的數據結構:LSM Tree,不得了解一下?

LSM Tree(Log-structured merge-tree)普遍應用在HBase,TiDB等諸多數據庫和存儲引擎上,咱們先來看一下它的一些應用:面試

HBase/TiDB都在用的數據結構:LSM Tree,不得了解一下?

這麼牛X的名單,你不想了解下LSM Tree嗎?裝X以前,咱們先來了解一些基本概念。設計數據存儲系統可能須要考慮的一些問題有:ACID,RUM(Read,Write,Memory)。算法

ACID

ACID 相信小夥伴都被面試官問過,我想簡單討論的一點是:如何 持久化數據 才能保證數據寫入的 事務性 和 讀寫性能?sql

事務性可簡單理解爲:1.數據必須持久化。2.一次數據的寫入返回給用戶 寫入成功就必定成功,失敗就必定失敗。數據庫

讀寫性能可簡單理解爲:一次讀 或 一次寫 須要的IO次數,由於訪問速率:CPU>>內存>>SSD/磁盤。數組

對於單機存儲,最簡單的方式固然是:寫一條就持久化一條,讀一條就遍歷一遍全部數據,而後返回。固然沒人這麼幹,在內存中咱們都還知道用個HashMap呢。數據結構

拿Mysql InnoDB舉例子:併發

讀性能體如今數據的索引在磁盤上主要用B+樹來保證。app

寫性能體如今運用 WAL機制 來避免每次寫都去更新B+樹上的全量索引和數據內容,而是經過redo log記錄下來每次寫的增量內容,順序將redo log寫入磁盤。同時在內存中記錄下來本次寫入應該在B+樹上更新的髒頁數據,而後在必定條件下觸發髒頁的刷盤。分佈式

HBase/TiDB都在用的數據結構:LSM Tree,不得了解一下?

redo log是數據增刪改的實時增量數據,順序寫入保證了寫性能和事務性。磁盤上的B+樹是數據增刪改的全量數據,經過定時髒頁刷盤保證這份數據的完整性。ide

這裏的問題是:雖然經過WAL機制,提升了寫入性能,可是仍然避免不了髒頁數據定時刷盤的隨機IO寫入。

由於數據在磁盤上是以block爲單位存儲的(好比4K,16K),假如你更新了Order表一條數據,又更新了Product表一條數據,髒頁刷盤時,這兩條數據在磁盤上的block位置可能差了十萬八千里,就變成了隨機IO的寫入。

HBase/TiDB都在用的數據結構:LSM Tree,不得了解一下?

RUM

RUM(Read,Write,Memory)相似分佈式領域的CAP定理。若是想獲得三者中的二者性能,必須以犧牲第三者的性能爲代價。衡量這三者的性能指標有:讀放大、寫放大、空間放大。

讀放大:是指每次查詢讀取磁盤的次數。若是每次查詢須要查詢5個page (或block),則讀放大是5.

寫放大:是指數據寫入存儲設備的量和數據寫入數據庫的量之比。若是用戶寫入數據庫的大小是10MB,而實際寫入磁盤的大小是30MB,則寫放大是3.另外SSD的寫入次數是有限的,寫放大會減小SSD的壽命。

空間放大:是指數據在存儲設備的總量和數據在數據庫的總量之比。若是用戶往數據庫上存10MB,而數據庫實際在磁盤上用了100MB去存,則空間放大就是10.

一般,數據結構能夠最多優化這其中的二者,如B+樹有更少的讀放大,LSM Tree有更少的寫放大。

下面咱們將介紹LSM Tree的結構,它是如何解決 B+樹中「全量數據的隨機寫入」 問題的;在RUM問題上,又該如何優化LSM Tree。

爲何要有LSM Tree

LSM Tree(Log-structured merge-tree)起源於1996年的一篇論文:The log-structured merge-tree (LSM-tree)。

當時的背景是:爲一張數據增加很快的歷史數據表設計一種存儲結構,使得它可以解決:在內存不足,磁盤隨機IO太慢下的嚴重寫入性能問題。

若是咱們前後修改兩條數據,那麼在髒數據塊落盤時,不是一條條隨機寫入,而是以一個髒塊批量落盤時,就能解決 B+樹中「全量數據的隨機寫入」 問題了。

HBase/TiDB都在用的數據結構:LSM Tree,不得了解一下?

因此大佬設計了這麼一種結構:

HBase/TiDB都在用的數據結構:LSM Tree,不得了解一下?

Ck tree是一個有序的樹狀結構,數據的寫入流轉從C0 tree 內存開始,不斷被合併到磁盤上的更大容量的Ck tree上。這種方式寫入是順序寫的,增刪改操做都是append。這樣一次I/O能夠進行多條數據的寫入,從而充分利用每一次寫I/O。

merge所作的事情有:當上一棵樹容量不足時,1.將上棵樹中要刪除的數據刪除掉,進行GC回收。2.合併數據的最新版本,使得Ck tree不會過分膨脹。

這種初始結構存在的問題有:隨着數據量的增大,merge費勁,沒有好的索引結構,讀放大嚴重。現代的LSM Tree結構以下:

HBase/TiDB都在用的數據結構:LSM Tree,不得了解一下?

MemTable:是LSM Tree在內存中還未刷盤的寫入數據,這裏面的數據能夠修改。是一個有序的樹狀結構,如RocksDB中的跳錶。

SSTable(Sorted String Table):是一個鍵是有序的,存儲字符串形式鍵值對的磁盤文件。當SSTable太大時,能夠將其劃分爲多個block;咱們須要經過 下圖中的Index 記錄下來每一個block的起始位置,那麼就能夠構建每一個SSTable的稀疏索引。這樣在讀SSTable前,經過Index結構就知道要讀取的數據塊磁盤位置了。

HBase/TiDB都在用的數據結構:LSM Tree,不得了解一下?

LSM Tree讀數據

讀數據 包括點讀和範圍讀,咱們分開討論。假設LSM Tree中的key爲[0-99],

點讀:是指準確地取出一條數據,如get(23),則其示意圖爲:

HBase/TiDB都在用的數據結構:LSM Tree,不得了解一下?

按照圖中標示的順序,會先讀取內存,在從Level0依次往高層讀取,直到找到key=23的數據。

範圍讀:是指讀取一個範圍內的數據,如讀取[23-40]內的全部數據( select * from t where key >= 23 && key <=40),

則須要作的是在每一層SSTable找到這個範圍內的第一個block,而後遍歷block塊的數據,直到不符合範圍條件。

HBase/TiDB都在用的數據結構:LSM Tree,不得了解一下?

ReadOnly MemTable:如RocksDB,在MemTable 和 Level0數據之間還有一個內存的ReadOnly MemTable結構。它是LSM Tree在內存中還未刷盤的寫入數據,這裏面的數據不能夠修改。是當MemTable到達必定量須要刷到SSTable時,由MemTable完整copy下來的。這樣可避免從MemTable直接刷到SSTable時的併發競爭問題。

LSM Tree寫數據

在LSM Tree寫入數據時,咱們把ReadOnly MemTable畫上,示意圖以下:

HBase/TiDB都在用的數據結構:LSM Tree,不得了解一下?

當有寫請求時,數據會先寫入MemTable,同時寫入 WAL Log;當MemTable空間不足時,觸發ReadOnly MemTable copy,同時寫入WAL Log;而後刷新到磁盤Level0的SSTable中。當低層SSTable空間不足時,不斷經過Compaction和高層SSTable進行Merge。

LSM Tree合併策略

LSM Tree有兩種合併策略,Tiering 和 Leveling。咱們看圖說話:

HBase/TiDB都在用的數據結構:LSM Tree,不得了解一下?

Tiering的特色是:每層可能有N個重疊的sorted runs,當上一層的sorted runs 要merge下來時,不會和當前層重疊的sorted runs立刻合併,而是等到當前層空間不足時,纔會合併,而後再merge到下一層。

Tiering這種策略能夠減少寫放大,可是以讀放大和空間放大爲代價。

Leveling的特色是:每層只有一個sorted run,當上一層的sorted run 要merge下來時,會立刻和當前層的sorted run合併。Leveling這種策略能夠減少讀放大和空間放大,但以寫放大爲代價。

LSM Tree的優化

上文中的大佬們提到了很多優化方式,我這裏從讀,合併方面簡要總結下LSM Tree的優化。

點讀的優化

儘管咱們能夠經過SSTable的內存block稀疏索引結構簡單判斷key是否可能存在於block中,但如上get(23),若是level1讀不到,仍須要往下讀level2,level3。(由於數據是按照增刪改的時間順序一層層往下落盤的,若是一個key不存在低level中,可能存在於更早的高level中)。這樣的點讀IO次數較多,讀放大嚴重。

布隆過濾器

對於精確查詢,hash的時間複雜度爲 O(1),那麼能夠經過布隆過濾器來優化。咱們只須要查詢時先過一遍及隆過濾器,就能排除掉必定不存在的key了,避免沒必要要的IO查詢。

「布隆過濾器:是經過hash算法來判斷一個key是否存在於某個集合中,布隆過濾器一般是一個bit數組,用針對一個key屢次hash算法肯定的多個bit值來表示key是否存在。多個bit值全爲1,則表示key可能存在,不然不存在。好處是多個bit值一般比直接存儲一個存在的key佔用空間小,省內存。

分層布隆過濾器

上述布隆過濾器是假設每層數據都使用相同的布隆過濾器來進行過濾,而數據隨着層數的增長一般是指數級增加的,若是使低層的數據使用更精確的布隆過濾器(所需bit數更多,可是精確度更高),高層的數據使用稍微不那麼精確的布隆過濾器,則總體點查的效率將提升。

上文中Monkey作了上述優化,即便隨着數據量和層數的增大,點查仍能保持在一個穩定的時間內。

HBase/TiDB都在用的數據結構:LSM Tree,不得了解一下?

範圍讀的優化

Hash的不足就是沒法支持範圍查詢索引,優化方式有:

並行查詢

由於讀數據不存在競爭問題,能夠並行讀每層的數據,縮短總體查詢時間,可是這種方式實際並無縮短IO次數。

前綴布隆過濾器

前綴布隆過濾器是以key的前綴來Hash構建布隆過濾器,而不是key自己的值。這樣能夠起到過濾 like Monica* 這樣的查詢條件做用。RocksDB有作此優化。

合併的優化

上面提到了兩種合併策略Tiering 和 Leveling。Tiering能夠減少寫放大,Leveling能夠減少讀放大和空間放大。上文中提到了數據庫所採用的合併策略走勢以下:

HBase/TiDB都在用的數據結構:LSM Tree,不得了解一下?

隨着數據量的增大,整條曲線會上移。優化的重點在於:如何結合兩種合併策略,使曲線總體下移。

Lazy Leveling

上文中Dostoevsky採用了一種Lazy Leveling的策略:它是對低層數據採用Tiering合併策略,對高層數據採用Leveling合併策略。從而權衡了讀寫性能。

HBase/TiDB都在用的數據結構:LSM Tree,不得了解一下?

RUM的取捨

RUM的取捨和權衡相似於分佈式領域的CAP,在特定的場景採用適合的優化手段才能使總體性能最優。上文中的大佬還提到了Fluid Merge策略。233醬表示雖過了眼癮,但還是個CRUD渣渣。關於RUM的優化,233我最後放一張圖,但願對你我有所啓發。

HBase/TiDB都在用的數據結構:LSM Tree,不得了解一下?

相關文章
相關標籤/搜索