SSTable全称sorted string table,是不可修改且持久化到磁盘的文件,将Immutable Memtable的内存数据flush到磁盘或相邻层的数据compaction机制等生成,可以多线程环境下被安全的访问而无需任何额外的同步措施。
SSTable的整体设计是一个相对比较大的篇幅,包括索引元数据及block格式、TableCache、BlockCache、读取查找、FilterPolicy、build生成、compaction等,本篇主要讲整体设计,后续几篇会详细讲细节。
相较于linux文件系统的设计,为解决低速磁盘与应用程序读写之间的速度不匹配引入了page cache,应用程序读写page cache(内存),读未命中页面缓存再从磁盘加载块到cache,写先写cache再由操作系统根据一定策略将脏页写入磁盘,page cache块大小默认为4K(即内存页大小,可通过getconf PAGESIZE获取)。leveldb在应用层上设计与此相似,因LSM树结构的存储设计机制,有两级cache,在查找时如果未命中MemTable(内存),先访问缓存SSTable文件指针及偏移(相当于SSTable索引信息)的TableCache,定位到SSTable再访问BlockCache,BlockCache是存放SSTable实际数据的基本单位,最后通过块缓存来获取key对应的实际数据,未命中时则反向更新相应缓存。
来看下SSTable的使用接口及设计,如下:
class Table { public: static Status Open(const Options& options, RandomAccessFile* file, uint64_t file_size, Table** table); ~Table(); // Returns a new iterator over the table contents. // The result of NewIterator() is initially invalid (caller must // call one of the Seek methods on the iterator before using it). Iterator* NewIterator(const ReadOptions&) const; // Given a key, return an approximate byte offset in the file where // the data for that key begins (or would begin if the key were // present in the file). The returned value is in terms of file // bytes, and so includes effects like compression of the underlying data. // E.g., the approximate offset of the last key in the table will // be close to the file length. uint64_t ApproximateOffsetOf(const Slice& key) const; private: struct Rep; Rep* rep_; explicit Table(Rep* rep) { rep_ = rep; } static Iterator* BlockReader(void*, const ReadOptions&, const Slice&); // Calls (*handle_result)(arg, ...) with the entry found after a call // to Seek(key). May not make such a call if filter policy says // that key is not present. friend class TableCache; Status InternalGet( const ReadOptions&, const Slice& key, void* arg, void (*handle_result)(void* arg, const Slice& k, const Slice& v)); void ReadMeta(const Footer& footer); void ReadFilter(const Slice& filter_handle_value); // No copying allowed Table(const Table&); void operator=(const Table&); };
Open方法以随机读的方式打开sst文件并读取索引元数据。
ApproximateOffsetOf通过key查找在数据块中的大致位置(通过索引块),以便后续发起读操作。
NewIterator返回访问SSTable的迭代器。
迭代器设计是整个leveldb设计上的一个亮点,定义一组接口,简化代码,隐藏实现细节,从MemTable、SkipList,再到SSTale、Block、Cache、DB,多层次迭代器组织实现对数据的访问。
再看内部有个private的Rep结构体指针成员,其定义如下:
struct Table::Rep { ~Rep() { delete filter; delete [] filter_data; delete index_block; } Options options; Status status; RandomAccessFile* file; uint64_t cache_id; FilterBlockReader* filter; const char* filter_data; BlockHandle metaindex_handle; // Handle to metaindex_block: saved from footer Block* index_block; };
Rep的成员明明可以直接放在Table类的成员中,这里却组织成一个结构体。在TableBuilder中也有类似设计,用意何在?
设计者并不想将内部私有数据成员的细节暴露给外部调用者,这里通过独立的结构体指针不放在头文件而放在具体实现文件来隐藏实现细节,类似Pimpl Idiom (private implementation),头文件中仅做前向声明,可以在设计上做到更好的封装,隐藏实现细节,用户无需重新编译也能方便升级,尤其对库开发者,这种设计模式是极其重要的,也能减小不同模块间的耦合。