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文件的物理劃分結構,同Log文件同樣,也是劃分爲固定大小的存儲塊,每一個Block分爲三個部分,包括Block、Type和CRC。Block爲數據存儲區,Type區用於標識Block中數據是否採用了數據壓縮算法(Snappy壓縮或者無壓縮兩種),CRC部分則是Block數據校驗碼,用於判別數據是否在生成和傳輸中出錯。
以上是.sst的物理佈局,下面介紹.sst文件的邏輯佈局,所謂邏輯佈局,就是說盡管你們都是物理塊,可是每一塊存儲什麼內容,內部又有什麼結構等。圖4.2展現了.sst文件的內部邏輯解釋。緩存
從圖2能夠看出,從大的方面,能夠將.sst文件劃分爲數據存儲區和數據管理區,數據存儲區存放實際的Key:Value數據,數據管理區則提供一些索引指針等管理數據,目的是更快速便捷的查找相應的記錄。兩個區域都是在上述的分塊基礎上的,就是說文件的前面若干塊實際存儲KV數據,後面數據管理區存儲管理數據。管理數據又分爲四種不一樣類型:紫色的Meta Block,紅色的MetaBlock Index和藍色的Index block以及一個文件尾部塊Footer。
LevelDB 1.2版對於Meta Block尚無實際使用,只是保留了一個接口,估計會在後續版本中加入內容,下面咱們看看Index block和文件尾部Footer的內部結構。app
圖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
上面主要介紹的是數據管理區的內部結構,下面咱們看看數據區的一個Block的數據部份內部是如何佈局的,圖5是其內部佈局示意圖。 函數
從圖中能夠看出,其內部也分爲兩個部分,前面是一個個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尾部就是指出哪些記錄是這些重啓點的。 佈局
在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中的偏移量了和大小
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
經過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 }
當一個DataBlock超過設定值(默認爲4K,1個page)時,執行Flush()操做
首先調用WriteBlock()寫入數據,而後對.sst文件執行fflush()將數據寫入磁盤
WriteBlock()首先調用BlockBuilder::Finish()完成一個DataBlock的建立並返回數據區的內容Slice,而後判斷是否須要進行壓縮,最後調用WriteRawBlock()寫入數據,並調用BlockBuilder::Reset()從新開始一個DataBlock
由以前對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的寫入了
調用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類用來描述一個SSTable文件,Table類中也只有一個成員變量Rep *rep_,其結構爲:
其內容主要包括經過SSTable文件中的Footer得到的IndexBlock和MetaIndexBlock(暫時不考慮filter)
經過Open一個.sst文件將其轉換爲Table結構,由下面的代碼可知SSTable中的Footer是長度固定的,爲2*BlockHandle::kMaxEncodedLength + 8,共28字節
所以可直接從一個SSTable中找到Footer結構體,而Footer是索引的索引,其中存儲着IndexBlock和MetaIndexBlock的信息,所以能夠很方便的獲取IndexBlock。
可經過InternalGet()來查找對應的記錄
一、首先在IndexBlock中找到目標key所在的DataBlock在SSTable文件中的偏移量和大小
二、而後根據找到的IndexBlock中的key,offset,size找到對應的DataBlock
三、DataBlock包含實際數據區、type和CRC,調用Table::ReadBlock()來從中找到實際的數據區
四、而後在數據區中對目標key進行查找
前面講過對於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的文件名。
經過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聯繫起來了。
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中的完整查找操做。