如果說Protocol Buffers是Google內部表示獨立數據記錄的單元,那么排序的字符串表--Sorted String Table(SSTable)--是存儲,處理和交換數據集的最流行的輸出之一。正如名字本身所包含的意思一樣,SSTable是一個簡單的抽象,用來高效地存儲大量的鍵-值對數據,同時做了優化來實現順序讀/寫操作的高吞吐量。
2015.5.28 update 重讀了一遍,做了一些小的修改:
SSTable的適用場景:
SSTable的產生背景:
假設我們需要處理輸入量級在G或者T字節級別的一系列任務。而且我們需要執行很多步驟,這些步驟是不同的程序執行的--換句話說,假設我們在運行一系列的Map-Reduce任務。鑒於輸入的數據量級很大,所以讀取和寫入數據就能夠占運行時間的大頭。因此,就不考慮隨機讀取和寫入的情況了,相反我們流式處理輸入數據,一旦處理完成,同樣利用流式操作把結果數據寫回到磁盤上。這樣,我們可以攤薄磁盤I/O操作的成本。
所以SSTable是一個簡單,但是非常有用的用來交換大量的、排好序的數據片段的數據結構。它的使用場景是:
- 需要高效地存儲大量的鍵-值對數據
- 數據是順序寫入
- 要求高效地順序讀寫
- 沒有隨機讀取或者對隨機讀取性能要求不高
SSTable簡介:
Google Bigtable論文中對SSTable的介紹:
SSTable提供一個可持久化[persistent],有序的、不可變的從鍵到值的映射關系,其中鍵和值都是任意字節長度的字符串。SSTable提供了以下操作:按照某個鍵來查詢關聯值,可以指定鍵的范圍,來遍歷其中所有的鍵值對。每個SSTable內部由一系列塊(block)組成(通常每塊大小為64KB,是可配置的)。使用存儲在SSTable結尾的塊索引(block index)來定位塊;當SSTable打開時,索引會被加載到內存里。一次磁盤尋道(disk seek)就可以完成查詢(lookup)操作:首先通過二分查找在存儲在內存的索引中找到對應的塊,然后從磁盤上讀取這塊內容。SSTable也可以完整地映射到內存里,這樣在執行查詢和掃描(scan)的時候就不用操作磁盤了.
所以可以簡單的總結:
SSTable是一個鍵是有序的,存儲字符串形式鍵值對的文件。
SSTable的設計:
"Sorted String Table"就如名字所言,它是一個內部包含了任意長度、排好序的鍵值對<key,value>集合的文件。其結構如上圖所示,SSTable文件由兩部分數據組成:索引和鍵值對數據。所有的key和value都是緊湊地存放在一起的,如果要讀取某個鍵對應的值,需要通過索引中的key:offset來定位。
從上圖可以看到,因為SSTable文件中所有的鍵值對<key,value>是存放到一起的,所以SSTable在序列化成文件之后,是不可變的,因為此時的SSTable,就類似於一個數組一樣,如果插入或者刪除,需要移動一大片數據,開銷比較大。
順序讀取整個文件,就拿到了一個排好序的索引。如果文件很大,還可以為了加速訪問而追加或者單獨建立一個獨立的key:offset的索引。
leveldb中,SSTable的實現
leveldb/目錄是存放對外開放的API頭文件的目錄,對作用域等做了嚴格的限制,為了避免引入多余的依賴關系,比較多的使用了類和結構體的前置聲明[forward declaration]。
SSTable對應的實現是Table類,頭文件是:include/leveldb/table.h。通過Table類開頭的注釋可以看到Table是不可變的,可持久化的。SSTable由於是不可改變的,只讀的,所以是線程安全的,不需要外界的同步操作。
Table對外接口:
Table類只提供了簡單的3個操作:
- 通過文件來反序列化,讀取SSTable的數據:
static Status Open(const Options& options, RandomAccessFile* file, uint64_t file_size, Table** table)
; - 獲得用來訪問SSTable數據的迭代器:
Iterator* NewIterator(const ReadOptions&) const
SSTable的數據讀取都是通過迭代器進行的,迭代器也只允許讀取操作,沒有提供寫入操作。 - 預估key[可能還沒有寫入到SSTable中]對應的數據存儲到SSTable文件的偏移:
uin64_t ApproximateOffsetOf(const Slice& key) const
leveldb對外提供了GetApproximateSizes()
--通過指定key的范圍來獲取存儲這些數據的文件大致大小的功能,所以需要底層的這些數據結構也來提供對應的功能。同時這類函數也能提高leveldb系統的可測性,通過文件的大小就可以判斷寫入數據是否正常。
可以看到SSTable的拷貝構造函數Table(const Table)
和賦值函數void operator=(const Table&)
都是私有的,這樣就是禁止SSTable對象的拷貝了。Table類的使用方,只能通過Open
接口來反序列化SSTable對象。
Table需要知道的類和結構體
通過table.h頭文件可以看到它需要打交道的類或者結構體主要有:
-
class Block:
上文提到每個SSTable文件由一系列可配置大小的塊(block)組成。Block就是對block塊數據的封裝,對外提供size()和迭代器Iterator接口。 -
class BlockHandle
定義在table/format.h中,代表了存儲數據的文件的范圍:偏移offset+大小size -
class Footer:
定義在table/format.h中,封裝了在每個SSTable文件尾部存儲的固定大小的元數據信息(metadata),包含了兩部分數據:metaindex和index數據。index數據就是上文中提到的SSTable的索引數據,而metaindex存儲的是過濾器(例如布隆過濾器)的信息。利用過濾器,可以顯著地減少磁盤訪問。 -
class RandomAccessFile:
SSTable關聯的文件,是可以隨機讀取的文件:可以指定從哪里開始讀,讀取多少字節,方便SSTable按照需要去讀取block的數據 -
struct ReadOptions, Struct Options
把一堆選項相關的參數定義到結構體里,方便傳遞參數,也方便理解,否則看到的就是一堆參數了。 -
class TableCache
一個SSTable的緩存(cache),每次需要對某個SSTable文件要做讀取操作時,去對應的TableCache里面進行操作[如果沒有命中緩存,會加載這個SSTable數據並更新緩存]。TableCache里面包含了SSTable的全部內容:索引+數據 -
struct Table::Rep
定義在table.cc中[是Table類內部使用的結構體],存儲了SSTable相關的一些元數據,例如當前SSTable實例對應的文件句柄[file]、在Table緩存中的句柄[cache_id]、過濾器的讀取對象[filter]、過濾器的數據[filter_data]、元索引[metaindex_handle]和索引[index]數據。有了這些數據,就可以唯一地代表一個SSTable的數據了。
單獨說一下迭代器Iterator,此接口類提供了豐富的數據訪問操作,所有對SSTable和SSTable中block的讀取操作都用迭代器來進行。迭代器定義在include/leveldb/iterator.h中,這里也只是定義了一個迭代器的接口類,規定了對外的統一接口函數,這些接口函數都是純虛函數,需要子類去實現。在leveldb中可以看到很多這樣的例子,這就是面向接口編程的思想。通過迭代器類Iterator的定義看到,table類對外的數據訪問只能通過迭代器類Iterator來進行,而且迭代器只提供讀取操作,key()和value()函數都是const類型,不允許修改Iterator類內部的數據。迭代器還提供了RegisterCleanup函數,可以用掛接多個CleanupFunction類型的回調函數並自定義兩個參數。CleanupFunction是用來在迭代器銷毀時,做自定義的清理工作。
從format.h中還可以看到Table定義了Table內部使用的類和結構體:
- struct BlockContents
封裝了SSTable中每一個block的數據信息,包含實際存儲的數據(Slice data), 是否可以緩存(bool cacheable), 是否需要調用方釋放存儲數據的內存這三類信息。
SSTable的特點
* 存儲的是<鍵,值>格式的字節數據
* 字節數據的長度隨意,沒有限制
* 鍵可以重復, 鍵值對不需要對齊
* 隨機讀取操作非常高效
SSTable的限制
* 一旦SSTable寫入硬盤后,就是不可變的,因為插入或者刪除需要對SSTable文件進行大量的I/O操作
* 不適合隨機讀取和寫入,因為效率很低,原因同上一條
關於SSTable的設計,還有一些東西沒有介紹,例如在磁盤上存儲的具體格式,如何序列化等,留待下一篇介紹。
回到本系列目錄:leveldb源碼學習系列