levelDB源碼學習總結

關於leveldb框架的介紹,已經有無數個大佬進行過詳細論述了。以下三篇博客就很不錯,本文的圖片也基本摘自第一篇博客:

LevelDB原理探究與代碼分析

Leveldb二三事

leveldb實現原理

本文爲看完以上博客後總結的一些問題,在雄神的鼎力相助下一起閱讀源碼找到的答案,歡迎交流指正。

目錄

一、log日誌文件相關

二、跳錶相關(memtable)

三、sstable相關

四、manifest文件相關


一、log日誌文件相關

1、Log日誌文件結構?

 

2、如何實現的log日誌順序讀寫?

       從文件的角度,數據庫操作被順序寫入的log文件;從操作系統的角度,linux 文件系統ext4會將文件分散在整個磁盤,在文件之間留有大量的自由空間,而不是像Windows那樣將文件一個接一個的放置。當一個文件被編輯了並且變大了,一般都會有足夠的自由空間來保存文件。如果碎片真的產生了,文件系統就會嘗試在日常使用中將文件移動來減少碎片,所以不需要專門的碎片整理程序。同時log文件的大小是有大小限制的,最大爲1M,這樣就可以儘量保證一個log文件在一塊連續的區間,而讀寫操作使用了內存映射的系統調用。

 

3、Down機恢復如何實現?

       每次打開一個DB的時候,首先根據當前manifest文件恢復信息到當前version(version提供對各文件的操作方法),從而獲取包含down機後沒有dump進disk的操作的log文件,再根據這些log文件重構memtable,並將其dump到磁盤中進行compaction操作,這時就保證了down機時丟失數據的恢復。恢復過程中新建了一個version,遍歷db目錄下所有文件,將文件元信息寫入manifest,再將current指向manifest文件。

 

4、什麼擴容機制?

       如果前一次映射的空間已寫滿,則先將文件擴展一定的長度(每次擴展的長度按64KB,128KB,...的順序逐次翻倍,最大到1MB),然後映射到內存,對映射的內存再以32KB的Page進行切分,每次寫入的日誌填充到Page中,攢積一定量後Sync到磁盤上

 

5、新老日誌文件如何更換?

       Leveldb有自己的垃圾回收機制,當觸發時會刪除過期文件(包括log文件)。觸發時機有三:

       a、將immemtable轉化爲sstable觸發leveldb的垃圾回收機制。

       b、後臺compaction時觸發垃圾回收機制。

       c、open一個db,將db恢復後觸發垃圾回收機制。

 

二、跳錶相關(memtable)

1、leveldb中跳錶最大幾層?

       12。

 

2、插入元素到跳錶第n層的概率?

       每上一層的概率爲25%。

 

3、跳錶中成員(即memtable的record)的結構?

 

4、跳錶中的record和log中的content和sstable中的record有何區別?

       與log中的content的結構都不同,而與sstable中的record內容順序基本相同,這裏的sequence Num實際就在sstable中record中的nonshared Key裏面。

 

三、sstable相關

1、.sst文件結構?

       實際上這裏的Record中Key存儲了Internal Key中的所有元素,通過sstable的迭代器(雙重迭代器,遍歷index block,然後遍歷date block返回一個record)獲取到的實際上是已經合併好的Key(shared + nonshared)。此外,一個sst文件內部的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”;

 

2、sstable中的每個.sst文件如何實現的存在磁盤連續空間?

       sst文件是將一個個block順序append的,從而實現磁盤順序寫,而不像其他B+樹型數據庫,需要將幾個K/V插入到不同文件位置中去。

 

3、compaction操作何時觸發?

(1)memtable會compact到sst中的觸發條件基本可以認爲是memtable大於4M(還有條件比如要求沒有達到IO文件的硬上限,這類條件一般都是滿足的)。

(2)對於level n(n>0)的文件來說,則超過指定字節數就進行壓縮。

(3)對於level 0的文件來說,超過4(可以在config中修改該數值)個文件即進行壓縮(level 0的文件之間,key可能是交叉重疊的,因此不希望level 0的文件數特別多。考慮write buffer 比較小的時候,如果使用size來限制,那麼level 0的文件數可能太多,另一個方面,如果write buffer過大,使用固定大小的size 來限制level 0的話,可能算出來的level 0的文件數又太少,觸發 level 0 compaction的情況發生的又太頻繁。因此level 0 走了一個特殊);

(4)當某個文件查找次數過多,也會觸發compaction操作。那麼查找次數多少算多呢,該文件size/16k(因爲查找操作需要I/O1M文件,compaction操作一般需要執行25M I/O操作,所以查找25次等於1次compaction操作,即查找一次與compaction40K文件成本相當,作者比較保守,就選擇了size/16k)。

 

4、如何compact?

compact分爲兩種:

(1)memtable到sst的compact:此時首先將memtable順序寫入一個sst文件,然後挑出一個合適的level放入(不一定是level0,若level0,level1都沒有與該sst重複,而level2與該sst重複了,則放入level1中)。

(2)sst之間的合併 首先需要挑出本層要與下一層進行合併的文件,這個文件如何挑選呢?選擇compact_pointer(也就是manifest文件中的Type = kCompactPointer點)之後的最近文件(若沒有或compact_pointer爲空,就挑第一個文件),若Level0則再挑所有與選出的文件重疊的所有文件。那麼compact_pointer怎麼選的呢?記錄上次上一層與該層文件compaction的範圍的最大key,即爲compact_pointer。然後將挑選出來要compact的文件們(這個過程是動態的,可能挑選了兩次才進行了compact ,也可能是level0挑出來的多個文件)逐個與下一層的文件們多路歸併,最後記錄到version中。至於歸併的過程,實際使用的是一個複合迭代器(要合併的各文件各自有table::iterator迭代器,每次走向next時,首先將最小的那個迭代器next,然後再選出當前最小的一個爲當前複合迭代器的key,實際也就是歸併的方式。至於各sst文件各自的table::iterator,則是用兩個Block::iter(index block和date block的)實現的,先走index block的,再根據offset走date block的。關於迭代器的內容具體可看levelDB之iterator總結),通過該複合迭代器遍歷,並判斷丟棄掉老數據,即完成了歸併。

 

5、查找一對key/value時如何快速定位到相關文件,具體實現?

       這是Version中的函數,Version中含有sstable的元信息,故其可以對內存中的sstable元信息(FileMetaData,記錄了一個sstable的number,最小最大key)進行操作,通過二分找到相應文件,然後先在cache中查找有沒有該K/V,若沒有,則open之前二分找到的文件,然後二分查找index block中的key,然後通過該index block中的offset來找到record。

 

6、Leveldb中的bloom filter如何實現?

       首先,對於Bloom filter參數的研究有以下結論:錯誤率不大於є的情況下,m最好要等於n log2(1/є)才能表示任意n個元素的集合在哈希函數的個數取到最優時,要讓錯誤率不超過є,m至少需要取到最小值的1.44倍;k = ln2· (m/n)時取得最優的哈希函數的個數其中m是位數,n是K/V數,k是哈希函數數。具體證明見Bloom Filter概念和原理Leveldb中,m/n是用戶的一個輸入,一般取10,由上面計算可知此時錯誤率略小於1%。此時關於bloom filter還有一個問題,就是如何實現k個哈希函數呢?Leveldb中使用了double-hashing方法,先將key哈希成一個uint數,然後再將其加一個delta,加k次,就有k個哈希結果了,具體代碼如下:

    //從這裏就是bloom過濾器的實現了

    uint32_t h = BloomHash(keys[i]);//就是一個hash將字符串轉換成一個unit32(這個值基本不會相同)

    const uint32_t delta = (h >> 17) | (h << 15);  // Rotate right 17 bits

    for (size_t j = 0; j < k_; j++) {

        const uint32_t bitpos = h % bits;//hash 函數   

        array[bitpos/8] |= (1 << (bitpos % 8));

        h += delta;(每次將h+delta,這樣就有k個初始值了)

    }

最後的結果以filter block的形式存在sst文件中的Meta Block中。

 

7、Leveldb中的cache如何實現?

       Leveldb中的cache基本可以說就是一個最基礎的LRU算法實現,主要用到了雙向鏈表、哈希表和LRU(least recently used)思想。LRUCache維護了一個雙向循環鏈表lru_和一個hash表table,當要插入一個元素時,首先將其插入到鏈表lru的尾部,然後根據hash值將其插入到hash表中。當hash表中已存在hash值與要插入元素的hash值相同的元素時,將原有元素從鏈表中移除,這樣就可以保證最近使用的元素在鏈表的最尾部,這也意味着最近最少使用的元素在鏈表的頭部,這樣即可實現LRU的思想。除了LRUCache,leveldb中還有ShardedLRUCache類(用於線程安全的cache),由於要保證線程安全,經常會出現有的線程需要等待鎖而阻塞的情況,這樣會導致效率降低,所以該類中包含了16個LRUCache對象,每次取出插入的LRUHandle中的hash值的高4位獲取數字i,並將該LRUHandle插入第i個LRUCache中,這樣就減少了線程間等待鎖的開銷。

 

四、manifest文件相關

1、manifest文件結構?

最前面的Type是後面字段的類型。

 

2、manifest文件作用?

      manifest文件是一個清單文件,記錄了當前數據庫的信息。SSTable中的某個文件屬於特定層級而且其存儲的記錄是key有序的那麼必然有文件中的最小key和最大key,這是非常重要的信息,LevelDb應該記下這些信息。Manifest就是幹這個的它記載了SSTable各個文件的管理信息比如屬於哪個Level,文件名稱叫啥最小key和最大key各自是多少。在數據庫重啓時,會遍歷所有文件,將各文件的元信息記錄在manifest文件中,然後LevleDb的運行過程中隨着Compaction的進行,SSTable文件會發生變化會有新的文件產生老的文件被廢棄,Manifest也會跟着反映這種變化此時內存中往往會新生成versionEdit來記載這種變化,manifest文件也會記錄這種變化(即重啓數據庫後,manifest文件不會再遍歷文件,而只是記錄之後文件的變化,如NewFile,DeleteFile)。因此,manifest文件只有在重啓leveldb或用戶修復leveldb的時候纔會替換。

 

3、Manifest文件內容如何更新?

       以log的機制更新的。

 

4、manifest文件何時如何更換?

       manifest文件只有在重啓leveldb或用戶修復leveldb的時候纔會替換。

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章