版本控制或元信息管理,是LevelDB中比較重要的內容。本文首先介紹其在整個LevelDB中不可替代的作用;之后從代碼結構引出其實現方式;最后由幾個主要的功能點入手詳細介紹元信息管理是如何提供不可或缺的支撐的。
作用
通過之前的博客,我們已經了解到了LevelDB整個的工作過程以及從Memtable,Log到SST文件的存儲方式。那么問題來了,LevelDB如何能夠知道每一層有哪些SST文件;如何快速的定位某條數據所在的SST文件;重啟后又是如何恢復到之前的狀態的,等等這些關鍵的問題都需要依賴元信息管理模塊。對其維護的信息及所起的作用簡要概括如下:
- 記錄Compaction相關信息,使得Compaction過程能在需要的時候被觸發;
- 維護SST文件索引信息及層次信息,為整個LevelDB的讀、寫、Compaction提供數據結構支持;
- 負責元信息數據的持久化,使得整個庫可以從進程重啟或機器宕機中恢復到正確的狀態;
- 記錄LogNumber,Sequence,下一個SST文件編號等狀態信息;
- 以版本的方式維護元信息,使得Leveldb內部或外部用戶可以以快照的方式使用文件和數據。
下面就將更詳細的進行說明。
實現
LeveDB用Version表示一個版本的元信息,Version中主要包括一個FileMetaData指針的二維數組,分層記錄了所有的SST文件信息。FileMetaData數據結構用來維護一個文件的元信息,包括文件大小,文件編號,最大最小值,引用計數等,其中引用計數記錄了被不同的Version引用的個數,保證被引用中的文件不會被刪除。除此之外,Version中還記錄了觸發Compaction相關的狀態信息,這些信息會在讀寫請求或Compaction過程中被更新。通過庖丁解LevelDB之概覽中對Compaction過程的描述可以知道在CompactMemTable和BackgroundCompaction過程中會導致新文件的產生和舊文件的刪除。每當這個時候都會有一個新的對應的Version生成,并插入VersionSet鏈表頭部。
VersionSet是一個Version構成的雙向鏈表,這些Version按時間順序先后產生,記錄了當時的元信息,鏈表頭指向當前最新的Version,同時維護了每個Version的引用計數,被引用中的Version不會被刪除,其對應的SST文件也因此得以保留,通過這種方式,使得LevelDB可以在一個穩定的快照視圖上訪問文件。VersionSet中除了Version的雙向鏈表外還會記錄一些如LogNumber,Sequence,下一個SST文件編號的狀態信息。
通過上面的描述可以看出,相鄰Version之間的不同僅僅是一些文件被刪除另一些文件被刪除。也就是說將文件變動應用在舊的Version上可以得到新的Version,這也就是Version產生的方式。LevelDB用VersionEdit來表示這種相鄰Version的差值。
為了避免進程崩潰或機器宕機導致的數據丟失,LevelDB需要將元信息數據持久化到磁盤,承擔這個任務的就是Manifest文件。可以看出每當有新的Version產生都需要更新Manifest,很自然的發現這個新增數據正好對應于VersionEdit內容,也就是說Manifest文件記錄的是一組VersionEdit值,在Manifest中的一次增量內容稱作一個Block,其內容如下:
Manifest Block := N * Item
Item := [kComparator] comparator
or [kLogNumber] 64位log_number
or [kPrevLogNumber] 64位pre_log_number
or [kNextFileNumber] 64位next_file_number_
or [kLastSequence] 64位last_sequence_
or [kCompactPointer] 32位level + 變長的key
or [kDeletedFile] 32位level + 64位文件號
or [kNewFile] 32位level + 64位 文件號 + 64位文件長度 + smallest key + largest key
可以看出恢復元信息的過程也變成了依次應用VersionEdit的過程,這個過程中有大量的中間Version產生,但這些并不是我們所需要的。LevelDB引入VersionSet::Builder來避免這種中間變量,方法是先將所有的VersoinEdit內容整理到VersionBuilder中,然后一次應用產生最終的Version,這種實現上的優化如下圖所示:
在這一節中,我們依次看到了LevelDB版本控制中比較重要的幾個角色:Version、FileMetaData、VersionSet、VersionEdit、Manifest和Version::Builder。同時了解了他們各自的作用。接下來就一起從LevelDB主要的功能點中欣賞下他們的英姿。
功能點
版本控制中維護的各種元信息,為LevelDB的各個工作流程中提供了必不可少的支持:
1,Get
我們已經知道,LevelDB嘗試獲取某個Key的值時會依次嘗試從Memtable,Immutable,SST文件中讀取。一旦需要從SST文件中讀取,就需要解決從大量文件中快速定位文件的問題。正是由于Version中記錄了當前每個文件的最大最小值,使得這個問題變成比較Key值與文件的Key Range的過程。
我們已經知道,LevelDB的寫操作會直接寫入Memtable并通過異步的Compaction過程寫入到不同層次的SST文件中,因此,上層文件擁有較新的數據,利用這個特征,LevelDB的Get接口會由上至下的依次從每一層中嘗試查找,一旦查找成功,便可以忽略下層的相同Key的記錄。
Level0層比較特殊,文件之間相互重疊無序,需要由新到舊的嘗試從每個文件中查找。其他Level,由于SST文件本身有序排列,因此可以利用二分查找快速定位Key所在文件。找到Key值所在文件后,再用庖丁解LevelDB之數據存儲中介紹的格式讀取文件中內容。
2,Compaction觸發時機
我們已經知道,LevelDB中會有后臺線程來執行Compaction的操作,將上層文件與下層文件歸并生成新的下層文件。Version中記錄的各層的文件信息來幫助決定進行Compaction的時機:
- 容量觸發Compaction:每個Version在其生成的時候會初始化兩個值compaction_level_、compaction_score_,記錄了當前Version最需要進行Compaction的Level,以及其需要進行Compaction的緊迫程度,score大于1被認為是需要馬上執行的。我們知道每次文件信息的改變都會生成新的Version,所以每個Version對應的這兩個值初始化后不會再改變。level0層compaction_score_與文件數相關,其他level的則與當前層的文件總大小相關。這種區分的必要性也是顯而易見的:每次Get操作都需要從level0層的每個文件中嘗試查找,因此控制level0的文件數是很有必要的。同時Version中會記錄每層上次Compaction結束后的最大Key值compact_pointer_,下一次觸發自動Compaction會從這個Key開始。容量觸發的優先級高于下面將要提到的Seek觸發。
- Seek觸發Compaction:Version中會記錄file_to_compact_和file_to_compact_level_,這兩個值會在Get操作每次嘗試從文件中查找時更新。LevelDB認為每次查找同樣會消耗IO,這個消耗在達到一定數量可以抵消一次Compaction操作消耗的IO,所以對Seek較多的文件應該主動觸發一次Compaction。但在引入布隆過濾器后,這種查找消耗的IO就會變得微不足道了,因此由Seek觸發的Compaction其實也就變得沒有必要了。
- 手動Compaction:LevelDB提供了外部接口CompactRange,用戶可以指定觸發某個Key Range的Compaction,LevelDB默認手動Compaction的優先級高于兩種自動觸發。
3,構造Compaction:
達到觸發條件進行Compaction操作時,會首先通過Version來構造所有本次Compaction所需要的信息,記錄在Compaction對象中,包括發生Compaction的level,所有參與的level和level+1層的文件信息,level+2層的文件信息等。 下面針對自動觸發Compaction的情況介紹,手動Compaction的過程大體類似,這個過程叫做PickCompaction。
- 獲得要Compaction的一個文件加入input_[0],容量觸發時這個文件由compaction_level_加compact_pointer_確定,否則由file_to_compact_level_和file_to_compact_確定。對于level0,由于其文件相互重合,需要將所有與當前Compaction文件重合的文件全部加入input_[0]。
- 獲得所有與level[0]有Key Range重合的level+1層文件加入input_[1],可以看出所有input_[1]文件的Key Range可能大于level[0],為了減少LevelDB整體Compaction次數,LevelDB會在不增加input_[1]文件數的前提下嘗試增加level[0]文件數來擴大level層文件的Key Range。
- 獲得所有與當前Key Range重合的level+2層文件加入input_[2],這里記錄level+2層的文件信息是為了Compaction生成新的level+1層文件時,保證新文件不會與level+2中太多的文件有Key Range的重合,從而導致以后該文件的Compaction有太大的Merge開銷,這個信息會在生成新文件的過程中不斷檢查。
- 生成歸并Iterator,接下來就是用上面收集的信息生成歸并Iterator,之后遍歷這個Iterator生成新的文件,Iterator相關的內容會在之后一篇博客詳細介紹。
4,Version持久化:
Compaction過程會造成文件的增加和刪除,這就需要生成新的Version,上面提到的Compaction對象包含本次Compaction所對應的VersionEdit,Compaction結束后這個VersionEdit會被用來構造新的VersionSet中的Version。同時為了數據安全,這個VersionEdit會被Append寫入到Manifest中。在庫重啟時,會首先嘗試從Manifest中恢復出當前的元信息狀態,過程如下:
- 依次讀取Manifest文件中的每一個Block, 將從文件中讀出的Record反序列化為VersionEdit;
- 將每一個的VersionEdit Apply到VersionSet::Builder中,之后從VersionSet::Builder的信息中生成Version;
- 計算compaction_level_、compaction_score_;
- 將新生成的Version掛到VersionSet中,并初始化VersionSet的manifest_file_number_, next_file_number_,last_sequence_,log_number_,prev_log_number_ 信息;
總結
版本控制或元信息管理,在LevelDB的各個流程中穿針引線,保證了整個數據庫的正確穩定,不可或缺。接下來的一篇博客將著重介紹,在LevelDB中出鏡率極高又十分優雅的Iterator。
參考
Source Code:https://github.com/google/leveldb
庖丁解LevelDB之概覽: http://catkang.github.io/2017/01/07/leveldb-summary.html
庖丁解LevelDB之數據管理: http://catkang.github.io/2017/01/07/leveldb-summary.html