SSTable 介绍(一)
如果说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源码学习系列