WiscKey: Separating Keys from Values in SSD-Conscious Storage架構
WiscKey是一個基於LSM的KV存儲引擎,特色是:針對SSD的順序和隨機讀寫都高效的特色,Key和Value分開存儲以最小化IO放大效應。YCSB場景中它比LevelDB和RocksDB都快。併發
目前的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的幾個不一樣:高併發
以上3個因素綜合起來,會致使LSM在SSD上損失90%的吞吐,並增長10倍的寫負載。工具
本文介紹的WiscKey是專門面向SSD的改良LSM系統,其核心思想是分離Key和Value,只在LSM中維護Key,把Value放在log中。這樣Key的排序和Value的GC就分開了,在排序時避免了Value的寫放大,整個LSM更小,cache效率更高。性能
分離Key和Value帶來的挑戰:測試
大多數場景WiscKey的性能都遠超LevelDB和RocksDB,除了一個場景:小Value隨機寫,且須要大範圍的Scan。優化
能夠看到LSM中一個kv對要經歷5次寫:
LSM用屢次的順序IO來避免隨機IO,從而在SATA磁盤上得到比B樹高得多的寫性能。
(下面是對compaction的介紹,LevelDB的基於層的compaction,略)
在讀的時候,LSM須要在全部可能包含這個Key的memtable和文件中查找,與B樹相比,多了不少IO。所以LSM適合於寫多讀少的場景。
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中作歸併。
讀寫放大是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。若是要讀的數據更小,這個係數會更大。
一項測試中能夠看到實際系統中的讀寫放大係數:
必需要說明的是,LSM的這種設計是爲了在SATA磁盤上得到更好的性能。SATA磁盤的一組典型數據是尋址10ms,吞吐100MB/s,而順序讀下一個Block的數據可能只要10us,與尋址相比延時是1:1000,所以只要LSM的寫放大係數不超過1000,就能得到比B樹更好的性能。而B樹的讀放大也不低,好比讀1KB的數據,B樹可能要讀6個4KB的Block,那麼讀放大係數是24,沒有徹底拉開和LSM的差距。
SSD上仍然不推薦隨機寫,由於SSD的整塊擦除再寫以及代價高昂的回收機制,當SSD上預留的Block用光時,它的寫性能會急劇降低。LSM的最大化順序寫的特性很適合SSD。
但與SATA很是不一樣的是,SSD的隨機讀性能很是好,且支持高併發。
WickKey的設計出發點就是如何利用上SSD的新特性:
WiscKey在去除了LSM的logfile後仍然能保證一致性。
WiscKey脫胎於LevelDB,能夠做爲關係型DB和分佈式KV的存儲引擎。它兼容LevelDB的API。
設計目標:
compaction就是致使LSM低效的主要緣由:一遍遍的過數據。但不作compaction又沒辦法保證讀的性能。
WiscKey受到了這麼一個小發現的啓示:咱們要排序的只是Key,Value徹底能夠另行處理。一般Key要比Value小不少,那麼排序Key的開銷也就比Value要小不少。
WiscKey中與Key放在一塊兒的只是Value的位置,Value自己存放在其它地方。
常見的使用場景下,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。
固然這樣的設計也遇到了不少挑戰。
LevelDB中這麼作RangeQuery:先Seek(),而後根據需求反覆調用Next()或Prev()讀出數據。LevelDB中Key和Value是存放在一塊兒的,這麼掃一遍對應底層就只有順序IO,性能很好(不考慮讀放大)。
WiscKey中Key和Value是分開存放的,這麼作就會帶來大量的串行隨機IO,不夠高效。WiscKey利用SSD的高併發隨機讀的特性,在對LSM調用RangeQuery期間,併發預讀後面的N個Value。
LSM都是經過compaction來回收無效數據的。WiscKey中Value不參與compaction,就須要單獨爲Value設計GC機制。
一個土辦法是掃描LSM,每一個Key對應的Value就是有效的,沒有Key對應的Value就是無效的。這麼作效率過低。
WiscKey的作法是每次寫入Value時也寫入對應的Key。
上圖中的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持久化下去:
<'tail', tail-vlog-offset>
;WiscKey的GC是可配置的,若是Key的刪除和更新都不多發生,就不須要怎麼作GC。
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相同的一致性。
WiscKey不會每筆寫入都調用一次ValueLog的write,這樣效率過低。WiscKey爲ValueLog準備了一個buffer,全部寫都寫進buffer,當寫滿或者有sync請求時再write寫到ValueLog中。讀取的時候優先讀取buffer。
缺點是在crash丟的數據會多一些,這點與LevelDB相似。
WiscKey中LSM只用於存儲Key,而ValueLog中也存儲了Key,那麼就不必再寫一遍LSM的log了。
WiscKey在LSM中存儲了一條記錄<'head', head-vlog-offset>
,在打開一個DB時就能夠從head-vlog-offset處開始恢復數據。將head保存在LSM中也保證了一致性不低於LevelDB,所以總體的一致性仍然不低於LevelDB。
ValueLog會被兩種方式訪問:
WiscKey用pthread_fadvise()在不一樣場景聲明不一樣的訪問模式。
WiscKey爲RangeQuery準備了一個32個線程的背景線程池來隨機讀ValueLog。
爲了有效地從ValueLog中回收空間,WiscKey利用了現代文件系統的另外一個特性:能夠給文件打洞(fallocate)。現代文件系統容許的最大文件大小足夠WiscKey用了(ext4容許64TB,xfs容許8EB,btrfs容許16EB),這樣就不須要考慮ValueLog的切換了。
機器配置:
第一輪:順序插入100GB的數據。第二輪:uniform隨機寫。注意第一輪順序寫不會致使LevelDB和WiscKey作compaction。
即便在256KB場景中,LevelDB的寫入吞吐仍然距離磁盤的帶寬上限很遠。
能夠看到小Value時LevelDB的延時主要花在寫log上,而大Value時延時主要花在等待寫memtable上。
LevelDB的吞吐如此之低,緣由在於compaction佔了太多資源,形成了太大的寫放大。WiscKey的compaction則只佔了不多的資源。
下圖是不一樣ValueSize下LevelDB的寫放大係數。
第一輪:在100GB隨機生成的DB上作100000次隨機查找。第二輪:在100GB隨機生成的DB上作4GB的範圍查找。
LevelDB的低吞吐緣由是讀放大和compaction資源佔用多。
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倍。
測試內容:1. 隨機生成DB;2. 刪掉必定比例的kv;3. 隨機插入數據同時後臺作GC。做者固定Key+Value爲4KB,但第二步刪除的kv的比例從25%-100%不等。
100%刪除時,GC掃過的都是無效的Value,也就不會寫數據,所以只下降了10%的吞吐。後面的場景GC都會把有效的Value再寫進ValueLog,所以下降了35%的吞。
不管哪一個場景,WiscKey都比LevelDB快至少70倍。
做者一邊作異步和同步的Put(),一邊用ALICE工具來模擬多種系統崩潰場景。ALICE模擬了3000種系統崩潰場景,沒有發現WiscKey引入的一致性問題。(不比LevelDB差)
WiscKey在恢復時要作的工做比LevelDB多一點,但都與LSM最後一次持久化memtable到崩潰發生之間寫入的數據量成正比。在一個最壞的場景中,ValueSize爲1KB,LevelDB恢復花了0.7秒,而WiscKey花了2.6秒。
WiscKey能夠經過加快LSM中head
記錄的持久化頻率來下降恢復時間。
咱們在評估一個kv系統時,每每只看它的讀寫性能。但在SSD上,它的空間放大也很重要,由於單GB的成本變高了。所謂空間放大就是kv系統實際佔用的磁盤空間除以用戶寫入的數據大小。壓縮能下降空間放大,而垃圾、碎片、元數據則在增長空間放大。做者關掉了壓縮,簡化討論。
徹底順序寫入的場景,空間放大係數很接近1。而對於隨機寫入,或是有更新的場景,空間放大係數就會大於1了。
下圖是LevelDB和WiscKey在載入一個100GB的隨機寫入的數據集後的DB大小。
LevelDB多出來的空間主要是在加載結束時還沒來得及回收掉的無效kv對。WiscKey多出來的空間包括了無效的數據、元數據(LSM中的Value索引,ValueLog中的Key)。在GC後無效數據就沒有了,而元數據又很是少,所以整個DB的大小很是接近原始數據大小。
KV存儲沒辦法兼顧寫放大、讀放大、空間放大,只能從中作取捨。LevelDB中GC和排序是在一塊兒的,它選擇了高的寫放大來換取低的空間放大,但與此同時在線請求就會受影響。WiscKey則用更多的空間來換取更低的IO放大,由於GC和排序被解耦了,GC能夠晚一點作,對在線請求的影響就會小不少。
能夠看到除了順序寫入以外,LevelDB的CPU使用率都要比WiscKey低。
順序寫入場景LevelDB要把kv都寫進log,還要編碼kv,佔了不少CPU。WiscKey寫的log更少,所以CPU消耗更低。
範圍讀場景WiscKey要用32個讀線程作背景的隨機讀,必然用多得多的CPU。
LevelDB不是一個面向高併發的DB,所以CPU不是瓶頸,這點RocksDB作得更好。
(直接上圖,結論不說了)