LevelDB庫簡介
一、LevelDB入門
LevelDB是Google開源的持久化KV單機數據庫,具有很高的隨機寫,順序讀/寫性能,但是隨機讀的性能很一般,也就是說,LevelDB很適合應用在查詢較少,而寫很多的場景。LevelDB應用了LSM (Log Structured Merge) 策略,lsm_tree對索引變更進行延遲及批量處理,並通過一種類似於歸並排序的方式高效地將更新遷移到磁盤,降低索引插入開銷,關於LSM,本文在后面也會簡單提及。
根據Leveldb官方網站的描述,LevelDB的特點和限制如下:
特點:
1、key和value都是任意長度的字節數組;
2、entry(即一條K-V記錄)默認是按照key的字典順序存儲的,當然開發者也可以重載這個排序函數;
3、提供的基本操作接口:Put()、Delete()、Get()、Batch();
4、支持批量操作以原子操作進行;
5、可以創建數據全景的snapshot(快照),並允許在快照中查找數據;
6、可以通過前向(或后向)迭代器遍歷數據(迭代器會隱含的創建一個snapshot);
7、自動使用Snappy壓縮數據;
8、可移植性;
限制:
1、非關系型數據模型(NoSQL),不支持sql語句,也不支持索引;
2、一次只允許一個進程訪問一個特定的數據庫;
3、沒有內置的C/S架構,但開發者可以使用LevelDB庫自己封裝一個server;
LevelDB本身只是一個lib庫,在源碼目錄make編譯即可,然后在我們的應用程序里面可以直接include leveldb/include/db.h頭文件,該頭文件有幾個基本的數據庫操作接口,下面是一個測試例子:
#include <iostream> #include <string> #include <assert.h> #include "leveldb/db.h" using namespace std; int main(void) { leveldb::DB *db; leveldb::Options options; options.create_if_missing = true; // open leveldb::Status status = leveldb::DB::Open(options,"/tmp/testdb", &db); assert(status.ok()); string key = "name"; string value = "chenqi"; // write status = db->Put(leveldb::WriteOptions(), key, value); assert(status.ok()); // read status = db->Get(leveldb::ReadOptions(), key, &value); assert(status.ok()); cout<<value<<endl; // delete status = db->Delete(leveldb::WriteOptions(), key); assert(status.ok()); status = db->Get(leveldb::ReadOptions(),key, &value); if(!status.ok()) { cerr<<key<<" "<<status.ToString()<<endl; } else { cout<<key<<"==="<<value<<endl; } // close delete db; return 0; }
上面的例子演示了如何插入、獲取、刪除一條記錄,編譯代碼:
g++ -o test test.cpp libleveldb.a -lpthread -Iinclude
執行./test后,會在/tmp下面生成一個目錄testdb,里面包含若干文件:
然后簡要說下各個文件的含義:
1、CURRENT
2、LOG
3、LOCK
4、MANIFEST
下圖是LevelDB運行一段時間后的存儲模型快照:內存中的MemTable和Immutable MemTable以及磁盤上的幾種主要文件:Current文件,Manifest文件,log文件以及SSTable文件。當然,LevelDb除了這六個主要部分還有一些輔助的文件,但是以上六個文件和數據結構是LevelDb的主體構成元素。
log文件、MemTable、SSTable文件都是用來存儲k-v記錄的,下面再說說manifest和Current文件的作用。
SSTable中的某個文件屬於特定層級,而且其存儲的記錄是key有序的,那么必然有文件中的最小key和最大key,這是非常重要的信息,Manifest 就記載了SSTable各個文件的管理信息,比如屬於哪個Level,文件名稱叫啥,最小key和最大key各自是多少。下圖是Manifest所存儲內容的示意:
另外,在LevleDb的運行過程中,隨着Compaction的進行,SSTable文件會發生變化,會有新的文件產生,老的文件被廢棄,Manifest也會跟着反映這種變化,此時往往會新生成Manifest文件來記載這種變化,而Current則用來指出哪個Manifest文件才是我們關心的那個Manifest文件。
二、讀寫數據
寫操作流程:
1、順序寫入磁盤log文件;
2、寫入內存memtable(采用skiplist結構實現);
3、寫入磁盤SST文件(sorted string table files),這步是數據歸檔的過程(永久化存儲);
注意:
- log文件的作用是是用於系統崩潰恢復而不丟失數據,假如沒有Log文件,因為寫入的記錄剛開始是保存在內存中的,此時如果系統崩潰,內存中的數據還沒有來得及Dump到磁盤,所以會丟失數據;
- 在寫memtable時,如果其達到check point(滿員)的話,會將其改成immutable memtable(只讀),然后等待dump到磁盤SST文件中,此時也會生成新的memtable供寫入新數據;
- memtable和sst文件中的key都是有序的,log文件的key是無序的;
- LevelDB刪除操作也是插入,只是標記Key為刪除狀態,真正的刪除要到Compaction的時候才去做真正的操作;
- LevelDB沒有更新接口,如果需要更新某個Key的值,只需要插入一條新紀錄即可;或者先刪除舊記錄,再插入也可;
讀操作流程:
1、在內存中依次查找memtable、immutable memtable;
2、如果配置了cache,查找cache;
3、根據mainfest索引文件,在磁盤中查找SST文件;
舉個例子:我們先往levelDb里面插入一條數據 {key="www.samecity.com" value="我們"},過了幾天,samecity網站改名為:69同城,此時我們插入數據{key="www.samecity.com" value="69同城"},同樣的key,不同的value;邏輯上理解好像levelDb中只有一個存儲記錄,即第二個記錄,但是在levelDb中很可能存在兩條記錄,即上面的兩個記錄都在levelDb中存儲了,此時如果用戶查詢key="www.samecity.com",我們當然希望找到最新的更新記錄,也就是第二個記錄返回,因此,查找的順序應該依照數據更新的新鮮度來,對於SSTable文件來說,如果同時在level L和Level L+1找到同一個key,level L的信息一定比level L+1的要新。
三、SSTable文件
SST文件並不是平坦的結構,而是分層組織的,這也是LevelDB名稱的來源。
SST文件的一些實現細節:
1、每個SST文件大小上限為2MB,所以,LevelDB通常存儲了大量的SST文件;
2、SST文件由若干個4K大小的blocks組成,block也是讀/寫操作的最小單元;
3、SST文件的最后一個block是一個index,指向每個data block的起始位置,以及每個block第一個entry的key值(block內的key有序存儲);
4、使用Bloom filter加速查找,只要掃描index,就可以快速找出所有可能包含指定entry的block。
5、同一個block內的key可以共享前綴(只存儲一次),這樣每個key只要存儲自己唯一的后綴就行了。如果block中只有部分key需要共享前綴,在這部分key與其它key之間插入"reset"標識。
由log直接讀取的entry會寫到Level 0的SST中(最多4個文件);
當Level 0的4個文件都存儲滿了,會選擇其中一個文件Compact到Level 1的SST中;
注意:Level 0的SSTable文件和其它Level的文件相比有特殊性:這個層級內的.sst文件,兩個文件可能存在key重疊,比如有兩個level 0的sst文件,文件A和文件B,文件A的key范圍是:{bar, car},文件B的Key范圍是{blue,samecity},那么很可能兩個文件都存在key=”blood”的記錄。對於其它Level的SSTable文件來說,則不會出現同一層級內.sst文件的key重疊現象,就是說Level L中任意兩個.sst文件,那么可以保證它們的key值是不會重疊的。
Log:最大4MB (可配置), 會寫入Level 0;
Level 0:最多4個SST文件,;
Level 1:總大小不超過10MB;
Level 2:總大小不超過100MB;
Level 3+:總大小不超過上一個Level ×10的大小。
比如:0 ↠ 4 SST, 1 ↠ 10M, 2 ↠ 100M, 3 ↠ 1G, 4 ↠ 10G, 5 ↠ 100G, 6 ↠ 1T, 7 ↠ 10T
在讀操作中,要查找一條entry,先查找log,如果沒有找到,然后在Level 0中查找,如果還是沒有找到,再依次往更底層的Level順序查找;如果查找了一條不存在的entry,則要遍歷一遍所有的Level才能返回"Not Found"的結果。
在寫操作中,新數據總是先插入開頭的幾個Level中,開頭的這幾個Level存儲量也比較小,因此,對某條entry的修改或刪除操作帶來的性能影響就比較可控。
可見,SST采取分層結構是為了最大限度減小插入新entry時的開銷;
Compaction操作
對於LevelDb來說,寫入記錄操作很簡單,刪除記錄僅僅寫入一個刪除標記就算完事,但是讀取記錄比較復雜,需要在內存以及各個層級文件中依照新鮮程度依次查找,代價很高。為了加快讀取速度,levelDb采取了compaction的方式來對已有的記錄進行整理壓縮,通過這種方式,來刪除掉一些不再有效的KV數據,減小數據規模,減少文件數量等。
LevelDb的compaction機制和過程與Bigtable所講述的是基本一致的,Bigtable中講到三種類型的compaction: minor ,major和full:
- minor Compaction,就是把memtable中的數據導出到SSTable文件中;
- major compaction就是合並不同層級的SSTable文件;
- full compaction就是將所有SSTable進行合並;
LevelDb包含其中兩種,minor和major。
Minor compaction 的目的是當內存中的memtable大小到了一定值時,將內容保存到磁盤文件中,如下圖:
immutable memtable其實是一個SkipList,其中的記錄是根據key有序排列的,遍歷key並依次寫入一個level 0 的新建SSTable文件中,寫完后建立文件的index 數據,這樣就完成了一次minor compaction。從圖中也可以看出,對於被刪除的記錄,在minor compaction過程中並不真正刪除這個記錄,原因也很簡單,這里只知道要刪掉key記錄,但是這個KV數據在哪里?那需要復雜的查找,所以在minor compaction的時候並不做刪除,只是將這個key作為一個記錄寫入文件中,至於真正的刪除操作,在以后更高層級的compaction中會去做。
當某個level下的SSTable文件數目超過一定設置值后,levelDb會從這個level的SSTable中選擇一個文件(level>0),將其和高一層級的level+1的SSTable文件合並,這就是major compaction。
我們知道在大於0的層級中,每個SSTable文件內的Key都是由小到大有序存儲的,而且不同文件之間的key范圍(文件內最小key和最大key之間)不會有任何重疊。Level 0的SSTable文件有些特殊,盡管每個文件也是根據Key由小到大排列,但是因為level 0的文件是通過minor compaction直接生成的,所以任意兩個level 0下的兩個sstable文件可能再key范圍上有重疊。所以在做major compaction的時候,對於大於level 0的層級,選擇其中一個文件就行,但是對於level 0來說,指定某個文件后,本level中很可能有其他SSTable文件的key范圍和這個文件有重疊,這種情況下,要找出所有有重疊的文件和level 1的文件進行合並,即level 0在進行文件選擇的時候,可能會有多個文件參與major compaction。
LevelDb在選定某個level進行compaction后,還要選擇是具體哪個文件要進行compaction,比如這次是文件A進行compaction,那么下次就是在key range上緊挨着文件A的文件B進行compaction,這樣每個文件都會有機會輪流和高層的level 文件進行合並。
如果選好了level L的文件A和level L+1層的文件進行合並,那么問題又來了,應該選擇level L+1哪些文件進行合並?levelDb選擇L+1層中和文件A在key range上有重疊的所有文件來和文件A進行合並。也就是說,選定了level L的文件A,之后在level L+1中找到了所有需要合並的文件B,C,D…..等等。剩下的問題就是具體是如何進行major 合並的?就是說給定了一系列文件,每個文件內部是key有序的,如何對這些文件進行合並,使得新生成的文件仍然Key有序,同時拋掉哪些不再有價值的KV 數據。
Major compaction的過程如下:對多個文件采用多路歸並排序的方式,依次找出其中最小的Key記錄,也就是對多個文件中的所有記錄重新進行排序。之后采取一定的標准判斷這個Key是否還需要保存,如果判斷沒有保存價值,那么直接拋掉,如果覺得還需要繼續保存,那么就將其寫入level L+1層中新生成的一個SSTable文件中。就這樣對KV數據一一處理,形成了一系列新的L+1層數據文件,之前的L層文件和L+1層參與compaction 的文件數據此時已經沒有意義了,所以全部刪除。這樣就完成了L層和L+1層文件記錄的合並過程。
那么在major compaction過程中,判斷一個KV記錄是否拋棄的標准是什么呢?其中一個標准是:對於某個key來說,如果在小於L層中存在這個Key,那么這個KV在major compaction過程中可以拋掉。因為我們前面分析過,對於層級低於L的文件中如果存在同一Key的記錄,那么說明對於Key來說,有更新鮮的Value存在,那么過去的Value就等於沒有意義了,所以可以刪除。
四、Cache
前面講過對於levelDb來說,讀取操作如果沒有在內存的memtable中找到記錄,要多次進行磁盤訪問操作。假設最優情況,即第一次就在level 0中最新的文件中找到了這個key,那么也需要讀取2次磁盤,一次是將SSTable的文件中的index部分讀入內存,這樣根據這個index可以確定key是在哪個block中存儲;第二次是讀入這個block的內容,然后在內存中查找key對應的value。
LevelDb中引入了兩個不同的Cache:Table Cache和Block Cache。其中Block Cache是配置可選的,即在配置文件中指定是否打開這個功能。
如上圖,在Table Cache中,key值是SSTable的文件名稱,Value部分包含兩部分,一個是指向磁盤打開的SSTable文件的文件指針,這是為了方便讀取內容;另外一個是指向內存中這個SSTable文件對應的Table結構指針,table結構在內存中,保存了SSTable的index內容以及用來指示block cache用的cache_id ,當然除此外還有其它一些內容。
比如在get(key)讀取操作中,如果levelDb確定了key在某個level下某個文件A的key range范圍內,那么需要判斷是不是文件A真的包含這個KV。此時,levelDb會首先查找Table Cache,看這個文件是否在緩存里,如果找到了,那么根據index部分就可以查找是哪個block包含這個key。如果沒有在緩存中找到文件,那么打開SSTable文件,將其index部分讀入內存,然后插入Cache里面,去index里面定位哪個block包含這個Key 。如果確定了文件哪個block包含這個key,那么需要讀入block內容,這是第二次讀取。
Block Cache是為了加快這個過程的,其中的key是文件的cache_id加上這個block在文件中的起始位置block_offset。而value則是這個Block的內容。
如果levelDb發現這個block在block cache中,那么可以避免讀取數據,直接在cache里的block內容里面查找key的value就行,如果沒找到呢?那么讀入block內容並把它插入block cache中。levelDb就是這樣通過兩個cache來加快讀取速度的。從這里可以看出,如果讀取的數據局部性比較好,也就是說要讀的數據大部分在cache里面都能讀到,那么讀取效率應該還是很高的,而如果是對key進行順序讀取效率也應該不錯,因為一次讀入后可以多次被復用。但是如果是隨機讀取,您可以推斷下其效率如何。
五、版本控制
在Leveldb中,Version就代表了一個版本,它包括當前磁盤及內存中的所有文件信息。在所有的version中,只有一個是CURRENT(當前版本),其它都是歷史版本。
當執行一次compaction 或者 創建一個Iterator后,Leveldb將在當前版本基礎上創建一個新版本,當前版本就變成了歷史版本。
VersionSet 是所有Version的集合,管理着所有存活的Version。
VersionEdit 表示Version之間的變化,相當於delta 增量,表示有增加了多少文件,刪除了文件:
Version0 + VersionEdit --> Version1
Version0->Version1->Version2->Version3
VersionEdit會保存到MANIFEST文件中,當做數據恢復時就會從MANIFEST文件中讀出來重建數據。
Leveldb的這種版本的控制,讓我想到了雙buffer切換,雙buffer切換來自於圖形學中,用於解決屏幕繪制時的閃屏問題,在服務器編程中也有用處。
比如我們的服務器上有一個字典庫,每天我們需要更新這個字典庫,我們可以新開一個buffer,將新的字典庫加載到這個新buffer中,等到加載完畢,將字典的指針指向新的字典庫。
Leveldb的version管理和雙buffer切換類似,但是如果原version被某個iterator引用,那么這個version會一直保持,直到沒有被任何一個iterator引用,此時就可以刪除這個version。
六、改進
HyperLevelDB是在LevelDB的基礎上做的一個分支,它主要在下面兩點提升了LevelDB的性能:
- 改進了寫線程的並發性;
- 改進的壓縮過程,提高了吞吐量;
參考文檔:
http://dailyjs.com/2013/04/19/leveldb-and-node-1/
http://blog.csdn.net/qq112928/article/details/21275999
http://www.cnblogs.com/haippy/archive/2011/12/04/2276064.html