LevelDB
一、LevelDB入門
LevelDB是Google開源的持久化KV單機(jī)數(shù)據(jù)庫,具有很高的隨機(jī)寫,順序讀/寫性能,但是隨機(jī)讀的性能很一般,也就是說,LevelDB很適合應(yīng)用在查詢較少,而寫很多的場景。LevelDB應(yīng)用了LSM?(Log Structured Merge) 策略,lsm_tree對索引變更進(jìn)行延遲及批量處理,并通過一種類似于歸并排序的方式高效地將更新遷移到磁盤,降低索引插入開銷,關(guān)于LSM,本文在后面也會簡單提及。
?
根據(jù)Leveldb官方網(wǎng)站的描述,LevelDB的特點(diǎn)和限制如下:
特點(diǎn):
1、key和value都是任意長度的字節(jié)數(shù)組;
2、entry(即一條K-V記錄)默認(rèn)是按照key的字典順序存儲的,當(dāng)然開發(fā)者也可以重載這個排序函數(shù);
3、提供的基本操作接口:Put()、Delete()、Get()、Batch();
4、支持批量操作以原子操作進(jìn)行;
5、可以創(chuàng)建數(shù)據(jù)全景的snapshot(快照),并允許在快照中查找數(shù)據(jù);
6、可以通過前向(或后向)迭代器遍歷數(shù)據(jù)(迭代器會隱含的創(chuàng)建一個snapshot);
7、自動使用Snappy壓縮數(shù)據(jù);
8、可移植性;
限制:
1、非關(guān)系型數(shù)據(jù)模型(NoSQL),不支持sql語句,也不支持索引;
2、一次只允許一個進(jìn)程訪問一個特定的數(shù)據(jù)庫;
3、沒有內(nèi)置的C/S架構(gòu),但開發(fā)者可以使用LevelDB庫自己封裝一個server;
?
?python 版示例
安裝依賴
pip install ?leveldb
?
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | python 2.7 示例<br>>>> import leveldb >>> db = leveldb.LevelDB('./db') >>> db.Put('hello',?'world') >>> print db.Get('hello') world >>> db.Delete('hello') >>> db.Get('hello') Traceback (most recent call last): ??File?"<stdin>", line 1,?in?<module> KeyError >>>?for?i?in?xrange(10): ...?? db.Put(str(i),?'string_%s'?% i) ... >>> print list(db.RangeIter(key_from =?'2', key_to =?'5')) [('2',?'string_2'), ('3',?'string_3'), ('4',?'string_4'), ('5',?'string_5')] >>> batch = leveldb.WriteBatch() >>>?for?i?in?xrange(1000): ...?? db.Put(str(i),?'string_%s'?% i) ... >>> db.Write(batch, sync = True)<br><br>python 3.5 將 字符串進(jìn)行編碼 如?'key'.encode() ,''.decode() <br><br>用于python3 sock通信<br> |
插入數(shù)據(jù)示例
?
| 1 2 3 4 5 6 7 | import leveldb db = leveldb.LevelDB('./db') batch = leveldb.WriteBatch() for?i?in?xrange(1000000): ...???? db.Put(str(i),'string_%s'?% i) ... db.Write(batch,sync=True) |
?
下面生成一個目錄db,里面包含若干文件:
?
?
然后簡要說下各個文件的含義:
1、CURRENT
2、LOG
3、LOCK
4、MANIFEST
?
下圖是LevelDB運(yùn)行一段時間后的存儲模型快照:內(nèi)存中的MemTable和Immutable MemTable以及磁盤上的幾種主要文件:Current文件,Manifest文件,log文件以及SSTable文件。當(dāng)然,LevelDb除了這六個主要部分還有一些輔助的文件,但是以上六個文件和數(shù)據(jù)結(jié)構(gòu)是LevelDb的主體構(gòu)成元素。
?
log文件、MemTable、SSTable文件都是用來存儲k-v記錄的,下面再說說manifest和Current文件的作用。
SSTable中的某個文件屬于特定層級,而且其存儲的記錄是key有序的,那么必然有文件中的最小key和最大key,這是非常重要的信息,Manifest 就記載了SSTable各個文件的管理信息,比如屬于哪個Level,文件名稱叫啥,最小key和最大key各自是多少。下圖是Manifest所存儲內(nèi)容的示意:
?
另外,在LevleDb的運(yùn)行過程中,隨著Compaction的進(jìn)行,SSTable文件會發(fā)生變化,會有新的文件產(chǎn)生,老的文件被廢棄,Manifest也會跟著反映這種變化,此時往往會新生成Manifest文件來記載這種變化,而Current則用來指出哪個Manifest文件才是我們關(guān)心的那個Manifest文件。
?
?
二、讀寫數(shù)據(jù)
?
?
寫操作流程:
1、順序?qū)懭氪疟Plog文件;
2、寫入內(nèi)存memtable(采用skiplist結(jié)構(gòu)實現(xiàn));
3、寫入磁盤SST文件(sorted string table files),這步是數(shù)據(jù)歸檔的過程(永久化存儲);
?
注意:
- log文件的作用是是用于系統(tǒng)崩潰恢復(fù)而不丟失數(shù)據(jù),假如沒有Log文件,因為寫入的記錄剛開始是保存在內(nèi)存中的,此時如果系統(tǒng)崩潰,內(nèi)存中的數(shù)據(jù)還沒有來得及Dump到磁盤,所以會丟失數(shù)據(jù);
- 在寫memtable時,如果其達(dá)到check point(滿員)的話,會將其改成immutable memtable(只讀),然后等待dump到磁盤SST文件中,此時也會生成新的memtable供寫入新數(shù)據(jù);
- memtable和sst文件中的key都是有序的,log文件的key是無序的;
- LevelDB刪除操作也是插入,只是標(biāo)記Key為刪除狀態(tài),真正的刪除要到Compaction的時候才去做真正的操作;
- LevelDB沒有更新接口,如果需要更新某個Key的值,只需要插入一條新紀(jì)錄即可;或者先刪除舊記錄,再插入也可;
?
?
讀操作流程:
1、在內(nèi)存中依次查找memtable、immutable memtable;
2、如果配置了cache,查找cache;
3、根據(jù)mainfest索引文件,在磁盤中查找SST文件;
?
?
舉個例子:我們先往levelDb里面插入一條數(shù)據(jù) {key="www.samecity.com" ?value="我們"},過了幾天,samecity網(wǎng)站改名為:69同城,此時我們插入數(shù)據(jù){key="www.samecity.com" ?value="69同城"},同樣的key,不同的value;邏輯上理解好像levelDb中只有一個存儲記錄,即第二個記錄,但是在levelDb中很可能存在兩條記錄,即上面的兩個記錄都在levelDb中存儲了,此時如果用戶查詢key="www.samecity.com",我們當(dāng)然希望找到最新的更新記錄,也就是第二個記錄返回,因此,查找的順序應(yīng)該依照數(shù)據(jù)更新的新鮮度來,對于SSTable文件來說,如果同時在level L和Level L+1找到同一個key,level L的信息一定比level L+1的要新。
?
?
?
?
三、SSTable文件
SST文件并不是平坦的結(jié)構(gòu),而是分層組織的,這也是LevelDB名稱的來源。
SST文件的一些實現(xiàn)細(xì)節(jié):
1、每個SST文件大小上限為2MB,所以,LevelDB通常存儲了大量的SST文件;
2、SST文件由若干個4K大小的blocks組成,block也是讀/寫操作的最小單元;
3、SST文件的最后一個block是一個index,指向每個data block的起始位置,以及每個block第一個entry的key值(block內(nèi)的key有序存儲);
4、使用Bloom filter加速查找,只要掃描index,就可以快速找出所有可能包含指定entry的block。
5、同一個block內(nèi)的key可以共享前綴(只存儲一次),這樣每個key只要存儲自己唯一的后綴就行了。如果block中只有部分key需要共享前綴,在這部分key與其它key之間插入"reset"標(biāo)識。
?
由log直接讀取的entry會寫到Level 0的SST中(最多4個文件);
當(dāng)Level 0的4個文件都存儲滿了,會選擇其中一個文件Compact到Level 1的SST中;
注意:Level 0的SSTable文件和其它Level的文件相比有特殊性:這個層級內(nèi)的.sst文件,兩個文件可能存在key重疊,比如有兩個level 0的sst文件,文件A和文件B,文件A的key范圍是:{bar, car},文件B的Key范圍是{blue,samecity},那么很可能兩個文件都存在key=”blood”的記錄。對于其它Level的SSTable文件來說,則不會出現(xiàn)同一層級內(nèi).sst文件的key重疊現(xiàn)象,就是說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"的結(jié)果。
在寫操作中,新數(shù)據(jù)總是先插入開頭的幾個Level中,開頭的這幾個Level存儲量也比較小,因此,對某條entry的修改或刪除操作帶來的性能影響就比較可控。
可見,SST采取分層結(jié)構(gòu)是為了最大限度減小插入新entry時的開銷;
?
?
Compaction操作
對于LevelDb來說,寫入記錄操作很簡單,刪除記錄僅僅寫入一個刪除標(biāo)記就算完事,但是讀取記錄比較復(fù)雜,需要在內(nèi)存以及各個層級文件中依照新鮮程度依次查找,代價很高。為了加快讀取速度,levelDb采取了compaction的方式來對已有的記錄進(jìn)行整理壓縮,通過這種方式,來刪除掉一些不再有效的KV數(shù)據(jù),減小數(shù)據(jù)規(guī)模,減少文件數(shù)量等。
LevelDb的compaction機(jī)制和過程與Bigtable所講述的是基本一致的,Bigtable中講到三種類型的compaction: minor ,major和full:
- minor Compaction,就是把memtable中的數(shù)據(jù)導(dǎo)出到SSTable文件中;
- major compaction就是合并不同層級的SSTable文件;
- full compaction就是將所有SSTable進(jìn)行合并;
LevelDb包含其中兩種,minor和major。
Minor compaction 的目的是當(dāng)內(nèi)存中的memtable大小到了一定值時,將內(nèi)容保存到磁盤文件中,如下圖:
?
immutable memtable其實是一個SkipList,其中的記錄是根據(jù)key有序排列的,遍歷key并依次寫入一個level 0 的新建SSTable文件中,寫完后建立文件的index 數(shù)據(jù),這樣就完成了一次minor compaction。從圖中也可以看出,對于被刪除的記錄,在minor compaction過程中并不真正刪除這個記錄,原因也很簡單,這里只知道要刪掉key記錄,但是這個KV數(shù)據(jù)在哪里?那需要復(fù)雜的查找,所以在minor compaction的時候并不做刪除,只是將這個key作為一個記錄寫入文件中,至于真正的刪除操作,在以后更高層級的compaction中會去做。
當(dāng)某個level下的SSTable文件數(shù)目超過一定設(shè)置值后,levelDb會從這個level的SSTable中選擇一個文件(level>0),將其和高一層級的level+1的SSTable文件合并,這就是major compaction。
我們知道在大于0的層級中,每個SSTable文件內(nèi)的Key都是由小到大有序存儲的,而且不同文件之間的key范圍(文件內(nèi)最小key和最大key之間)不會有任何重疊。Level 0的SSTable文件有些特殊,盡管每個文件也是根據(jù)Key由小到大排列,但是因為level 0的文件是通過minor compaction直接生成的,所以任意兩個level 0下的兩個sstable文件可能再key范圍上有重疊。所以在做major compaction的時候,對于大于level 0的層級,選擇其中一個文件就行,但是對于level 0來說,指定某個文件后,本level中很可能有其他SSTable文件的key范圍和這個文件有重疊,這種情況下,要找出所有有重疊的文件和level 1的文件進(jìn)行合并,即level 0在進(jìn)行文件選擇的時候,可能會有多個文件參與major compaction。
LevelDb在選定某個level進(jìn)行compaction后,還要選擇是具體哪個文件要進(jìn)行compaction,比如這次是文件A進(jìn)行compaction,那么下次就是在key range上緊挨著文件A的文件B進(jìn)行compaction,這樣每個文件都會有機(jī)會輪流和高層的level 文件進(jìn)行合并。
如果選好了level L的文件A和level L+1層的文件進(jìn)行合并,那么問題又來了,應(yīng)該選擇level L+1哪些文件進(jìn)行合并?levelDb選擇L+1層中和文件A在key range上有重疊的所有文件來和文件A進(jìn)行合并。也就是說,選定了level L的文件A,之后在level L+1中找到了所有需要合并的文件B,C,D…..等等。剩下的問題就是具體是如何進(jìn)行major 合并的?就是說給定了一系列文件,每個文件內(nèi)部是key有序的,如何對這些文件進(jìn)行合并,使得新生成的文件仍然Key有序,同時拋掉哪些不再有價值的KV 數(shù)據(jù)。
?
Major compaction的過程如下:對多個文件采用多路歸并排序的方式,依次找出其中最小的Key記錄,也就是對多個文件中的所有記錄重新進(jìn)行排序。之后采取一定的標(biāo)準(zhǔn)判斷這個Key是否還需要保存,如果判斷沒有保存價值,那么直接拋掉,如果覺得還需要繼續(xù)保存,那么就將其寫入level L+1層中新生成的一個SSTable文件中。就這樣對KV數(shù)據(jù)一一處理,形成了一系列新的L+1層數(shù)據(jù)文件,之前的L層文件和L+1層參與compaction 的文件數(shù)據(jù)此時已經(jīng)沒有意義了,所以全部刪除。這樣就完成了L層和L+1層文件記錄的合并過程。
那么在major compaction過程中,判斷一個KV記錄是否拋棄的標(biāo)準(zhǔn)是什么呢?其中一個標(biāo)準(zhǔn)是:對于某個key來說,如果在小于L層中存在這個Key,那么這個KV在major compaction過程中可以拋掉。因為我們前面分析過,對于層級低于L的文件中如果存在同一Key的記錄,那么說明對于Key來說,有更新鮮的Value存在,那么過去的Value就等于沒有意義了,所以可以刪除。
?
?
?
?
四、Cache
前面講過對于levelDb來說,讀取操作如果沒有在內(nèi)存的memtable中找到記錄,要多次進(jìn)行磁盤訪問操作。假設(shè)最優(yōu)情況,即第一次就在level 0中最新的文件中找到了這個key,那么也需要讀取2次磁盤,一次是將SSTable的文件中的index部分讀入內(nèi)存,這樣根據(jù)這個index可以確定key是在哪個block中存儲;第二次是讀入這個block的內(nèi)容,然后在內(nèi)存中查找key對應(yīng)的value。
LevelDb中引入了兩個不同的Cache:Table Cache和Block Cache。其中Block Cache是配置可選的,即在配置文件中指定是否打開這個功能。
?
如上圖,在Table Cache中,key值是SSTable的文件名稱,Value部分包含兩部分,一個是指向磁盤打開的SSTable文件的文件指針,這是為了方便讀取內(nèi)容;另外一個是指向內(nèi)存中這個SSTable文件對應(yīng)的Table結(jié)構(gòu)指針,table結(jié)構(gòu)在內(nèi)存中,保存了SSTable的index內(nèi)容以及用來指示block cache用的cache_id ,當(dāng)然除此外還有其它一些內(nèi)容。
比如在get(key)讀取操作中,如果levelDb確定了key在某個level下某個文件A的key range范圍內(nèi),那么需要判斷是不是文件A真的包含這個KV。此時,levelDb會首先查找Table Cache,看這個文件是否在緩存里,如果找到了,那么根據(jù)index部分就可以查找是哪個block包含這個key。如果沒有在緩存中找到文件,那么打開SSTable文件,將其index部分讀入內(nèi)存,然后插入Cache里面,去index里面定位哪個block包含這個Key 。如果確定了文件哪個block包含這個key,那么需要讀入block內(nèi)容,這是第二次讀取。
?
?
Block Cache是為了加快這個過程的,其中的key是文件的cache_id加上這個block在文件中的起始位置block_offset。而value則是這個Block的內(nèi)容。
如果levelDb發(fā)現(xiàn)這個block在block cache中,那么可以避免讀取數(shù)據(jù),直接在cache里的block內(nèi)容里面查找key的value就行,如果沒找到呢?那么讀入block內(nèi)容并把它插入block cache中。levelDb就是這樣通過兩個cache來加快讀取速度的。從這里可以看出,如果讀取的數(shù)據(jù)局部性比較好,也就是說要讀的數(shù)據(jù)大部分在cache里面都能讀到,那么讀取效率應(yīng)該還是很高的,而如果是對key進(jìn)行順序讀取效率也應(yīng)該不錯,因為一次讀入后可以多次被復(fù)用。但是如果是隨機(jī)讀取,您可以推斷下其效率如何。
?
?
?
五、版本控制
在Leveldb中,Version就代表了一個版本,它包括當(dāng)前磁盤及內(nèi)存中的所有文件信息。在所有的version中,只有一個是CURRENT(當(dāng)前版本),其它都是歷史版本。
當(dāng)執(zhí)行一次compaction 或者 創(chuàng)建一個Iterator后,Leveldb將在當(dāng)前版本基礎(chǔ)上創(chuàng)建一個新版本,當(dāng)前版本就變成了歷史版本。
?
VersionSet?是所有Version的集合,管理著所有存活的Version。
VersionEdit?表示Version之間的變化,相當(dāng)于delta 增量,表示有增加了多少文件,刪除了文件:
Version0 + VersionEdit --> Version1 Version0->Version1->Version2->Version3?
VersionEdit會保存到MANIFEST文件中,當(dāng)做數(shù)據(jù)恢復(fù)時就會從MANIFEST文件中讀出來重建數(shù)據(jù)。
Leveldb的這種版本的控制,讓我想到了雙buffer切換,雙buffer切換來自于圖形學(xué)中,用于解決屏幕繪制時的閃屏問題,在服務(wù)器編程中也有用處。
比如我們的服務(wù)器上有一個字典庫,每天我們需要更新這個字典庫,我們可以新開一個buffer,將新的字典庫加載到這個新buffer中,等到加載完畢,將字典的指針指向新的字典庫。
Leveldb的version管理和雙buffer切換類似,但是如果原version被某個iterator引用,那么這個version會一直保持,直到?jīng)]有被任何一個iterator引用,此時就可以刪除這個version。
?
學(xué)習(xí)參看
http://blog.csdn.net/qq112928/article/details/21172841
http://www.cnblogs.com/chenny7/p/4026447.html
http://www.cnblogs.com/haippy/archive/2011/12/04/2276064.html
創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎勵來咯,堅持創(chuàng)作打卡瓜分現(xiàn)金大獎總結(jié)
- 上一篇: tomcat web.xml配置
- 下一篇: ES 在数据量很大的情况下如何提高查询效