[LevelDB] 5.SSTable

 SSTable是Bigtable中相當重要的一塊,對於LevelDB來講也是如此,對LevelDB的SSTable實現細節的瞭解也有助於瞭解Bigtable中一些實現細節。 
    本節內容主要講述SSTable的靜態佈局結構,SSTable文件造成了不一樣Level的層級結構,至於這個層級結構是如何造成的咱們放在後面Compaction一節細說。本節主要介紹SSTable某個文件的物理佈局和邏輯佈局結構,這對了解LevelDB的運行過程頗有幫助。 
  LevelDB不一樣層級都有一個或多個SSTable文件(之後綴.sst爲特徵),全部.sst文件內部佈局都是同樣的。上節介紹Log文件是物理分塊的,SSTable也同樣會將文件劃分爲固定大小的物理存儲塊Block,可是二者邏輯佈局大不相同,根本緣由是:Log文件中的記錄是Key無序的,即前後記錄的key大小沒有明確大小關係,而.sst文件內部則是根據記錄的Key由小到大排列的,從下面介紹的SSTable佈局能夠體會到Key有序是爲什麼如此設計.sst文件結構的關鍵。 算法

 
圖1 .sst文件的分塊結構 

  圖1展現了一個.sst文件的物理劃分結構,同Log文件同樣,也是劃分爲固定大小的存儲塊,每一個Block分爲三個部分,包括Block、Type和CRC。Block爲數據存儲區,Type區用於標識Block中數據是否採用了數據壓縮算法(Snappy壓縮或者無壓縮兩種),CRC部分則是Block數據校驗碼,用於判別數據是否在生成和傳輸中出錯。 
  以上是.sst的物理佈局,下面介紹.sst文件的邏輯佈局,所謂邏輯佈局,就是說盡管你們都是物理塊,可是每一塊存儲什麼內容,內部又有什麼結構等。圖4.2展現了.sst文件的內部邏輯解釋。緩存

 

圖2 邏輯佈局 

  從圖2能夠看出,從大的方面,能夠將.sst文件劃分爲數據存儲區和數據管理區,數據存儲區存放實際的Key:Value數據,數據管理區則提供一些索引指針等管理數據,目的是更快速便捷的查找相應的記錄。兩個區域都是在上述的分塊基礎上的,就是說文件的前面若干塊實際存儲KV數據,後面數據管理區存儲管理數據。管理數據又分爲四種不一樣類型:紫色的Meta Block,紅色的MetaBlock Index和藍色的Index block以及一個文件尾部塊Footer。 
  LevelDB 1.2版對於Meta Block尚無實際使用,只是保留了一個接口,估計會在後續版本中加入內容,下面咱們看看Index block和文件尾部Footer的內部結構。app

 
圖3 Index block結構 

  圖3是Index block的內部結構示意圖。再次強調一下,Data Block內的KV記錄是按照Key由小到大排列的,Index block的每條記錄是對某個Data Block創建的索引信息,每條索引信息包含三個內容:Data Block中key上限值(不必定是最大key)、Data Block在.sst文件的偏移和大小,以圖3所示的數據塊i的索引Index i來講:紅色部分的第一個字段記載大於等於數據塊i中最大的Key值的那個Key,第二個字段指出數據塊i在.sst文件中的起始位置,第三個字段指出Data Block i的大小(有時候是有數據壓縮的)。後面兩個字段好理解,是用於定位數據塊在文件中的位置的,第一個字段須要詳細解釋一下,在索引裏保存的這個Key值未必必定是某條記錄的Key,以圖3的例子來講,假設數據塊i 的最小Key=「samecity」,最大Key=「the best」;數據塊i+1的最小Key=「the fox」,最大Key=「zoo」,那麼對於數據塊i的索引Index i來講,其第一個字段記載大於等於數據塊i的最大Key(「the best」),同時要小於數據塊i+1的最小Key(「the fox」),因此例子中Index i的第一個字段是:「the c」,這個是知足要求的;而Index i+1的第一個字段則是「zoo」,即數據塊i+1的最大Key。
  文件末尾Footer塊的內部結構見圖4,metaindex_handle指出了metaindex block的起始位置和大小;inex_handle指出了index Block的起始地址和大小;這兩個字段能夠理解爲索引的索引,是爲了正確讀出索引值而設立的,後面跟着一個填充區和魔數(0xdb4775248b80fb57)。 dom

 
圖4 Footer 

  上面主要介紹的是數據管理區的內部結構,下面咱們看看數據區的一個Block的數據部份內部是如何佈局的,圖5是其內部佈局示意圖。 函數

 


圖5 Data Block內部結構 

  從圖中能夠看出,其內部也分爲兩個部分,前面是一個個KV記錄,其順序是根據Key值由小到大排列的,在Block尾部則是一些「重啓點」(Restart Point),實際上是一些指針,指出Block內容中的一些記錄位置。 
  「重啓點」是幹什麼的呢?簡單來講就是進行數據壓縮,減小存儲空間。咱們一再強調,Block內容裏的KV記錄是按照Key大小有序的,這樣的話,相鄰的兩條記錄極可能Key部分存在重疊,好比key i=「the car」,Key i+1=「the color」,那麼二者存在重疊部分「the c」,爲了減小Key的存儲量,Key i+1能夠只存儲和上一條Key不一樣的部分「olor」,二者的共同部分從Key i中能夠得到。記錄的Key在Block內容部分就是這麼存儲的,主要目的是減小存儲開銷。「重啓點」的意思是:在這條記錄開始,再也不採起只記載不一樣的Key部分,而是從新記錄全部的Key值,假設Key i+1是一個重啓點,那麼Key裏面會完整存儲「the color」,而不是採用簡略的「olor」方式。可是若是記錄條數比較多,隨機訪問一條記錄,須要從頭開始一直解析才行,這樣也產生很大的開銷,因此設置了多個重啓點,Block尾部就是指出哪些記錄是這些重啓點的。 佈局


圖6 記錄格式 

  在Block內容區,每一個KV記錄的內部結構是怎樣的?圖6給出了其詳細結構,每一個記錄包含5個字段:key共享長度,key非共享長度,value長度,key非共享內容,value內容。好比上面的「the car」和「the color」記錄,key共享長度5;key非共享長度是4;而key非共享內容則實際存儲「olor」;value長度及內容分別指出Key:Value中Value的長度和存儲實際的Value值。 
  上面講的這些就是.sst文件的所有內部奧祕。ui


 

由上圖可知,SSTable主要分爲五部分:spa

1)DataBlock:存儲Key-Value記錄,分爲Data、type、CRC三部分,其中Data部分的詳細結構見 leveldb之SSTable.net

2)MetaBlock:暫時沒有使用設計

3)MetaBlock_index:記錄filter的相關信息(本文暫時沒有考慮filter)

4)IndexBlock:描述一個DataBlock,存儲着對應DataBlock的最大Key值,DataBlock在.sst文件中的偏移量和大小

5)Footer :索引的索引,記錄IndexBlock和MetaIndexBlock在SSTable中的偏移量了和大小

一、TableBuilder

leveldb經過TableBuilder類來構建每個.sst文件,TableBuilder類的成員變量只有一個結構體Rep* rep_,Rep的結構爲:

 1 struct TableBuilder::Rep {  
 2   Options options;  
 3   Options index_block_options;  
 4   WritableFile* file;//要生成的.sst文件  
 5   uint64_t offset;  
 6   Status status;  
 7   BlockBuilder data_block;//數據區  
 8   BlockBuilder index_block;//索引  
 9   std::string last_key;//上一個插入的key值,新插入的key必須比它大,保證.sst文件中的key是從小到大排列的  
10   int64_t num_entries;//.sst文件中存儲的全部記錄總數  
11   bool closed;           
12   FilterBlockBuilder* filter_block;  
13   bool pending_index_entry;//當DataBlock爲空時,爲true  
14   BlockHandle pending_handle; //BlockHandle只有offset_和size_兩個變量,用來記錄DataBlock在.sst文件中的偏移量和大小  
15   
16   std::string compressed_output;//是否須要對DataBlock中的內容進行壓縮  
17 };  

TableBuilder與BlockBuilder相似,經過Add()函數向文件中加入一條記錄,經過Finish()來完成一個SSTable的構建。在下面的分析中,暫時不考慮filter_block

1.1 TableBuilder::Add()

經過Add()函數向一個.sst文件中加入一條記錄,主要分爲:寫index_block,寫Data_block,更新相關變量,可能完成一個DataBlock並將數據寫入磁盤

 
 1 void TableBuilder::Add(const Slice& key, const Slice& value) {  
 2   Rep* r = rep_;  
 3   if (r->num_entries > 0) {  
 4     assert(r->options.comparator->Compare(key, Slice(r->last_key)) > 0);//待插入的key值必須比ast_key大  
 5   }  
 6   
 7   if (r->pending_index_entry) {//DataBlock爲空時,爲true  
 8     assert(r->data_block.empty());  
 9     r->options.comparator->FindShortestSeparator(&r->last_key, key);  
10     std::string handle_encoding;  
11     r->pending_handle.EncodeTo(&handle_encoding);//handle_encoding記錄每一個DataBlock的偏移量和大小  
12     r->index_block.Add(r->last_key, Slice(handle_encoding));//將DataBlock的last_key、offset和size寫入到index_block  
13     r->pending_index_entry = false;//變爲false  
14   }  
15   
16   r->last_key.assign(key.data(), key.size());//更新last_key  
17   r->num_entries++;//更新記錄總數  
18   r->data_block.Add(key, value);//將key-value寫入一個DataBlock  
19   
20   const size_t estimated_block_size = r->data_block.CurrentSizeEstimate();//DataBlock的大小  
21   if (estimated_block_size >= r->options.block_size) {//當DataBlock所佔空間超過設定值(默認爲4K)時  
22     Flush();//完成一個DataBlock,並將DataBlock寫入到磁盤.sst文件中  
23   }  
24 }  

1.1.1TableBuilder::Flush()

當一個DataBlock超過設定值(默認爲4K,1個page)時,執行Flush()操做

 
  1. void TableBuilder::Flush() {  
  2.   Rep* r = rep_;  
  3.   WriteBlock(&r->data_block, &r->pending_handle);  
  4.   if (ok()) {  
  5.     r->pending_index_entry = true;  
  6.     r->status = r->file->Flush();  
  7.   }  
  8. }  

首先調用WriteBlock()寫入數據,而後對.sst文件執行fflush()將數據寫入磁盤

1.1.2TableBuilder::WriteBlock()

WriteBlock()首先調用BlockBuilder::Finish()完成一個DataBlock的建立並返回數據區的內容Slice,而後判斷是否須要進行壓縮,最後調用WriteRawBlock()寫入數據,並調用BlockBuilder::Reset()從新開始一個DataBlock

 

1.1.3TableBuilder::WriteRawBlock()

由以前對SSTable佈局的分析可知,一個.sst文件的數據區分爲三部分:DataBlock、Type和CRC

傳入三個參數:block_contents爲調用BlockBuilder::Finish()返回的數據區的內容,type爲是否壓縮,handle爲DataBlock的偏移量和大小

 1 void TableBuilder::WriteRawBlock(const Slice& block_contents,  
 2                                  CompressionType type,  
 3                                  BlockHandle* handle) {  
 4   Rep* r = rep_;  
 5   handle->set_offset(r->offset);//更新DataBlock在.sst文件中的偏移量和大小  
 6   handle->set_size(block_contents.size());  
 7   r->status = r->file->Append(block_contents);//最終會調用fwrite將數據區內容寫入到.sst文件中  
 8   if (r->status.ok()) {  
 9     char trailer[kBlockTrailerSize];  
10     trailer[0] = type;//第一個字節爲type  
11     uint32_t crc = crc32c::Value(block_contents.data(), block_contents.size());  
12     crc = crc32c::Extend(crc, trailer, 1);  // Extend crc to cover block type  
13     EncodeFixed32(trailer+1, crc32c::Mask(crc));//將CRC寫入trailer  
14     r->status = r->file->Append(Slice(trailer, kBlockTrailerSize));//將type和CRC寫入.sst文件  
15     if (r->status.ok()) {  
16       r->offset += block_contents.size() + kBlockTrailerSize;  
17     }  
18   }  
19 }  

這樣就完成了.sst文件中DataBlock的寫入了

1.2TableBuilder::Finish()

調用Finish()來完成一個SSTable的建立,主要包括前面的DataBlock,還有IndexBlock、MetaIndexBlock、Footer等

 1 Status TableBuilder::Finish() {  
 2   Rep* r = rep_;  
 3   Flush();//將數據區的內容所有寫入到SSTable中  
 4   
 5   BlockHandle filter_block_handle, metaindex_block_handle, index_block_handle;  
 6   
 7   // Write metaindex block  
 8   if (ok()) {    
 9     BlockBuilder meta_index_block(&r->options);  
10     if (r->filter_block != NULL) {//記錄filter相關信息,暫時沒有考慮  
11       // Add mapping from "filter.Name" to location of filter data  
12       std::string key = "filter.";  
13       key.append(r->options.filter_policy->Name());  
14       std::string handle_encoding;  
15       filter_block_handle.EncodeTo(&handle_encoding);  
16       meta_index_block.Add(key, handle_encoding);  
17     }  
18     WriteBlock(&meta_index_block, &metaindex_block_handle);  
19   }  
20   
21   // Write index block  
22   if (ok()) {  
23     if (r->pending_index_entry) {  
24       r->options.comparator->FindShortSuccessor(&r->last_key);  
25       std::string handle_encoding;  
26       r->pending_handle.EncodeTo(&handle_encoding);  
27       r->index_block.Add(r->last_key, Slice(handle_encoding));  
28       r->pending_index_entry = false;  
29     }  
30     WriteBlock(&r->index_block, &index_block_handle);//將indexblock中的全部數據寫入到SSTable文件中  
31   }  
32   
33   // Write footer  
34   if (ok()) {  
35     Footer footer;//footer記錄MetaIndexBlock和IndexBlock在SSTable文件中的偏移量和大小  
36     footer.set_metaindex_handle(metaindex_block_handle);  
37     footer.set_index_handle(index_block_handle);  
38     std::string footer_encoding;  
39     footer.EncodeTo(&footer_encoding);  
40     r->status = r->file->Append(footer_encoding);//將footer寫入到SSTable中  
41     if (r->status.ok()) {  
42       r->offset += footer_encoding.size();  
43     }  
44   }  
45   return r->status;  
46 }  

file->Append()最終都會調用到fwrite(),將數據寫入到磁盤中。

這樣就將數據區和數據管理區的全部內容都寫入到磁盤中的.sst文件中了

二、Table

Table類用來描述一個SSTable文件,Table類中也只有一個成員變量Rep *rep_,其結構爲:

 

[cpp]  view plain  copy
 
  1. struct Table::Rep {  
  2.   Options options;  
  3.   Status status;  
  4.   RandomAccessFile* file;//.sst文件  
  5.   uint64_t cache_id;  
  6.   FilterBlockReader* filter;  
  7.   const char* filter_data;  
  8.   
  9.   BlockHandle metaindex_handle;  // Handle to metaindex_block: saved from footer  
  10.   Block* index_block;  
  11. };  


其內容主要包括經過SSTable文件中的Footer得到的IndexBlock和MetaIndexBlock(暫時不考慮filter)

 

2.1Table::Open()

經過Open一個.sst文件將其轉換爲Table結構,由下面的代碼可知SSTable中的Footer是長度固定的,爲2*BlockHandle::kMaxEncodedLength + 8,共28字節

 
  1. void Footer::EncodeTo(std::string* dst) const {  
  2.   metaindex_handle_.EncodeTo(dst);  
  3.   index_handle_.EncodeTo(dst);  
  4.   dst->resize(2 * BlockHandle::kMaxEncodedLength);  // Padding  
  5.   PutFixed32(dst, static_cast<uint32_t>(kTableMagicNumber & 0xffffffffu));  
  6.   PutFixed32(dst, static_cast<uint32_t>(kTableMagicNumber >> 32));  
  7. }  

所以可直接從一個SSTable中找到Footer結構體,而Footer是索引的索引,其中存儲着IndexBlock和MetaIndexBlock的信息,所以能夠很方便的獲取IndexBlock。

2.2Table::InternalGet()

可經過InternalGet()來查找對應的記錄

一、首先在IndexBlock中找到目標key所在的DataBlock在SSTable文件中的偏移量和大小

二、而後根據找到的IndexBlock中的key,offset,size找到對應的DataBlock

三、DataBlock包含實際數據區、type和CRC,調用Table::ReadBlock()來從中找到實際的數據區

四、而後在數據區中對目標key進行查找

三、TableCache

前面講過對於levelDb來講,讀取操做若是沒有在內存的memtable中找到記錄,要屢次進行磁盤訪問操做。假設最優狀況,即第一次就在level 0中最新的文件中找到了這個key,那麼也須要讀取2次磁盤,一次是將SSTable的文件中的index部分讀入內存,這樣根據這個index能夠肯定key是在哪一個block中存儲;第二次是讀入這個block的內容,而後在內存中查找key對應的value。

LevelDb中引入了兩個不一樣的Cache:Table Cache和Block Cache。其中Block Cache是配置可選的,即在配置文件中指定是否打開這個功能。

 

如上圖,在Table Cache中,key值是SSTable的文件名稱,Value部分包含兩部分,一個是指向磁盤打開的SSTable文件的文件指針,這是爲了方便讀取內容;另一個是指向內存中這個SSTable文件對應的Table結構指針,table結構在內存中,保存了SSTable的index內容以及用來指示block cache用的cache_id ,固然除此外還有其它一些內容。

好比在get(key)讀取操做中,若是levelDb肯定了key在某個level下某個文件A的key range範圍內,那麼須要判斷是否是文件A真的包含這個KV。此時,levelDb會首先查找Table Cache,看這個文件是否在緩存裏,若是找到了,那麼根據index部分就能夠查找是哪一個block包含這個key。若是沒有在緩存中找到文件,那麼打開SSTable文件,將其index部分讀入內存,而後插入Cache裏面,去index裏面定位哪一個block包含這個Key 。若是肯定了文件哪一個block包含這個key,那麼須要讀入block內容,這是第二次讀取。

Block Cache是爲了加快這個過程的,其中的key是文件的cache_id加上這個block在文件中的起始位置block_offset。而value則是這個Block的內容。

若是levelDb發現這個block在block cache中,那麼能夠避免讀取數據,直接在cache裏的block內容裏面查找key的value就行,若是沒找到呢?那麼讀入block內容並把它插入block cache中。levelDb就是這樣經過兩個cache來加快讀取速度的。從這裏能夠看出,若是讀取的數據局部性比較好,也就是說要讀的數據大部分在cache裏面都能讀到,那麼讀取效率應該仍是很高的,而若是是對key進行順序讀取效率也應該不錯,由於一次讀入後能夠屢次被複用。可是若是是隨機讀取,您能夠推斷下其效率如何。

由以前對Cache的分析:leveldb之cache 可知,內存訪問效率比磁盤訪問效率要高得多,所以leveldb將經過Cache在內存中緩存最近使用到的一些文件,以提升訪問效率。.sst文件主要對應的是TableCache,經過TableCache將最近使用到的.sst文件緩存在內存中,類Table經過成員變量cache_來管理緩存文件,cache_的成員變量key對應的則是每一個SSTable的文件名。

3.1TableCache::Get()

經過Get()進行查找相應記錄

 1 Status TableCache::Get(const ReadOptions& options,  
 2                        uint64_t file_number,  
 3                        uint64_t file_size,  
 4                        const Slice& k,  
 5                        void* arg,  
 6                        void (*saver)(void*, const Slice&, const Slice&)) {  
 7   Cache::Handle* handle = NULL;  
 8   Status s = FindTable(file_number, file_size, &handle);//查找.sst文件  
 9   if (s.ok()) {  
10     Table* t = reinterpret_cast<TableAndFile*>(cache_->Value(handle))->table;  
11     s = t->InternalGet(options, k, arg, saver);//在目標.sst文件中查找目標記錄  
12     cache_->Release(handle);  
13   }  
14   return s;  
15 }  

在查找時,首先調用FindTable()來查找目標記錄所在的.sst文件,而後在.sst文件中調用InternalGet()查找目標記錄(見上面的2.2),這樣就完成了查找操做,並將.sst文件與緩存cache聯繫起來了。

3.2TableCache::FindTable()

 1 Status TableCache::FindTable(uint64_t file_number, uint64_t file_size,  
 2                              Cache::Handle** handle) {  
 3   Status s;  
 4   char buf[sizeof(file_number)];  
 5   EncodeFixed64(buf, file_number);  
 6   Slice key(buf, sizeof(buf));//.sst文件名  
 7   *handle = cache_->Lookup(key);//首先在現有的緩存中進行查找,具體實現見<a target=_blank href="http://blog.csdn.net/u012658346/article/details/45486051">leveldb之cache</a>   
 8   if (*handle == NULL) {//若是文件不存在於緩存中  
 9     std::string fname = TableFileName(dbname_, file_number);  
10     RandomAccessFile* file = NULL;  
11     Table* table = NULL;  
12     s = env_->NewRandomAccessFile(fname, &file);//打開一個.sst文件  
13     if (!s.ok()) {  
14       std::string old_fname = SSTTableFileName(dbname_, file_number);  
15       if (env_->NewRandomAccessFile(old_fname, &file).ok()) {  
16         s = Status::OK();  
17       }  
18     }  
19     if (s.ok()) {  
20       s = Table::Open(*options_, file, file_size, &table);//而後將.sst文件轉換爲Table  
21     }  
22   
23     if (!s.ok()) {  
24       assert(table == NULL);  
25       delete file;  
26     } else {  
27       TableAndFile* tf = new TableAndFile;  
28       tf->file = file;  
29       tf->table = table;  
30       *handle = cache_->Insert(key, tf, 1, &DeleteEntry);//若是此文件不在cache_中,則將其加入到緩存中  
31     }  
32   }  
33   return s;  
34 }  

四、總結

1.類TableBuilder用來寫入一個.sst文件:經過Add()向文件中加入一條記錄,經過Finish()完成一個.sst文件的建立和寫入

2.類Table利用成員變量index_block來描述一個.sst文件,經過Open()從一個.sst文件中獲取index_block的內容,經過InternalGet()在一個.sst文件中查找目標記錄

3.類TableCache經過成員變量cache_來將最近使用的.sst文件存放在內存中進行管理(LRU思想)。

4.SSTable的查找:

leveldb在查找一條記錄時,首先是在Memtable中查找,當在Memtable中沒有找到時,纔在SSTable中查找。SSTable是存放在磁盤中的,而訪問磁盤速度很是慢,所以leveldb將最近使用的SSTable文件緩存在內存中,以提升訪問效率,這是經過TableCache實現的。

在SSTable中查找時,具體的步驟爲:

1)經過cache->Lookup()在緩存中查找目標所在的.sst文件,當其不在緩存中時,在內存中建立一個.sst文件並調用cache->Insert()將其加入到緩存中。

2)在找到的.sst文件中調用InternalGet(),首先在index_block中進行查找,找到對應的DataBlock在.sst文件中的偏移和大小。因爲DataBlock是由Block、type(是否壓縮)和CRC三部分組成的,所以須要調用ReadBlock()獲取真正的數據區。

3)而後調用block_iter->Seek(k)在數據區中進行查找,因爲數據區包含多個重啓點,所以首先是在重啓點中進行二分查找,找到目標對應的重啓點。而後從重啓點開始找到重啓點對應的一部分記錄,並在其中查找目標key值。

這樣就完成了在SSTable中的完整查找操做。

相關文章
相關標籤/搜索