上篇文章初識:LevelDB介紹了啥是LevelDB,LevelDB有啥特性,以及Linux環境下編譯,使用及調試方法。html
這篇文章的話,算是LevelDB源碼學習的開端吧,主要講下LevelDB的源碼結構及LevelDB官方給出一些幫助文檔內容,對於我我的來講,我感受搞懂一門技術,不能直接陷到最層源碼實現,而是先了解其設計原理,而後對照學習底層源碼時纔不會頭昏腦脹~git
LevelDB源碼下載地址:https://github.com/google/leveldb.git。github
leveldb-1.22 - cmake - db LevelDB底層核心代碼目錄,諸如跳錶,版本,MemTable等實現都在該目錄下 - doc LevelDB的幫助文檔 - helpers - include LevelDB使用時須要引入的頭文件 - issues - port LevelDB底層是接入不一樣的文件系統的,這個目錄主要是爲了配置不一樣的文件系統平臺的 - table LevelDB底層MemTable相關的諸如合併,遍歷,布隆過濾器等相關實現源碼 - util LevelDB底層實現中的一些工具類,例如hash,status等 - ...
上面對LevelDB源碼的目錄結構作了基本介紹,源碼嘛,先不着急看,咱們先來看看LevelDB官方給出了哪些幫助文檔。算法
doc
目錄下是LevelDB提供給咱們的一些幫助文檔,以下圖所示。shell
leveldb-1.22 - doc - bench - benchmark.html # 這兩個文件呢,是LevelDB與SQLite等KV存儲的性能對比,有興趣的本身去看吧 - impl.md # 這個文件主要講LevelDB底層實現原理,磁盤上存儲的文件及大合併設計原理等 - index.md # 這個文件主要講LevelDB基本API的使用方法 - log_format.md # 這個文件主要講LevelDB的日誌文件格式 - table_format.md # 這個文件主要講LevelDB底層排序表的格式
接下來四部份內容依次對應doc
目錄中後四部分,第一部分性能對比有興趣本身看吧~數據庫
LevelDB基本上是高度復刻了BigTable中的Tablet的,具體Tablet是啥樣子,能夠參考初識:BigTable,裏面挺詳細的,不清楚的小夥伴能夠先去看下這篇文章。數組
儘管LevelDB高度復刻了Tablet的設計,然而,在底層文件組織中,仍是與Tablet存在一些不一樣的。數據結構
對於數據庫管理系統來講,每一個數據庫最終都是與某個目錄下的一組文件對應的,對於LevelDB來講,每一個數據庫文件目錄下的文件大體分爲日誌(Log)文件,排序表(Sorted Table)文件,Manifest文件,Current文件等等。app
日誌(Log)文件中存儲一系列順序的更新操做,每次更新操做都會被追加到當前日誌文件中。工具
當日志文件大小達到預約的大小(默認配置爲4MB)時,日誌文件會被轉換爲排序表(Sorted Table),同時建立新的日誌文件,後續的更新操做會追加到新的日誌文件中。
當前日誌(Log)文件的拷貝會在內存中以跳錶的數據結構形式(稱爲MemTable)保存,每次讀取操做都會先查詢該內存中的MemTable,以便全部的讀操做都拿到的是最新更新的數據。
LevelDB中,每一個排序表都存儲一組按Key排序的KV鍵值對。每一個鍵值對中要麼存儲的是Key的Value,要麼存儲的是Key的刪除標記(刪除標記主要用來隱藏以前舊的排序表中的過時Key)。
排序表(Sorted Tables)以一系列層級(Level)形式組織,由日誌(Log)生成的排序表(Sorted Table)會被放在一個特殊的年輕(young)層級(Level 0),年輕(young)層級的文件數量超過某個閾值(默認爲4個)時,全部年輕(young)層級的文件與Level 1層級重疊的全部文件合併在一塊兒,生成一系列新的Level 1層級文件(默認狀況下,咱們將每2MB的數據生成一個新的Level 1層級的文件)。
年輕層級(Level 0)的文件可能存在重疊的Key,可是,其餘級別的每一個文件的Key範圍都是非重疊的。
對於Level L(L>=1)層級的文件,當L層級的合併文件大小超過10^LMB(即Level爲10MB, Level2爲100MB...時進行合併。
將Level L層的一個文件file{L}與Level L+1層中全部與文件file{L}存在衝得的文件合併爲Level L+1層級的一組新文件,這些合併過程會組件將最近的key更新操做與層級最高的文件經過批量讀寫的方式進行,優勢在於,這種方式能夠最大程度地減小昂貴的磁盤尋道操做。
單詞Manifest
本意是指清單,文件列表的意思,這裏指的是每一個Level中包含哪些排序表文件清單。
Manifest文件會列出當前LevelDB數據庫中每一個Level包含哪些排序表(Sorted Table),以及每一個排序表文件包含的Key的範圍以及其餘重要的元數據。
只要從新打開LevelDB數據庫,就回自動從新建立一個新的Manifest文件(文件名後綴使用新的編號)。
注意:Manifest會被格式化爲log文件,LevelDB底層文件發生改變(添加/新建文件)時會自動追加到該log中。
當前(CURRENT)文件是一個文本文件,改文件中只有一個文件名(最近生成的Manifest的文件名)。
Informational messages are printed to files named LOG and LOG.old.
在某些特定場景下,LevelDB也會建立一些特定的文件(例如,LOCK, *.dbtmp等)。
當日志(Log)文件大小增長到特定值(默認爲4MB)時,LevelDB會建立新的MemTable和日誌(Log)文件,而且後續用戶寫入的數據及更新操做都會直接寫到新的MemTable和Log中。
後臺主要完成的工做:
當Level L層級的大小超過限制時,LevelDB會在後臺進行大合併。
大合併會從Level L選擇一個文件file{L},假設文件file{L}的key範圍爲[keyMin, keyMax],LevelDB會從Level L+1層級選擇在文件file{L}的key範圍內的全部文件files{L+1}與文件file{L}進行合併。
注意:若是Level L層級中文件file{L}僅僅與Level L+1層中某個文件的一部分key範圍重疊,則須要將L+1層級中的整個文件做爲合併的輸入文件之一進行合併,並在合併以後丟棄該文件。
注意:Level 0層級是特殊的,緣由在於,Level 0層級中的不一樣文件的範圍多是重疊的,這種場景下,若是0層級的文件須要合併時,則須要選擇多個文件,以免出現部分文件重疊的問題。
大合併操做會對前面選擇的全部文件進行合併,並生成一系列L+1層級的文件,當輸出的文件達到指定大小(默認大小爲2MB)時,會切換到新的L+1層級的文件中;另外,噹噹前輸出文件的key範圍能夠覆蓋到L+2層級中10個以上的文件時,也會自動切換到新的文件中;最後這條規則能夠確保之後在壓縮L+1層級的文件時不會從L+2層級中選擇過多的文件,避免一次大合併的數據量過大。
大合併結束後,舊的文件(排序表SSTable)會被丟棄掉,新文件則會繼續對外提供服務。
其實,LevelDB本身有一個版本控制系統,即便在合併過程當中,也能夠正常對外提供服務的。
特定特定層級的大合併過程會在該層級key範圍內進行輪轉,更直白點說,就是針對每一個層級,會自動記錄該層級最後一次壓縮時最大的key值,下次該層級壓縮時,會選擇該key以後的第一個文件進行壓縮,若是沒有這樣的文件,則自動回到該層級的key最小的文件進行壓縮,壓縮在該層級是輪轉的,而不是老是選第一個文件。
對於制定key,大合併時會刪除覆蓋的值;若是當前合併的層級中,該key存在刪除標記,若是在更高的層級中不存在該key,則同時會刪除該key及該key的刪除標記,至關於該key從數據庫中完全刪除了!!!
Level 0層級合併時,最多讀取該層級全部文件(默認4個,每一個1MB),最多讀取Level 1層級全部文件(默認10個,每一個大小約1MB),則對於Level 0層級合併來講,最多讀取14MB,寫入14MB。
除了特殊的Level 0層級大合併以外,其他的大合併會從L層級選擇一個2MB的文件,最壞的狀況下,須要從L+1層級中選擇12個文件(選擇10個文件是由於L+1層級文件總大小約是L層的10倍,另外兩個文件則是邊界範圍,由於層級L的文件中key範圍一般不會與層級L+1的對齊),總的來講,這些大合併最多讀取26MB,寫入26MB。
假設磁盤IO速率爲100MB(現代磁盤驅動器的大體範圍),最差的場景下,一次大合併須要約0.5秒。
假設咱們將後臺磁盤寫入速度限制在較小的範圍內,好比10MB/s,則大合併大約須要5秒,假設用戶以10MB/s的速度寫入,則咱們可能會創建大量的Level 0級文件(約50個來容納5*10MB文件),因爲每次讀取時須要合併更多的文件,則數據讀取成本會大大增長。
解決方案一:爲了解決這個問題,在Level 0級文件數量過多時,考慮增長Log文件切換閾值,這個解決方案的缺點在於,日誌(Log)文件閾值越大,保存相應MemTable所需的內存就越大。
解決方案二:當Level 0級文件數量增長時,須要人爲地下降寫入速度。
解決方案三:致力於下降很是普遍的合併的成本,將大多數Level 0級文件的數據塊以不壓縮的方式放在內存中便可,只須要考慮合併的迭代複雜度爲O(N)便可。
總的來講,方案一和方案三合起來應該就能夠知足大多數場景了。
LevelDB生成的文件大小是可配置的,配置更大的生成文件大小,能夠減小總的文件數量,不過,這種方式可能會致使較多的突發性大合併。
2011年2月4號,在ext3文件系統上進行的一個實驗結果顯示,單個文件目錄下不一樣文件數量時,執行100k次文件打開平均耗費時間結果以下:
目錄下文件數量 | 打開單個文件平均耗時時間 |
---|---|
1000 | 9 |
10000 | 10 |
100000 | 16 |
從上面的結果來看,單個目錄下,文件數量小於10000時,打開文件平均耗時差很少的,儘可能控制單個目錄下文件數量不要超過1w。
在每次執行完大合併以及數據庫恢復後,會調用DeleteObsoleteFiles()
方法,該方法會檢索數據庫,獲取數據庫中中全部的文件名稱,自動刪除全部不是CURRENT文件中的日誌(Log)文件,另外,該方法也會刪除全部未被某個層級引用的,且不是某個大合併待輸出的日誌文件。
LevelDB日誌文件是由一系列32KB文件塊(Block)構成的,惟一例外的是日誌文件中最後一個Block大小可能小於32KB。
每一個文件塊(Block)是由一系列記錄(Record)組成的,具體格式以下:
block := record* trailer? // 每一個Block由一系列Record組成 record := checksum: uint32 // type和data[]的crc32校驗碼;小端模式存儲 length: uint16 // 小端模式存儲 type: uint8 // Record的類型,FULL, FIRST, MIDDLE, LAST data: uint8[length]
注意:若是當前Block僅剩餘6字節空間,則不會存儲新的Record,由於每一個Record至少須要6字節存儲校驗及長度信息,對於這些剩餘的字節,會使用全零進行填充,做爲當前Block的尾巴。
注意:若是當前Block剩餘7字節,且用戶追加了一個數據(data
)長度非零的Record,該Block會添加類型爲FIRST的Record來填充剩餘的7個字節,並在後續的Block中寫入用戶數據。
Record目前只有四種類型,分別用數字標識,後續會新增其餘類型,例如,使用特定數字標識須要跳過的Record數據。
FULL == 1 FIRST == 2 MIDDLE == 3 LAST == 4
FULL類型Record標識該記錄包含用戶的整個數據記錄。
用戶記錄在Block邊界處存儲時,爲了明確記錄是否被分割,使用FIRST,MIDDLE,LAST進行標識。
FIRST類型Record用來標識用戶數據記錄被切分的第一個Record。
LAST類型Record用來標識用戶數據記錄被切分的最後一個Record。
MIDDLE則用來標識用戶數據記錄被切分的中間Record。
例如,假設用戶寫入三條數據記錄,長度分別以下:
Record 1 Length | Record 2 Length | Record 3 Length |
---|---|---|
1000 | 97270 | 8000 |
Record 1將會以FULL類型存儲在第一個Block中;
Record 2的第一部分數據長度爲31754字節以FIRST類型存儲在第一個Block中,第二部分數據以長度爲32761字節的MIDDLE類型存儲在第二個Block中,最易一個長度爲32761字節數據以LAST類型存儲在第三個Block中;
第三個Block中剩餘的7個字節以全零方式進行填充;
Record 3則將以Full類型存儲在第三個Block的開頭;
上述能夠說是把Record格式的老底掀了個底掉,下面給出Block的數據格式究竟是啥樣,小夥伴們很差奇嘛?趕快一塊兒瞅一眼吧
經過上圖能夠清晰的看到Block與Record之間的關係究竟是啥樣?
人間事,十有八九不如意;人間情,難有白頭不相離。
LevelDB這種日誌格式也不可能完美咯,讓咱們一塊兒來掰扯掰扯其優缺點吧~
額(⊙o⊙)…看起來,好像沒有啥缺點,O(∩_∩)O哈哈~
我的感受哈,對於日誌來講,LevelDB的這種格式問題不大,畢竟,日誌(例如,WAL)等一般存在磁盤上,通常狀況下,也會作按期清理,對系統來講,壓力不會太大,也還行,問題不大。
SSTable全稱Sorted String Table
,是BigTable,LevelDB及其衍生KV存儲系統的底層數據存儲格式。
SSTable存儲一系列有序的Key/Value鍵值對,Key/Value是任意長度的字符串。Key/Value鍵值對根據給定的比較規則寫入文件,文件內部由一系列DataBlock構成,默認狀況下,每一個DataBlock大小爲4KB,一般會配置爲64KB,同時,SSTable存儲會必要的索引信息。
每一個SSTable的格式大概是下面下面這個樣子:
<beginning_of_file> [data block 1] [data block 1] ... ... [data block N] [meta block 1] ... ... [meta block K] ===> 元數據塊 [metaindex block] ===> 元數據索引塊 [index block] ==> 索引塊 [Footer] ===> (固定大小,起始位置start_offset = filesize - sizeof(Footer)) <end_of_file>
SSTable文件中包含文件內部指針,每一個文件內部指針在LevelDB源碼中稱爲BlockHandle,包含如下信息:
offset: varint64 size: varint64 # 注意,varint64是可變長64位整數,這裏,暫時不詳細描述該類型數據的實現方式,後續再說
block_builder.cc
文件,每一個數據塊能夠以壓縮方式存儲Footer的格式大概是下面這個樣子:
metaindex_handle: char[p]; // MetaDataIndex的BlockHanlde信息 index_handle: char[q]; // DataBlockIndex的BlockHandle信息 padding: char[40-q-p]; // 全零字節填充 // (40==2*BlockHandle::kMaxEncodedLength) magic: fixed64; // == 0xdb4775248b80fb57 (little-endian)
注意:metaindex_handle和index_handle最大佔用空間爲40字節,本質上就是varint64最大佔用字節致使,後續,抽時間將varint64時再給你們好好掰扯掰扯~
上面全是文字描述,有點不是特別好懂,這裏呢,給你們看下我畫的一張圖,能夠說是很是的清晰明瞭~
每一個SSTable文件包含多個DataBlock,多個MetaBlock,一個MetaBlockIndex,一個DataBlockIndex,Footer。
Footer詳解:
Footer長度固定,48個字節,位於SSTable尾部;
MetaBlockIndex的OffSet和Size及DataBlockIndex的OffSet和Size分別組成BlockHandle類型,用於在文件中尋址MetaBlockIndex與DataBlockIndex,爲了節省磁盤空間,使用varint64編碼,OffSet與Size分別最少佔用1個字節,最多佔用10個字節,兩個BlockHandle佔用的字節數量少於40時使用全零字節進行填充,最後8個字節放置SSTable魔數。
例如,DataBlockIndex.offset==64, DataBlockIndex.size=216,表示DataBlockIndex位於SSTable的第64字節到第280字節。
DataBlock詳解:
每一個DataBlock默認配置4KB大小,一般推薦配置64KB大小。
每一個DataBlock由多個RestartGroup,RestartOffSet集合及RestartOffSet總數,Type,CRC構成。
每一個RestartGroup由K個RestartEntry組成,K能夠經過options配置,默認值爲16,每16個Key/Value鍵值對構成一個RestartGroup;
每一個RestartEntry由共享字節數,非共享字節數,Value字節數,Key非共享字節數組,Value字節數組構成;
DataBlockIndex詳解:
DataBlockIndex包含DataBlock索引信息,用於快速定位到給定Key所在的DataBlock;
DataBlockIndex包含Key/Value,Type,CRC校驗三部分,Type標識是否使用壓縮算法,CRC是Key/Value及Type的校驗信息;Key的取值是大於等於其索引DataBlock的最大Key且小於下一個DataBlock的最小Key,Value是BlockHandle類型,由變長的OffSet和Size組成。
爲何DataBlockIndex中Key不採用其索引的DataBlock的最大Key?
主要是爲了節省存儲空間,假設該Key其索引的DataBlock的最大Key是"acknowledge",下一個block最小的key爲"apple",若是DataBlockIndex的key採用其索引block的最大key,佔用長度爲len("acknowledge");採用後一種方式,key值能夠爲"ad"("acknowledge" < "ad" < "apple"),長度僅爲2,且檢索效果是同樣的。
爲何BlockHandle的offset和size的單位是字節數而不是DataBlock?
SSTable中的DataBlock大小是不固定的,儘管option中能夠指定block_size參數,但SSTable中存儲數據時,並未嚴格按照block_size對齊,因此offset和size指的是偏移字節數和長度字節數;這與Innodb中的B+樹索引block偏移有區別。主要有兩個緣由:
基於以上實現邏輯,SSTable中的每一個DataBlock主要支持兩種方式讀取存儲的Key/Value鍵值對:
給定Key,SSTable檢索流程:
不行了,再寫下去,這篇文章字數又要破萬了,寫不動了,下篇文章再說吧、先打個Log,暫時有些問題還沒講清楚,以下:
上面這些問題下篇文章再說,另外,個人每篇文章都是本身親手敲滴,圖也是本身畫的,不容許轉載的呦,有問題請私信呦~
其實,感受LevelDB裏面每一個設計細節均可以好好學習學習的,歡迎各位小夥伴私信,一塊兒討論呀~
另外,但願你們關注個人我的公衆號,更多高質量的技術文章等你來白嫖呦~~~