LevelDb是Google開源的嵌入式持久化KV 單機存儲引擎。采用LSM(Log Structured Merge)tree的形式組織持久化存儲的文件sstable。LSM會造成寫放大、讀放大的問題。
1. LevelDb特點:
1、 順序寫、隨機寫性能高,順序讀性能高,但是隨機讀性能差,適合於讀少寫多的場景中。讀場景下,可以加一層記錄級別的緩存,緩存常用的熱點數據,熱點數據淘汰算法可以選擇LRU算法。LevelDb內部有table cache\block cache,相比於記錄級別的緩存,粒度還是比較大。
2、 支持原子操作以批量處理的方式進行
3、 記錄存儲內部有序,按照Key排序,所以支持有序的范圍查詢,單點查詢是肯定支持的。
4、 范圍查詢時,支持前序遍歷和后序遍歷。
5、 支持快照讀
6、 數據自動壓縮,使用snappy壓縮算法
2. LevelDb文件視圖
內存中的MemTable和Immutable MemTable,都是由SkipList結構組織
磁盤上的幾種主要文件Current文件,Manifest文件,log文件以及SSTable文件。log文件、SSTable文件都是用來存儲k-v記錄的。SSTable文件屬於不同的Level,Level0中的數據最新鮮最熱點,隨着時間的推移,冷數據會逐步下移到高Level中(Compact操作)。
Manifest 就記載了SSTable各個文件的管理信息,格式如下
Current則用來指出哪個Manifest文件是當前LevleDb進行Compaction操作反應的那個Manifest文件,在LevleDb的運行過程中,隨着Compaction的進行,SSTable文件會發生變化,會有新的文件產生,老的文件被廢棄,Manifest也會跟着反映這種變化,此時往往會新生成Manifest文件來記載這種變化。
3.LevelDb寫入流程
1、順序寫入磁盤log文件;
2、寫入內存memtable(采用skiplist結構實現);
3、寫入磁盤SST文件(sorted string table files),這步是數據歸檔的過程(永久化存儲);
其中:
log文件的作用是是用於系統崩潰恢復而不丟失數據,假如沒有Log文件,因為寫入的記錄剛開始是保存在內存中的,此時如果系統崩潰,內存中的數據還沒有來得及Dump到磁盤,所以會丟失數據;
在寫memtable時,如果其達到check point(滿員)的話,會將其改成immutable memtable(只讀),然后等待dump到磁盤SST文件中,此時也會生成新的memtable供寫入新數據;
memtable和sstable文件中的key都是有序的,log文件的key是無序的;
LevelDB刪除操作也是插入,只是標記Key為刪除狀態,真正的刪除要到Compaction的時候才去做真正的操作;
LevelDB沒有更新接口,如果需要更新某個Key的值,只需要插入一條新紀錄即可;或者先刪除舊記錄,再插入也可;
4. LevelDb讀取流程
1、在內存中依次查找memtable、immutable memtable;
2、如果配置了cache,查找cache;
3、根據mainfest索引文件,在磁盤中查找sstable文件;
極端情況下,隨機讀取一條很冷的數據,已經被compact到了Level6層,這時候,讀取的操作要跨越多個Level查找,性能很低。如果再加一層記錄級別的緩存,可以避免這種消耗。
5. LevelDb compact流程
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 文件進行合並。
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就等於沒有意義了,所以可以刪除。
6. LevelDb 內部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進行順序讀取效率也應該不錯,因為一次讀入后可以多次被復用。但是如果是隨機讀取,效率很低。
7.LevelDb sstable文件存儲格式
整體上,sstable文件分為數據區與索引區,尾部的footer指出了meta index block與data index block的偏移與大小,data index block指出了各data block的偏移與大小,meta index block指出了各meta block的偏移與大小。
1)DataBlock:存儲Key-Value記錄,分為Data、type、CRC三部分
2)MetaBlock:暫時沒有使用
3)MetaBlock_index:記錄filter的相關信息(本文暫時沒有考慮filter)
4)IndexBlock:描述一個DataBlock,存儲着對應DataBlock的最大Key值,DataBlock在sst文件中的偏移量和大小
5)Footer :索引的索引,記錄IndexBlock和MetaIndexBlock在SSTable中的偏移量了和大小
block
邏輯上主要分為數據與重啟點。重啟點也是一個指針,指出了一些特殊的位置。data block中的key是有序存儲的,相鄰的key之間可能有重復,因此存儲時采用前綴壓縮,后一個key只存儲與前一個key不同的部分。那些重啟點指出的位置就表示該key不按前綴壓縮,而是完整存儲該key。除了減少壓縮空間之外,重啟點的第二個作用就是加速讀取。如果說data index block可以通過二分來定位具體的block,那么重啟點則可以通過二分的方法來定位具體的重啟點位置,進一步減少了需要讀取的數據。對於leveldb來講,可以通過options.block_size與options.block_restart_interval來設置block的大小與重啟點的間隔。默認data block的大小為4K。而重啟點則每隔16個key。具體的單條record的存儲格式如下圖所示。
Block格式
Record 格式
data index block
Index Block的結構與Data Block一樣,只不過每個group只包含一條記錄,即Data Block的最大Key與偏移。其實這里說最大Key並不是很准確,理論上,只要保存最大Key就可以實現二分查找,但是Level DB在這里做了個優化,它並保存最大key,而是保存一個能分隔兩個Data Block的最短Key。
如:假定Data Block1的最后一個Key為“abcdefg”,Data Block2的第一個Key為“abzxcv”,則index可以記錄Data Block1的索引key為“abd”;這樣的分割串可以有很多,只要保證Data Block1中的所有Key都小於等於此索引,Data Block2中的所有Key都大於此索引即可。這種優化縮減了索引長度,查詢時可以有效減小比較次數。
data block與meta index block、data index block都是采用block來存儲的(filter block稍微不同)。而對於block來講,其都是按(key,value)格式存儲一條條的record的。對於這些不同類型的block,其(key,value)都是什么了?現在只有一個meta block用於filter,因此meta index block中也只有一條記錄,其key是filter. + filter_policy的name。 不同block的Key、Value格式如下圖。