LevelDB,你好~

LevelDB,你好~

上篇文章初識:LevelDB介紹了啥是LevelDB,LevelDB有啥特性,以及Linux環境下編譯,使用及調試方法。html

這篇文章的話,算是LevelDB源碼學習的開端吧,主要講下LevelDB的源碼結構及LevelDB官方給出一些幫助文檔內容,對於我我的來講,我感受搞懂一門技術,不能直接陷到最層源碼實現,而是先了解其設計原理,而後對照學習底層源碼時纔不會頭昏腦脹~git

LevelDB源碼結構

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實現原理

​ LevelDB基本上是高度復刻了BigTable中的Tablet的,具體Tablet是啥樣子,能夠參考初識:BigTable,裏面挺詳細的,不清楚的小夥伴能夠先去看下這篇文章。數組

​ 儘管LevelDB高度復刻了Tablet的設計,然而,在底層文件組織中,仍是與Tablet存在一些不一樣的。數據結構

​ 對於數據庫管理系統來講,每一個數據庫最終都是與某個目錄下的一組文件對應的,對於LevelDB來講,每一個數據庫文件目錄下的文件大體分爲日誌(Log)文件,排序表(Sorted Table)文件,Manifest文件,Current文件等等。app

日誌(Log)文件

日誌(Log)文件中存儲一系列順序的更新操做,每次更新操做都會被追加到當前日誌文件中。工具

當日志文件大小達到預約的大小(默認配置爲4MB)時,日誌文件會被轉換爲排序表(Sorted Table),同時建立新的日誌文件,後續的更新操做會追加到新的日誌文件中。

當前日誌(Log)文件的拷貝會在內存中以跳錶的數據結構形式(稱爲MemTable)保存,每次讀取操做都會先查詢該內存中的MemTable,以便全部的讀操做都拿到的是最新更新的數據。

排序表(Sorted Table)

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)文件

單詞Manifest本意是指清單,文件列表的意思,這裏指的是每一個Level中包含哪些排序表文件清單。

Manifest文件會列出當前LevelDB數據庫中每一個Level包含哪些排序表(Sorted Table),以及每一個排序表文件包含的Key的範圍以及其餘重要的元數據。

只要從新打開LevelDB數據庫,就回自動從新建立一個新的Manifest文件(文件名後綴使用新的編號)。

注意:Manifest會被格式化爲log文件,LevelDB底層文件發生改變(添加/新建文件)時會自動追加到該log中。

當前(Current)文件

當前(CURRENT)文件是一個文本文件,改文件中只有一個文件名(最近生成的Manifest的文件名)。

Info Logs文件

Informational messages are printed to files named LOG and LOG.old.

其餘文件

在某些特定場景下,LevelDB也會建立一些特定的文件(例如,LOCK, *.dbtmp等)。

Level 0層級

當日志(Log)文件大小增長到特定值(默認爲4MB)時,LevelDB會建立新的MemTable和日誌(Log)文件,而且後續用戶寫入的數據及更新操做都會直接寫到新的MemTable和Log中。

後臺主要完成的工做:

  1. 將以前內存中的MemTable寫入到SSTable中
  2. 丟棄掉舊的MemTable
  3. 刪除舊的日誌(Log)文件和MemTable
  4. 添加新的SSTable到年輕(Level 0)層級

合併基本原理

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。

LevelDB數據庫重啓流程

  1. 讀取CURRENT文件中存儲的最近提交的MANIFEST文件名稱
  2. 讀取MANIFEST文件
  3. 清理過時文件
  4. 這一步能夠打開全部SSTable,不過,最好使用懶加載,避免內存佔用太高
  5. 將日誌文件轉換爲Level 0級的SSTable
  6. 開始新的寫入請求重定向到新的日誌文件中

文件垃圾回收

在每次執行完大合併以及數據庫恢復後,會調用DeleteObsoleteFiles()方法,該方法會檢索數據庫,獲取數據庫中中全部的文件名稱,自動刪除全部不是CURRENT文件中的日誌(Log)文件,另外,該方法也會刪除全部未被某個層級引用的,且不是某個大合併待輸出的日誌文件。

LevelDB日誌(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會添加類型爲FIRSTRecord來填充剩餘的7個字節,並在後續的Block中寫入用戶數據。

Record格式詳解

Record目前只有四種類型,分別用數字標識,後續會新增其餘類型,例如,使用特定數字標識須要跳過的Record數據。

FULL == 1
FIRST == 2
MIDDLE == 3
LAST == 4

FULL類型Record標識該記錄包含用戶的整個數據記錄。

用戶記錄在Block邊界處存儲時,爲了明確記錄是否被分割,使用FIRSTMIDDLELAST進行標識。

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的開頭;

Block格式詳解

上述能夠說是把Record格式的老底掀了個底掉,下面給出Block的數據格式究竟是啥樣,小夥伴們很差奇嘛?趕快一塊兒瞅一眼吧

經過上圖能夠清晰的看到BlockRecord之間的關係究竟是啥樣?

  1. LevelDB的日誌文件將用戶數據切分稱連續的大小爲32KB的Block塊;
  2. 每一個Block由連續的Log Record構成;
  3. 每一個Log Record由CRC32,Length,Type,Content總共4部分構成;

Level日誌格式優缺點

人間事,十有八九不如意;人間情,難有白頭不相離。

LevelDB這種日誌格式也不可能完美咯,讓咱們一塊兒來掰扯掰扯其優缺點吧~

LevelDB日誌格式優勢

  1. 在日誌數據從新同步時,只須要轉到下一個Block繼續掃描便可,若是Block存在有損壞,直接跳到下個Block處理便可。
  2. 當一個日誌文件的部份內容做爲記錄嵌入到另外一個日誌文件中時,不須要特殊處理便可使用。
  3. 對於須要在Block邊緣處進行拆分的應用程序(例如,MapReduce),處理時很簡單:找到下個Block邊界並跳過非FIRST/FULL類型記錄,直到找到FULLFIRST類型記錄爲止。
  4. 對於較大的記錄,不須要額外的緩衝區便可處理。

LevelDB日誌格式缺點

  1. 對於小的Record數據沒有進行打包處理,不過,這個問題能夠經過添加Record類型進行處理。
  2. 數據沒有進行壓縮,不過,這個問題一樣能夠經過添加Record類型進行處理。

額(⊙o⊙)…看起來,好像沒有啥缺點,O(∩_∩)O哈哈~

我的感受哈,對於日誌來講,LevelDB的這種格式問題不大,畢竟,日誌(例如,WAL)等一般存在磁盤上,通常狀況下,也會作按期清理,對系統來講,壓力不會太大,也還行,問題不大。

LevelDB Table Format

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位整數,這裏,暫時不詳細描述該類型數據的實現方式,後續再說
  • SSTable中的key/value鍵值對在底層文件中以有序的方式存儲在一系列DataBlock中,這些DataBlock在文件開頭處順序存儲,每一個數據塊的實現格式對應LevelDB源碼中的block_builder.cc文件,每一個數據塊能夠以壓縮方式存儲
  • DataBlock後面存儲了一系列元數據塊(MetaBlock),元數據塊格式化方式與DataBlock一致
  • MetaIndex索引塊(MetaBlockIndex),該索引塊中每項對應一個元數據塊的信息,包括元數據塊名稱及元數據塊在文件中的存儲位置信息(即前面提到的BlockHandle)
  • DataIndex索引塊(DataBlockIndex),該索引塊中每項(Entry)對應一個數據塊信息,每項信息中包含一個大於等於DataBlock中最大的Key且小於後續DataBlock中第一個Key的字符串以及該DataBlockBlockHandle信息
  • 每一個SSTable文件的尾部都是一個固定大小的Footer,該Footer包含MetaBlockIndexDataBlockIndexBlockHandle信息以及尾部魔數,中間空餘字節使用全零字節進行填充

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格式圖文詳解

上面全是文字描述,有點不是特別好懂,這裏呢,給你們看下我畫的一張圖,能夠說是很是的清晰明瞭~

每一個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偏移有區別。主要有兩個緣由:

  1. LevelDB能夠存儲任意長度的key和任意長度的value(不一樣於Innodb,限制每行數據的大小爲16384個字節),而同一個key/value鍵值對是不能跨DataBlock存儲的,極端狀況下,好比咱們的單 個 value 就很大,已經超過了 block_size,那麼這種狀況,SSTable就沒法進行存儲了。因此,一般狀況下,實際的DataBlock的大小都是要略微大於options中配置的block_size的;
  2. 若是嚴格按照block_size對齊存儲數據,必然有不少DataBlock須要經過補0的方式進行對齊,確定會浪費存儲空間;

SSTable檢索邏輯

基於以上實現邏輯,SSTable中的每一個DataBlock主要支持兩種方式讀取存儲的Key/Value鍵值對:

  1. 支持順序讀取DataBlock中全部Key/Value鍵值對
  2. 支持給定Key定位其所在的DataBlock,從而實現提升檢索效率。

給定Key,SSTable檢索流程:

遺留問題

不行了,再寫下去,這篇文章字數又要破萬了,寫不動了,下篇文章再說吧、先打個Log,暫時有些問題還沒講清楚,以下:

  • varint64究竟是怎麼實現的?
  • SSTable中的DataBlock究竟是怎麼回事?
  • LevelDB提供了哪些基礎的API?

上面這些問題下篇文章再說,另外,個人每篇文章都是本身親手敲滴,圖也是本身畫的,不容許轉載的呦,有問題請私信呦~

其實,感受LevelDB裏面每一個設計細節均可以好好學習學習的,歡迎各位小夥伴私信,一塊兒討論呀~

另外,但願你們關注個人我的公衆號,更多高質量的技術文章等你來白嫖呦~~~

相關文章
相關標籤/搜索