深刻 LevelDB 數據文件 SSTable 的結構

LevelDB 的鍵值對內容都存儲在擴展名爲 sst 的 SSTable 文件中,SSTable 的磁盤文件結構比較複雜,讀者在閱讀本節以前要作好心理準備。若是有任何看得不明白的地方,必定要在下方的問答區及時提問。 算法

圖片
SSTable 文件的內容分爲 5 個部分,Footer、IndexBlock、MetaIndexBlock、FilterBlock 和 DataBlock。其中存儲了鍵值對內容的就是 DataBlock,存儲了布隆過濾器二進制數據的是 FilterBlock,DataBlock 有多個,FilterBlock 也能夠有多個,可是一般最多隻有 1 個,之因此設計成多個是考慮到擴展性,也許將來會支持其它類型的過濾器。另外 3 個部分爲管理塊,其中 IndexBlock 記錄了 DataBlock 相關的元信息,MetaIndexBlock 記錄了過濾器相關的元信息,而 Footer 則指出 IndexBlock 和 MetaIndexBlock 在文件中的偏移量信息,它是元信息的元信息,它位於 sstable 文件的尾部。下面咱們至頂向下挨個分析每一個結構

Footer 結構

它的佔用空間很小隻有 48 字節,內部只存了幾個字段。下面咱們用僞代碼來描述一下它的結構數據庫

// 定義了數據塊的位置和大小
struct BlockHandler {
  varint offset;
  varint size;
}

struct Footer {
  BlockHandler metaIndexHandler;  // MetaIndexBlock的文件偏移量和長度
  BlockHandler indexHandler; // IndexBlock的文件偏移量和長度
  byte[n] padding;  // 內存墊片
  int32 magicHighBits;  // 魔數後32位
  int32 magicLowBits; // 魔數前32位
}
複製代碼

Footer 結構的中間部分增長了內存墊片,其做用就是將 Footer 的空間撐到 48 字節。結構的尾部還有一個 64位的魔術數字 0xdb4775248b80fb57,若是文件尾部的 8 字節不是這個數字說明文件已經損壞。這個魔術數字的來源頗有意思,它是下面返回的字符串的前64bit。數組

$ echo http://code.google.com/p/leveldb/ | sha1sum
db4775248b80fb57d0ce0768d85bcee39c230b61
複製代碼

IndexBlock 和 MetaIndexBlock 都只有惟一的一個,因此分別使用一個 BlockHandler 結構來存儲偏移量和長度。bash

Block 結構

除了 Footer 以外,其它部分都是 Block 結構,在名稱上也都是以 Block 結尾。所謂的 Block 結構是指除了內部的有效數據外,還會有額外的壓縮類型字段和校驗碼字段。app

struct Block {
  byte[] data;
  int8 compressType;
  int32 crcValue;
}
複製代碼

每個 Block 尾部都會有壓縮類型和循環冗餘校驗碼(crcValue),這會要佔去 5 字節。若是是壓縮類型,塊內的數據 data 會被壓縮。校驗碼會針對壓縮和的數據和壓縮類型字段一塊兒計算循環冗餘校驗和。壓縮算法默認是 snappy ,校驗算法是 crc32。優化

crcValue = crc32(data, compressType)
複製代碼

在下面介紹的全部 Block 結構中,咱們再也不說起壓縮和校驗碼。ui

DataBlock 結構

DataBlock 的大小默認是 4K 字節(壓縮前),裏面存儲了一系列鍵值對。前面提到 sst 文件裏面的 Key 是有序的,這意味着相鄰的 Key 會有很大的機率有共同的前綴部分。正是考慮到這一點,DataBlock 在結構上作了優化,這個優化能夠顯著減小存儲空間。google

Key = sharedKey + unsharedKey
複製代碼

Key 會劃分爲兩個部分,一個是 sharedKey,一個是 unsharedKey。前者表示相對基準 Key 的共同前綴內容,後者表示相對基準 Key 的不一樣後綴部分。 spa

圖片
好比基準 Key 是 helloworld,那麼 hellouniverse 這個 Key 相對於基準 Key 來講,它的 sharedKey 就是 hello,unsharedKey 就是 universe。
圖片
DataBlock 中存儲的是連續的一系列鍵值對,它會每隔若干個 Key 設置一個基準 Key。基準 Key 的特色就是它的 sharedKey 部分是空串。基準 Key 的位置,也就是它在塊中的偏移量咱們稱之爲「重啓點」RestartPoint,在 DataBlock 中會記錄全部「重啓點」位置。第一個「重啓點」的位置是零,也就是 DataBlock 中的第一個 Key。

struct Entry {
  varint sharedKeyLength;
  varint unsharedKeyLength;
  varint valueLength;
  byte[] unsharedKeyContent;
  byte[] valueContent;
}

struct DataBlock {
  Entry[] entries;
  int32 [] restartPointOffsets;
  int32 restartPointCount;
}
複製代碼

DataBlock 中基準 Key 是默認每隔 16 個 Key 設置一個。從節省空間的角度來講,這並非一個智能的策略。好比連續 26 個 Key 僅僅是最後一個字母不一樣,DataBlock 卻每隔 16 個 Key 強制「重啓」,這明顯不是最優的。這同時也意味着 sharedKey 是空串的 Key 未必就是基準 Key。設計

一個 DataBlock 的默認大小隻有 4K 字節,因此裏面包含的鍵值對數量一般只有幾十個。若是單個鍵值對的內容太大一個 DataBlock 裝不下咋整?

這裏就必須糾正一下,DataBlock 的大小是 4K 字節,並非說它的嚴格大小,而是在追加完最後一條記錄以後發現超出了 4K 字節,這時就會再開啓一個 DataBlock。這意味着一個 DataBlock 能夠大於 4K 字節,若是 value 值很是大,那麼相應的 DataBlock 也會很是大。DataBlock 並不會將同一個 Value 值分塊存儲。

FilterBlock 結構

若是沒有開啓布隆過濾器,FilterBlock 這個塊就是不存在的。FilterBlock 在一個 SSTable 文件中能夠存在多個,每一個塊存放一個過濾器數據。不過就目前 LevelDB 的實現來講它最多隻能有一個過濾器,那就是布隆過濾器。

布隆過濾器用於加快 SSTable 磁盤文件的 Key 定位效率。若是沒有布隆過濾器,它須要對 SSTable 進行二分查找,Key 若是不在裏面,就須要進行屢次 IO 讀才能肯定,查完了才發現原來是一場空。布隆過濾器的做用就是避免在 Key 不存在的時候浪費 IO 操做。經過查詢布隆過濾器能夠一次性知道 Key 有沒有可能在裏面。

圖片
單個布隆過濾器中存放的是一個定長的位圖數組,該位圖數組中存放了若干個 Key 的指紋信息。這若干個 Key 來源於 DataBlock 中連續的一個範圍。FilterBlock 塊中存在多個連續的布隆過濾器位圖數組,每一個數組負責指紋化 SSTable 中的一部分數據。

struct FilterEntry {
  byte[] rawbits;
}

struct FilterBlock {
  FilterEntry[n] filterEntries;
  int32[n] filterEntryOffsets;
  int32 offsetArrayOffset;
  int8 baseLg;  // 分割係數
}
複製代碼

其中 baseLg 默認 11,表示每隔 2K 字節(2<<11)的 DataBlock 數據(壓縮後),就開啓一個布隆過濾器來容納這一段數據中 Key 值的指紋。若是某個 Value 值過大,以致於超出了 2K 字節,那麼相應的布隆過濾器裏面就只有 1 個 Key 值的指紋。每一個 Key 對應的指紋空間在打開數據庫時指定。

// 每一個 Key 佔用 10bit 存放指紋信息
options.SetFilterPolicy(levigo.NewBloomFilter(10))
複製代碼

這裏的 2K 字節的間隔是嚴格的間隔,這樣才能夠經過 DataBlock 的偏移量和大小來快速定位到相應的布隆過濾器的位置 FilterOffset,再進一步得到相應的布隆過濾器位圖數據。

至於爲何 LevelDB 的布隆過濾器數據不是整個塊而是分紅一段一段的,這個緣由筆者也沒有徹底整明白。期待有讀者能夠提供思路。

MetaIndexBlock 結構

MetaIndexBlock 存儲了前面一系列 FilterBlock 的元信息,它在結構上和 DataBlock 是同樣的,只不過裏面 Entry 存儲的 Key 是帶固定前綴的過濾器名稱,Value 是對應的 FilterBlock 在文件中的偏移量和長度。

key = "filter." + filterName
// value 定義了數據塊的位置和大小
struct BlockHandler {
  varint offset;
  varint size;
}
複製代碼

就目前的 LevelDB,這裏面最多隻有一個 Entry,那麼它的結構很是簡單,以下圖所示

圖片

IndexBlock 結構

它和 MetaIndexBlock 結構同樣,也存儲了一系列鍵值對,每個鍵值對存儲的是 DataBlock 的元信息,SSTable 中有幾個 DataBlock,IndexBlock 中就有幾個鍵值對。鍵值對的 Key 是對應 DataBlock 內部最大的 Key,Value 是 DataBlock 的偏移量和長度。不考慮 Key 之間的前綴共享,不考慮「重啓點」,它的結構以下圖所示

圖片

SSTable 的結構就講到這裏,下一節咱們繼續觀察日誌文件的結構

相關文章
相關標籤/搜索