这学期选的数据库实现课程的期末大作业是修改leveldb的源代码来实现某些特定的功能,我在班里大佬的给Hint的情况下大约4天撸完了整个作业(大佬反而花了很多时间,因为他是先行者,而且自己造了很多轮子,膜拜orz),因为我懒,不想自己造轮子写功能,所以只能通过看代码来寻找到相应的函数,在这个过程中对于sstable这个结构也有了更深入的了解.
SSTable 格式
- footer: 整个sstable文件的头部, 用来存放metaindex block 和 index block这两个块的索引
- index block: 指向所有的data block,保存所有data block的索引
- metaindex block: 指向所有的meta bock,保存所有meta block的索引
- data block: 用于存储数据
- meta block:在正常情况下为空, 如果在打开数据库时声明使用bloom filter,那么此处他们将会保存bloom filter的相关数据.
Block 的结构
所有的block的结构都是如此,包含了若干个数据项和若干个重启点. 在这里你可能会困惑于什么是重启点. 想要搞清楚这个,必须先了解数据项的结构.
为了压缩数据,leveldb 的开发人员使用了一种(在我看来)非常tricky的技术, 他们在保存数据的key时并不直接存储整个字符串,而是使用一种共享字段技术.
举个例子, 我有两个key,分别是“abcd“和 ”abcep“,如果以”abcd“为基准,那么此时第二个key,也就是"abcep"的存储格式将会如下.
共享长度3 是 “abc”的长度, 非共享长度10 是 “ep”(2) + seq number 和 value type的编码.
介绍完这些就可以了解什么是重启点了,重启点其实就是刷新每个基准的点, 例如前16个entry 都是以 “abcd”这个key作为基准, 而之后的16个entry又是以“acfg”这个key作为基准,那么每16个entry中的第一个entry就是一个重启点.
详细解读
- footer:
Footer的固定大小为48个bytes,最后的8个bytes为magic number,相当于是让系统知道它是footer的一个令牌, 最开始是两个handle, handle之后是一段填充字节(全为0).
class
上面代码为format.h 中的Footer class代码, kEncodedLength 就是Footer的size, 而kMaxEncodedLength则是一个 BlockHandle可以达到的最大size(这里为10+10=20),priave变量中保存了两个handle的信息, 均可以通过public函数进行访问.
这里再补充一下Handle的结构,Handle 本质上就是一个指针,下面以IndexBlock handle为例子.
他由两部分组成,分别是表示相当于sstable文件启始流偏移的Offset和表示所指向文件大小的Size.这两个变量的最大Size为20(注意上文中提到的10+10).
footer的读取:
*
上述代码为http://table.cc 中的 Open()函数.
#1 处首先进行了对于文件的size进行了判断,如果文件大小小于48个字节的话,说明肯定发生了某些错误,因为哪怕你什么都不存,一个footer的大小为48bytes.
#2 处则对文件进行了读取,通关参数传递将对于的footer数据存储于footer中. file->Read()函数中第一个参数为所要读取数据的偏移,在这里就是 文件的size - 48 个bytest,第二个参数为所要读取的长度.
#3 处则是对读取出来的footer_input数据进行解码处理, 在这里要注意, 为了存储效率,leveldb对于所有刷盘的数据都进行了Encode处理,要读出可以理解的数据,都需要进行Decode处理.
读取完Footer之后,随即开始解析其中的index block handle.
BlockContents
可以看出这里调用了ReadBlock函数直接读取Index block的内容并将相应的数据存储在index block contents这个变量当中.
if
成功读取之后就利用Table::Rep这一结构体来保存一些变量(绝大多数与sstable相关的内容都保存于此),从代码中可以看出保存的有改sstable 的文件指针,metaindex handle, index block等内容.
#1处的代码可以发现leveldb首先默认将filter data 和filter 置为空值, 这是因为如果在打开DB 是没有设置filter policy的话,整个于Filter相关的数据(即Meta data)都将不会被用到
至此,Footer的使用价值已经基本结束
2. Index Block
在调用了Table::Open()之后我们已经获取了sstable的的大致信息,接下来就可以就可以使用其中的index block来寻找具体的数值了.
接下来可以把注意力放到http://table.cc 中的 InternalGet()函数.
Status
首先在在这个函数中,Table 中的index_block会新建一个迭代器来确定所要寻找的key所在的data block.
Seek 会在restart point数组上进行二分查找以找到确定的datablock(注意, index_block 记录的是指向data block的指针,而并非数据本身)
if
#1 处代码中的逻辑其实非常值得细细品味, 它非常成功得体现出了写着一行代码人的功力(膜拜orz)
这里的核心思想其实就是利用bloom过滤器判断这个data block中是否有可能包含这个key.
当且仅当filter不为空且handle value成功decode并且filter中的KeyMayMatch不命中,那么才说明这个data block绝对不包含在合格key值,可以跳过. (难以理解的朋友可以先去看一下bloom 过滤器的知识)
如果上述有一个条件不满足,例如没有开启bloom过滤器(即filer == nullptr)或者KeyMayMatch为真,那么程序就会跳转到#2处, 这个data block 都会被遍历读取.(一般来说handle.DecodeFrom(&handle_value).ok()这个条件不会出现问题,他是将index block 中保存的offset值decode 出来)
接下来再来看#2 处的代码
else
首先是调用BlockReader()这一函数, 它接受三个参数,分别是调用它的table(this), Readoptions(options)以及未经过decode的handl->value(iiter->value()),并且返回一个读取block的迭代器.
接下来便使用这个迭代器去寻找需要的key值, 寻找的方法也是二分查找,具体代码我们按下不表.如果成功找到(block_iter->Valid()=ok()),那么就会调用一个函数将寻找到的key,value保存下来.
#1 处的函数指针实际上是一个save value的函数,不必要太过于深入追究,重要的是成功的读出了key 和 value的值(block_iter->key(), block_iter->value())
至此,利用sstable 读取数据的过程便彻底结束.
reference:
LevelDB 完全解析(3):SSTable