概述

本文主要描述基本的數據結構:LSM-Trees和B-tree,其中LSM-Trees構成了leveldb、rocksdb等的基礎,B-tree是大多數關係型資料庫的基礎。我們先來看一個最簡單的資料庫的雛形:

write() {

echo "$1,$2" >>file}

read(){

grep "^$1," file |sed -e "s/^$1,//" | tail -n 1}

這個資料庫的write功能是很簡單但是很強大,因為它只需要append-only。與write類似,好多資料庫內部使用append-only的log,大型資料庫可能需要解決並發控制、磁碟載入和容錯處理。

這個資料庫的read性能很差,每次查詢都需要O(n)的時間。加索引可以解決這個問題。在本文我們會分析幾種常見的索引,基本思想都是保存額外的metadata來輔助查詢。如果你想通過多種方式來查詢,你可能需要做不同的索引。

索引一般來源於資料庫的primary數據,這個不會影響資料庫的內容,但是會帶來額外的開銷,尤其是對write操作,因為每次write都需要更新索引。所以索引是一個讀寫的折中:索引可以增加度的性能,但是會減少寫的性能。這一般交給使用者來決定怎麼加索引。

哈希索引

最簡單的索引結構是key-value結構,類似於大多數語言里的HashMap結構。這種結構將數據的key和offset保存在內存里,真是數據可以存儲在disk中,從而突破內存大小的限制。Bitcask就使用了這一結構,一次讀根據索引只會有一次磁碟查詢。如果數據剛好在緩存里,一次讀根本不需要訪問磁碟。Bitcask適合key總數小但是更新頻繁的情況,否則內存無法承載所有索引。

append-only結構存在超過磁碟大小的風險。一個解決方法是將log拆分成小的segment,當segment大小超過設置時再起一個新的segment來寫。然後,我們可以對這些segment做compaction,旨在去除重複的key,保持每隔key的最新值。如下所示:

segment 1

a:1b:2c:3d:delete

segment 2

a:2b:1c:4d:5merged segment 1 and segment 2a:1b:2c:3

compaction可以使segment更小,我們可以同時合併多個compaction。另外,segment是不可更改的文件,compaction不會影響正常的讀寫操作。當compaction在後台線程進行,使用舊的segment進行讀。一旦合併操作完成,我們將讀操作切換到新的segment,這個時候舊的segnent可以被刪除了。

這個時候,每個segment在內存中都有自己的hash table結構了。對於一個查詢,我們先從最新的segment的hash table結構查詢,如果沒有再查詢次新的hash table。由於合併過程使得segment個數變少,所以我們並不需要查詢過多的文件。但是在實際生產中,還有許多細節要考慮:

  • 文件格式。csv並不是最好的格式,二進位格式會更快和更簡單。二進位格式會首先編碼value的大小,然後跟一個原始的字元串。
  • 刪除數據。由於採用了append-only的方式,刪除數據需要特殊標記,在compaction的時候再真正刪除。
  • 宕機恢復。如果資料庫重啟,內存中的哈希索引需要丟失重建,如果從原始數據重建,由於數據量大會很慢,從而導致回復很慢。所以,需要定期的對索引做快照到disk。
  • 部分寫操作。如果數據寫了一半資料庫宕機了,需要通過checksums來檢查數據的完整性,對不完整的數據進行丟棄。
  • 並發控制。由於寫操作具有嚴格的時序性,所以常見的實現方式是單線程來完成。由於segment不可更改,所以可以多線程共同讀。

為什麼使用append-only而不是原地更新呢?原因如下:

  • appending和compaction是順序操作,遠比隨機寫快得多,尤其是在磁性旋轉盤硬碟驅動器上,在flash-based solid state drivers(SSDs)也是如此。
  • 並發控制和宕機恢復會很簡單。比如,不使用append-only,宕機時你正在寫,會導致寫一半的狀況,使得數據部分新部分舊。
  • 合併舊的segment避免了文件碎片。

但是,此時的hash table也會有一些限制:

  • hash table必須呆在內存里。當key數量過多時,無法全部保存在內存里。原則上,你可以將hash table保存在disk上,但是實際操作上比較困難,這會需要很多隨機IO,當磁碟滿了時很難擴充,hash碰撞也會比難處理。
  • scan會很困難。比如掃描000-999之間的數據,實際上需要查看所有的hash table。

下面我們會介紹不受這些約束的索引結構。

SSTables和LSM-Trees

在基於log結構的segment中,數據是一系列的key-value結構,這些數據對沒有順序可言。現在,我們要求這些數據對有序,這樣的數據格式被稱為SSTable(Sorted String Table)。另外,每個key在segment中要求只出現一次,compaction可以確保做到。SStable有很多優勢,如下:

  • 合併segment簡單且高效,即使是在文件大於內存的情況下。可以採用一種類似mergesort的演算法(同時開始讀取輸入文件的第一個key,將最小的key拷貝到輸出文件,repeat),從而產生一個新的segment。如果key出現在多個文件中,將使用最新的值。
  • 對於讀操作,不需要在內存中記錄所有的key,只需要記錄一些粗力度的索引即可。比如,在一個segment中要查找一個key,可以先定位到具體的block(block是對segment進一步的劃分),然後遍歷即可。(如果所有的key-value大小一樣,可以進行二分查找,但是實際情況每條數據大小並不相同)
  • Block可以進行壓縮,減少磁碟大小和磁碟IO。

構建SSTable

對於有序segment,依靠append-only是無法構建的,但是可以首先在內存中構建(採用紅黑樹、AVL樹)。SSTable的構建過程如下:

  • 當來了一個write操作,將數據插入到內存的平衡樹結構中(memtable)。
  • 當memtable大小超過設置(一般MB),將memtable寫出到磁碟的SSTable。這可以很高效地做到,因為memtable是有序的。這個SSTable會成為最新的SSTable。在寫memtable到SSTable的同時,可以令起一個實例來寫memtable。
  • 當來了一個read操作,先在memtable中查詢,再按照新舊順序在SSTable中查詢。
  • 後台有一個合併的compaction在合併文件。

為了防止宕機丟memtable的數據,每個寫操作需要append到一個disk上的log文件,用於宕機恢復。當memtable寫到SSTable後,就可以將對應的log刪除了。

LSM-tree的由來

構建SSTable的演算法被使用在levelDB和RocksDB中,一種可嵌套key-value存儲引擎。levelDB可以在Riak中替代Bitcask,類似的存儲引擎被應用到Cassandra和HBase中,二者都是受谷歌BigTable論文的啟發。

最開始,這種索引結構出現在Partrick ONeil et al.的Log-Structured Merge-Tree(LSM-Tree)中,所以後續的基於這種合併、compaction的有序文件都被稱作LSM存儲引擎。

Lucene是Elasticsearch和Solr使用的全文本搜索技術,雖然全文本索引比較複雜,但是思想類似,它會保存一個key-list的結構,其中list中是所有包含key的文檔ID。

性能優化

實際生產中有很多細節需要考慮以提升性能。例如,LSM-tree的演算法在查找不存在的key時會很慢,因為需要遍歷所有文件。針對這種問題,存儲引擎使用了Bloom filters來優化,這是一種逼近所有內容的內存結構,它可以告訴你key是否存在。

針對SSTable的compaction和合併,也有許多策略,最常見的是按照size-tiered和level。levelDB和RocksDB使用level的compaction,HBase使用size-tiered的,Cassandra都支持。在size-tiered中,新的和小的SSTable被合併成老的和大的SSTable。在level中,key 範圍被分成更小的SSTable,老的數據被移到單獨的level,從而允許增量compaction和減少磁碟空間。

B-Trees

前面討論了基於log結構的索引,它們被廣泛地接受,但是B-Tree還是當前使用最廣泛的索引系統。自1970年被引入以來,它被廣泛的應用在各種關係型和非關係型資料庫中。LSM-Tree將資料庫分成了更小的segment,採用順序寫的方式。B-Tree則相反,它將資料庫切分成固定大小的blocks(或者叫page),一般是4KB大小(可能更大)。這種設計更貼合硬體系統,因為磁碟也是被組織成固定大小的block。

B-Tree中每一頁都有一個地址,允許其他頁引用,類似於指針,但是存儲在磁碟上。我們可以使用這些頁引用構建一個樹形頁,如下圖所示:

如果要查詢一個key,從根節點開始。頁里包含了key和到子頁的引用,每個引用負責的內容由它前後的key來指定。比如,我們要查找key=40的內容,先在根頁中定位到30-60的索引,進入子頁繼續查詢,就能查到key=40的內容。

如果要更新一個已經存在的key,先定位到包含key的頁,改變key的value,再將頁寫回到磁碟(任何引用都不會失效)。

如果要插入一條新數據,需要首先找到能夠包含新數據的頁,並把新數據插入。如果沒有足夠的空間來插入,這個頁會切分成兩個頁,同時更新父頁。如下所示,插入F:

這種演算法確保了樹的平衡性:n個key的樹會有一個O(n)的深度。大部分資料庫會有三到四層深度,比如一個4層樹,每頁大小4KB,每頁引用為500,可以存儲256TB的數據。

使B-Tree可信賴

B-Tree的寫需要覆蓋disk的舊頁,地址不會變,其他頁對它的引用也不會變。可以認為overwriting是一個硬體操作,在一般的磁碟中,先將磁碟頭移到正確的位置,等待旋轉盤上的正確位置出現,然後overwriting合適的sector。至於SSDs,會更複雜一些,因為SSDs必須擦除和重寫存儲晶元的一大塊。LSM-trees則沒有這個問題。

有些操作可能需要同時更新多個頁,比如上面提高的的插入分裂操作,這種操作必須確保原子性,否則宕機恢復後會出現孤立頁的情況。B-Tree採用了一種額外的數據結構來保證——write-ahead log(WAL,或者叫做redo log),這是一種append-only的文件,需要在具體操作前寫入。當出現宕機恢復時,這個log可以確保將B-Tree恢復到一致性的狀態。

並發訪問也會造成問題。B-Tree採用了一個叫做latches的輕量鎖來保證並發訪問時的一致性。

B-Tree優化

B-Tree歷史悠久,有很多優化,如下所示:

  • 一些資料庫使用copy-on-write技術來取代WAL。一個更改頁被寫到另外的地方,同時在父頁中創建一個針對此頁的新版本。這種方法也可以用於並發控制。
  • 我們可以不存儲完整的key,可以只存儲key的縮寫,只要能區分查找邊界即可。
  • 一般來說,頁可以存儲在磁碟上的任意空間。對於跨頁的scan,這種存儲方式很低效。一些資料庫嘗試將相鄰頁存在一起。但是實際上做起來比較困難,隨著樹的增長。LSM-trees則沒有這個問題。
  • 額外的數據結構允許子頁指向父頁,可以方便的過渡到相鄰頁。
  • B-Tree中的fractal tree借鑒了日誌結構的思想來減少磁碟seek。

B-Tree與LSM-Tree的對比

儘管B-Tree實現起來更自然,但是LSM-Tree也有一定的性能優勢。總的來說,LSM-Tree寫很快,B-Tree讀很快。

LSM-Tree的優勢

B-Tree每寫一次數據需要額外再寫一次WAL,每次更改需要重寫整個頁。當宕機恢復時,有可能還需要重寫一次。LSM-Tree的compaction也會寫多次數據。對資料庫的一次寫導致的多次寫磁碟,被稱作write amplification。在SSDs中,write amplification更需要關注,因為SSDs的block寫是有壽命次數的。在寫頻繁的操作中,過大的write amplification會影響寫性能。LSM-Tree相比B-Tree,能承載更高的寫入量,一方面是因為具備更低的write amplification,另外一方面是因為順序寫更快,這種效果在非SSDs的普通磁碟上更加明顯。

LSM-Tree可以更好地被壓縮,從而節省磁碟IO和磁碟容量。B-Tree在頁變更時可能會導致磁碟碎片,導致有些空間不可利用。

SSDs的某些固件採用了日誌結構的演算法來講隨機寫轉變成順序寫,更低的write amplification和更少的磁碟碎片顯然對SSDs更有利。

LSM-Tree的不足

LSM-Tree的不足主要在於compaction可能會影響系統的性能。磁碟資源有限,很容易發生請求被阻塞在磁碟昂貴的compaction操作中。對吞吐量和平均反應時間的影響通常很小,但是在更高的百分位數上會很高,相比B-Trees的可預測性。

當寫入量很大時,compaction可能會對此造成影響。磁碟的寫入帶寬是有限的,這時需要初始寫入(logging和flush)、compaction共享帶寬。隨著數據量的變大,compaction可能需要更多的磁碟帶寬。

如果寫入量很大,但是compaction配置的不合理,會發生compaction跟不上寫入量的情況,有可能會導致disk耗盡。通常, LSM-Tree不會限制寫入吞吐,如果compaction跟不上寫入量,需要用戶檢查並進行相應的配置。

B-Trees的優勢是key只存在一個地方,而LSM-Tree則可能多處存在相同的key。對於強事務性的資料庫來說,選擇B-Trees更好一些,更加容易加鎖。

B-Trees可以更好的保持數據一致性,所以不會輕易退出歷史舞台。但是對一些新的資料庫,LSM-Tree則變得越來越有吸引力。


推薦閱讀:
相关文章