前言
数据顺利写入到LevelDB的Log 和MemTable之后,对客户端写入的请求而言,本次写入已经完成了。而本文主要探讨的就是MemTable中的数据到底是如何写入到磁盘上的,写入后的样子是什么样的。为了后续使用做了哪些工作?即介绍SSTable。
SSTable的具体实现位置有table/*
,可以说出了merge相关的文件都会涉及。本文中介绍部分来自于debug代码,主要还是看的doc/table_format.md
数据结构
在讨论他的数据结构的时候,我们设想下需要的功能,最简单粗暴的方式就是直接序列化为链表,每次查询从头到尾依次反序列化为SkipList,然后查询。这种如果数据量小的情况下,我个人觉得是完全可行的,因为其实就已经完全变成了一个Map。但是LevelDB明显不是为了小数据量而设计的,在大数据量的情况下如果每次都反序列化,文件表多,序列化时间肯定长,随着数据量的增加,单台机器的内存资源总是有限的。所以SSTable的设计必须要考虑到查询的效率以及存储时候的数据压缩等问题。来看下LevelDB中的SSTable是如何实现的。
首先看下SSTable的编码格式图:
和Log文件中有点类似,SSTable的数据被切分成一个个Block,这个Block的长度为4KB,或者说每次SSTable在超过4Kb 就会触发一次数据持久化的操作。这个后面代码详细介绍,这里先看下布局和作用:
- Data Block 用于存储KV中的数据
- Meta Block 现在是filter block,后面可能会新增很多的包含了信息的数据。
- Meta Index Block 包含了一个Meta Block中filter的名字 和Block的原始数据Handle(offset,size)
- Index Block 包含了每一个DataBlock的Handle(offset,size)最后还包含了当前存在的最大值的下一个值
- Footer 是一个定长48字节的数据,其中40字节(l两个64位的Varint)Meta Index Block的索引+20字节的(2个64位的Index block的Varint)Index Block的索引,如果两个不超过则padding对齐,最后有8字节的magic(0xdb4775248b80fb57ull)
也就是说将数据分为了索引文件,过滤文件和具体的数据文件,每一部分又是有不同Block数据组成的。
Data Block 数据结构
接下来看下Data Block中的数据结构。首先在LevelDB中,可以设置数据压缩,这个信息也写在了Block中,而且Block本身也使用了一种在数据字符排序的情况下会有较好压缩的办法,具体如下图:
假设当前写入数据的分别为hello0,hello1 ,enjoy0,enjoy1,那么SkipList中的数据链表就是上图中的SkipList表格中的顺序(从上往下),最后的Key值Encode后就是一行数据,而在Block中,去掉了前面的Internal_key_size。整个逻辑如下:
-
从链表最小写入,所以是enjoy0,此时初始化restarts为空
-
然后写入enjoy1,此时会判断他的和前面数据的相同前缀,enjoy都是,所以不在写入enjoy,而是记录当前的后缀值,此处是1
-
写入hello0的时候,此时完全不同,所以分为两种情况:
- 如果当前的hello0 写入的时候还没有到生成restarts的时机,(默认16个key一组),那么继续将hello的全部数据写入
- 如果达到了restarts的时机,写入刚好超过6个,则将当前的值作为第一个,并且在restart中写入当前buffer的size,其实也就是下次写入的offset。
写入data结束后,每个block 会写两个值,一个是type,表示压缩使用的算法,crc用于校验当前的block数据。
在LevelDB中,每一个Block 最后的写入都会使用这个方式。
从DataBlock 来看,每一次都需要编解码这个值进行合并,在查询的时候可以根据二分查找查找某个Key是否在这个里面。但是很奇怪的是,将key恢复到了原来的值,而不是使用的SkipList中的key,为了节约开始的字节么?
Filter Block
在LevelDB中,需要使用了filter才会有这个filter:
ini
leveldb::FilterPolicy* bloom_filter_policy = const_cast<leveldb::FilterPolicy*>(leveldb::NewBloomFilterPolicy(10));
options.filter_policy=bloom_filter_policy;
上面的写法可能有问题,笔者为了调通也没有在意这么多,如果有其他的方式烦请在评论区说下。filter_policy 中会返回当前这个filter的名字,这个是需要定义的,算得上是标识FIlter的唯一Key了。
Filter Block 有个专属的文件table/filter_block.h
和table/filter_block.cc
。本文介绍的是LevelDB中的BloomFIlter,前面已经介绍过布隆过滤器了,本文就不在详细展开了,虽然LevelDB的使用的是double hash的方式进行的hash计算,但是本质上的原理都是一样的。Bloom过滤器并不是一条记录所有的数据。而是将原始数据进行切分
// Generate new filter every 2KB of data
static const size_t kFilterBaseLg = 11;
static const size_t kFilterBase = 1 << kFilterBaseLg;
所以filter的格式为:
上面的KFilterBaseLg 是可以根据需要调节的,上面是默认的1kb。
后面的Meta Index Block 主要就只是记录filter 名称和具体位置,其实记录的过程主要都是一些初始的offset。下面了解下Index 相关的Block。FIlter Block后面crc,type 图上就没有展示出来了。
Index Block
Index Block 的格式其实和上面都差不多的,只是从原来的Key,Value变成了Key 写入的时候最后一个Key的最后一个记录的值+1,一般情况下是4kb的Key,Value 会有一个index 的entry。
结构相对简单,但是注意的是这个LastK+1不是简单的说加1,比如当前的最后的Key的值是hello,那么他的下一个值就是i+MaxSequence|type,也就是Key的排序的下一个。
为什么不是使用下一个字符加上最小的Sequence呢?这一块笔者暂时没有想明白。
Footer Block
这个就相对比较简单了,第一张图里也介绍了下,其实核心就是指向两个地方,第一个是filter 的Meta Index Block的Handle,还有一个就是指向Index Block 的Handle。便于加载过滤器和索引文件。这里就不在叙述了。
总结
本文简单的介绍了下SSTable中的数据结构,通过画图的方式加深自己对整体结构的理解。也是对后面自己挨个看代码的时候的一个提前架构规划。在写这个文章的过程中,感觉到jeff在写LevelDB的时候看重的主要还是尽可能多的写入和存储数据。笔者对为什么一定要使用这种压缩的感到奇怪,在变化比较频繁的Level0 和Level1 层,是否有必要做这种加解码。希望这个答案能够在后面看到底如何读取和写入的时候了解的更加深入点。下一篇是了解到如何管理,既Version,需要了解到如何管理每一个SSTable文件,何时合并等,然后再跟着代码看合并,这样对SSTable的读写再和源码结合一起看。
PS:如有错漏,希望读者能够在评论区指出来。因为这个编码看的实在是有点害怕哪里错漏。。