WiscKey: Separating Keys from Values in SSD-Conscious Storage [讀後整理]

WiscKey: Separating Keys from Values in SSD-Conscious Storage架構

WiscKey是一個基於LSM的KV存儲引擎,特色是:針對SSD的順序和隨機讀寫都高效的特色,Key和Value分開存儲以最小化IO放大效應。YCSB場景中它比LevelDB和RocksDB都快。併發

1 介紹

目前的KV存儲引擎中,對寫性能要求比較高的大多數都採用了LSM,典型的有BigTable/LevelDB/Cassandra/HBase/RocksDB/PNUTS/Riak。LSM相比其它索引樹(如B樹)的主要優點在於,它的寫都是順序寫。B樹在有少許修改時,均可能產生大量的隨機寫,不論是SSD仍是SATA上都表現不佳。app

爲了保證寫性能,LSM會不停的批量把KV對寫成文件;爲了保證讀性能,LSM還須要不停的作背景compaction,把這些文件合併成單個按Key有序的文件。這就致使了相同的數據在它的生命期中被反覆讀寫。一個典型的LSM系統中數據的IO放大係數能夠達到50倍以上。異步

LSM的成功在於,它充分利用了SATA磁盤順序IO性能遠超隨機IO的特色(100倍以上),只要IO放大不超過這個數字,那麼用順序IO來替代隨機IO就是成功的。分佈式

但到了SSD上就不同了。SSD與SATA的幾個不一樣:高併發

  1. 順序IO和隨機IO的差異沒那麼大,這讓LSM爲了減小隨機IO而付出的額外的IO變得再也不必要。
  2. SSD能夠承受高併發的IO,而LSM利用的並很差。
  3. 長期大量的重複寫會影響SSD的性能和壽命。

以上3個因素綜合起來,會致使LSM在SSD上損失90%的吞吐,並增長10倍的寫負載。工具

本文介紹的WiscKey是專門面向SSD的改良LSM系統,其核心思想是分離Key和Value,只在LSM中維護Key,把Value放在log中。這樣Key的排序和Value的GC就分開了,在排序時避免了Value的寫放大,整個LSM更小,cache效率更高。性能

分離Key和Value帶來的挑戰:測試

  1. Scan時性能受影響,由於Value再也不按Key的順序排列了。WiscKey的解法是充分利用SSD的高併發。
  2. 須要單獨作GC來清理無效數據,回收空間。WiscKey提出在線作輕量GC,只須要順序IO,最小化對前臺負載的影響。
  3. crash時如何保證一致性。WiscKey利用了現代文件系統的一個特性:append不會產生垃圾。

大多數場景WiscKey的性能都遠超LevelDB和RocksDB,除了一個場景:小Value隨機寫,且須要大範圍的Scan。優化

2 背景和動機

2.1 LSM

wisckey_lsm.png

能夠看到LSM中一個kv對要經歷5次寫:

  1. log文件;
  2. memtable;
  3. immutable memtable;
  4. L0文件;
  5. 其它level的文件。

LSM用屢次的順序IO來避免隨機IO,從而在SATA磁盤上得到比B樹高得多的寫性能。

(下面是對compaction的介紹,LevelDB的基於層的compaction,略)

在讀的時候,LSM須要在全部可能包含這個Key的memtable和文件中查找,與B樹相比,多了不少IO。所以LSM適合於寫多讀少的場景。

2.2 LevelDB

LevelDB的總體架構見上節的圖。LevelDB包括一個磁盤上的logfile,兩個內存中的memtable(memtable和immutable memtable),以及若干個磁盤上的L0-L6的SSTable文件。

LevelDB插入時先寫logfile,再寫進memtable;memtable滿了以後變成immutable memtable,再寫成L0的SSTable文件。每層SSTable文件的size比例差很少是10。L1-L6的SSTable都是經過compaction生成的,LevelDB保證每一層的各個SSTable文件的KeyRange不重疊,L0除外。

查找時就是在全部memtable和SSTable中作歸併。

2.3 讀寫放大

讀寫放大是LSM的主要問題。

寫放大:文件從Li-1到Li的過程當中,由於兩層的size limit差10倍,所以此次Compaction的放大係數最大能夠到10。這樣從L0到L6的放大係數能夠達到50(L1-L6每層10)。

(這裏我有疑問,相同的數據從寫入到L6,一共被寫入了8次磁盤,所以放大係數最可能是8吧?)

讀放大:假設L0有8個文件,那麼查找時最多須要讀14個文件(L1-L6每層最多1個文件);假設要讀1KB的數據,那麼每一個文件最多要讀24KB的數據(index block + bloom-filter blocks + data block)。這麼算下來讀的放大係數就是14*24=336。若是要讀的數據更小,這個係數會更大。

一項測試中能夠看到實際系統中的讀寫放大係數:

wisckey_wr_amplification.png

必需要說明的是,LSM的這種設計是爲了在SATA磁盤上得到更好的性能。SATA磁盤的一組典型數據是尋址10ms,吞吐100MB/s,而順序讀下一個Block的數據可能只要10us,與尋址相比延時是1:1000,所以只要LSM的寫放大係數不超過1000,就能得到比B樹更好的性能。而B樹的讀放大也不低,好比讀1KB的數據,B樹可能要讀6個4KB的Block,那麼讀放大係數是24,沒有徹底拉開和LSM的差距。

2.4 快速存儲硬件

SSD上仍然不推薦隨機寫,由於SSD的整塊擦除再寫以及代價高昂的回收機制,當SSD上預留的Block用光時,它的寫性能會急劇降低。LSM的最大化順序寫的特性很適合SSD。

wisckey_ssd_performance.png

但與SATA很是不一樣的是,SSD的隨機讀性能很是好,且支持高併發。

3 WiscKey

WickKey的設計出發點就是如何利用上SSD的新特性:

  1. Key與Value分離,Key由LSM維護,而Value則寫入logfile。
  2. 鑑於Value再也不排序,WiscKey在讀的時候會併發隨機讀。
  3. WiscKey在Value log的管理上有本身的一致性和回收機制。

WiscKey在去除了LSM的logfile後仍然能保證一致性。

3.1 設計目標

WiscKey脫胎於LevelDB,能夠做爲關係型DB和分佈式KV的存儲引擎。它兼容LevelDB的API。

設計目標:

  1. 低寫放大:既爲了寫性能,也爲了SSD的壽命。
  2. 低讀放大:讀放大會下降讀的吞吐,同時還下降了cache效率。
  3. 面向SSD優化。
  4. 豐富的API。
  5. 針對實際的Key-Value大小,不作太不實際的假設。一般的Key都很小(16B),Value則從100B到4KB都很常見。

3.2 Key與Value分離

compaction就是致使LSM低效的主要緣由:一遍遍的過數據。但不作compaction又沒辦法保證讀的性能。

WiscKey受到了這麼一個小發現的啓示:咱們要排序的只是Key,Value徹底能夠另行處理。一般Key要比Value小不少,那麼排序Key的開銷也就比Value要小不少。

WiscKey中與Key放在一塊兒的只是Value的位置,Value自己存放在其它地方。

wisckey_ssd_layout.png

常見的使用場景下,WiscKey中的LSM要比LevelDB小得多。這樣就大大下降了寫的放大係數。Key爲16B,Value爲1KB的場景,假設Key的放大係數是10(LSM帶來的),Value的放大係數是1,那麼WiscKey的總體放大係數是(10 × 16 + 1024) / (16 + 1024) = 1.14。

查找的時候,WiscKey先在LSM中查找Key,再根據Key中Value的位置查找Value。由於WiscKey中的LSM比LevelDB中的小不少,前面的查找會快不少,絕大多數狀況下都能命中cache,這樣整個開銷就是一次隨機查找。而SSD的隨機查找性能又這麼好,所以WiscKey的讀性能就比LevelDB好不少。

插入一組kv時,WiscKey先把Value寫入ValueLog,而後再把Key插入到LSM中。刪除一個Key則只從LSM中刪除它,不動ValueLog。

固然這樣的設計也遇到了不少挑戰。

3.3 挑戰

3.3.1 併發範圍查找

LevelDB中這麼作RangeQuery:先Seek(),而後根據需求反覆調用Next()或Prev()讀出數據。LevelDB中Key和Value是存放在一塊兒的,這麼掃一遍對應底層就只有順序IO,性能很好(不考慮讀放大)。

WiscKey中Key和Value是分開存放的,這麼作就會帶來大量的串行隨機IO,不夠高效。WiscKey利用SSD的高併發隨機讀的特性,在對LSM調用RangeQuery期間,併發預讀後面的N個Value。

3.3.2 垃圾回收

LSM都是經過compaction來回收無效數據的。WiscKey中Value不參與compaction,就須要單獨爲Value設計GC機制。

一個土辦法是掃描LSM,每一個Key對應的Value就是有效的,沒有Key對應的Value就是無效的。這麼作效率過低。

WiscKey的作法是每次寫入Value時也寫入對應的Key。

wisckey_log_layout.png

上圖中的head老是指向ValueLog的尾部,新數據寫到這裏。而tail會隨着GC的進行向後移動。全部有效數據都在tail~head區間中,每次GC都從tail開始,也只有GC線程能夠修改tail。

GC時WiscKey每次從tail開始讀若干MB的數據,而後再查找對應的Key,看這個Key如今對應的Value仍是不是log中的Value,若是是,再把數據追加到head處。最終,ValueLog中的無效數據就都被清理掉了。

爲了不GC時crash致使丟數據,WiscKey要保證在真正回收空間前先把新追加的數據和新的tail持久化下去:

  1. 追加數據;
  2. GC線程調用fsync()將新數據寫下去;
  3. 向LSM中同步寫一條記錄:<'tail', tail-vlog-offset>
  4. 回收空間。

WiscKey的GC是可配置的,若是Key的刪除和更新都不多發生,就不須要怎麼作GC。

3.3.3 崩潰時的一致性

WiscKey爲了保證系統崩潰時的一致性,使用了現代文件系統(ext4/btrfs/xfs等)的一個特性:追加寫不會產生垃圾,只可能在尾部缺乏一些數據。在WiscKey中這個特性意味着:若是Value X在一次crash後從ValueLog中丟失了,那麼全部X後面寫入的Value就都丟了。

crash中丟失的Key是沒辦法被發現的,這個Key對應的Value會被看成無效數據GC掉。若是查找時發現Key存在,但對應的Value不在ValueLog中,就說明這個Value丟失了,WiscKey會將這個Key從LSM中刪除,並返回"Key不存在"。(沒辦法找回上一個Value了是嗎?)

若是用戶配置了sync,WiscKey會在每次寫完ValueLog後,寫LSM前,調用一次fsync。

總之WiscKey保證了與LevelDB相同的一致性。

3.4 優化

3.4.1 ValueLog的寫緩衝

WiscKey不會每筆寫入都調用一次ValueLog的write,這樣效率過低。WiscKey爲ValueLog準備了一個buffer,全部寫都寫進buffer,當寫滿或者有sync請求時再write寫到ValueLog中。讀取的時候優先讀取buffer。

缺點是在crash丟的數據會多一些,這點與LevelDB相似。

wisckey_write_unit_size.png

3.4.2 優化LSM的log

WiscKey中LSM只用於存儲Key,而ValueLog中也存儲了Key,那麼就不必再寫一遍LSM的log了。

WiscKey在LSM中存儲了一條記錄<'head', head-vlog-offset>,在打開一個DB時就能夠從head-vlog-offset處開始恢復數據。將head保存在LSM中也保證了一致性不低於LevelDB,所以總體的一致性仍然不低於LevelDB。

3.5 實現

ValueLog會被兩種方式訪問:

  1. 讀取時會隨機訪問ValueLog。
  2. 寫入時會順序寫入ValueLog。

WiscKey用pthread_fadvise()在不一樣場景聲明不一樣的訪問模式。

WiscKey爲RangeQuery準備了一個32個線程的背景線程池來隨機讀ValueLog。

爲了有效地從ValueLog中回收空間,WiscKey利用了現代文件系統的另外一個特性:能夠給文件打洞(fallocate)。現代文件系統容許的最大文件大小足夠WiscKey用了(ext4容許64TB,xfs容許8EB,btrfs容許16EB),這樣就不須要考慮ValueLog的切換了。

4 評價

機器配置:

  1. CPU:Intel(R) Xeon(R) CPU E5-2667 v2 @ 3.30GHz * 2;
  2. 內存:64GB;
  3. OS:64-bit Linux 3.14;
  4. 文件系統:ext4;
  5. SSD:500-GB Samsung 840 EVO SSD,順序讀500MB/s,順序寫400MB/s。

4.1 基準測試

  1. 工具:db_bench;
  2. DB:LevelDB/WiscKey;
  3. Key:16B;
  4. Value:不少大小;
  5. 壓縮:關閉。

4.1.1 Load

第一輪:順序插入100GB的數據。第二輪:uniform隨機寫。注意第一輪順序寫不會致使LevelDB和WiscKey作compaction。

wisckey_load_perf_seq.png

即便在256KB場景中,LevelDB的寫入吞吐仍然距離磁盤的帶寬上限很遠。

wisckey_load_perf_ldb_dist.png

能夠看到小Value時LevelDB的延時主要花在寫log上,而大Value時延時主要花在等待寫memtable上。

wisckey_load_perf_rand.png

LevelDB的吞吐如此之低,緣由在於compaction佔了太多資源,形成了太大的寫放大。WiscKey的compaction則只佔了不多的資源。

下圖是不一樣ValueSize下LevelDB的寫放大係數。

wisckey_load_perf_write_amp.png

4.1.2 Query

第一輪:在100GB隨機生成的DB上作100000次隨機查找。第二輪:在100GB隨機生成的DB上作4GB的範圍查找。

wisckey_query_perf_rand.png

LevelDB的低吞吐緣由是讀放大和compaction資源佔用多。

wisckey_query_perf_range.png

ValueSize超過4KB後,LevelDB生成的SSTable文件變多,吞吐變差。此時WiscKey吞吐是LevelDB的8.4倍。而在ValueSize爲64B時,受限於SSD的隨機讀能力,LevelDB的吞吐是WiscKey的12倍。若是換一塊支持更高併發的盤,這裏的性能差距會變小一些。

但若是數據是順序插入的,那麼WiscKey的ValueLog也會被順序訪問,差距就沒有這麼大。64B時LevelDB是WiscKey的1.3倍,而大Value時WiscKey是LevelDB的2.8倍。

4.1.3 垃圾回收

測試內容:1. 隨機生成DB;2. 刪掉必定比例的kv;3. 隨機插入數據同時後臺作GC。做者固定Key+Value爲4KB,但第二步刪除的kv的比例從25%-100%不等。

wisckey_gc_perf.png

100%刪除時,GC掃過的都是無效的Value,也就不會寫數據,所以只下降了10%的吞吐。後面的場景GC都會把有效的Value再寫進ValueLog,所以下降了35%的吞。

不管哪一個場景,WiscKey都比LevelDB快至少70倍。

4.1.4 崩潰時的一致性

做者一邊作異步和同步的Put(),一邊用ALICE工具來模擬多種系統崩潰場景。ALICE模擬了3000種系統崩潰場景,沒有發現WiscKey引入的一致性問題。(不比LevelDB差)

WiscKey在恢復時要作的工做比LevelDB多一點,但都與LSM最後一次持久化memtable到崩潰發生之間寫入的數據量成正比。在一個最壞的場景中,ValueSize爲1KB,LevelDB恢復花了0.7秒,而WiscKey花了2.6秒。

WiscKey能夠經過加快LSM中head記錄的持久化頻率來下降恢復時間。

4.1.5 空間放大

咱們在評估一個kv系統時,每每只看它的讀寫性能。但在SSD上,它的空間放大也很重要,由於單GB的成本變高了。所謂空間放大就是kv系統實際佔用的磁盤空間除以用戶寫入的數據大小。壓縮能下降空間放大,而垃圾、碎片、元數據則在增長空間放大。做者關掉了壓縮,簡化討論。

徹底順序寫入的場景,空間放大係數很接近1。而對於隨機寫入,或是有更新的場景,空間放大係數就會大於1了。

下圖是LevelDB和WiscKey在載入一個100GB的隨機寫入的數據集後的DB大小。

wisckey_space_amp_perf.png

LevelDB多出來的空間主要是在加載結束時還沒來得及回收掉的無效kv對。WiscKey多出來的空間包括了無效的數據、元數據(LSM中的Value索引,ValueLog中的Key)。在GC後無效數據就沒有了,而元數據又很是少,所以整個DB的大小很是接近原始數據大小。

KV存儲沒辦法兼顧寫放大、讀放大、空間放大,只能從中作取捨。LevelDB中GC和排序是在一塊兒的,它選擇了高的寫放大來換取低的空間放大,但與此同時在線請求就會受影響。WiscKey則用更多的空間來換取更低的IO放大,由於GC和排序被解耦了,GC能夠晚一點作,對在線請求的影響就會小不少。

4.1.6 CPU使用率

wisckey_cpu_usage.png

能夠看到除了順序寫入以外,LevelDB的CPU使用率都要比WiscKey低。

順序寫入場景LevelDB要把kv都寫進log,還要編碼kv,佔了不少CPU。WiscKey寫的log更少,所以CPU消耗更低。

範圍讀場景WiscKey要用32個讀線程作背景的隨機讀,必然用多得多的CPU。

LevelDB不是一個面向高併發的DB,所以CPU不是瓶頸,這點RocksDB作得更好。

4.2 YCSB測試

wisckey_ycsb.png

(直接上圖,結論不說了)

相關文章
相關標籤/搜索