LevelDB基础原理(1) SSTable
1. 介绍
1.1 描述
SSTable(Sorted String Table)是一个通常放在磁盘上的,排序的字符串表, 用来高效存储大量的键值对数据, 同时搭配上优化实现IO操作的高吞吐量.
1.2 背景
当我们要运行一系列的Map-Reduce任务时,因为输入的数据量很大,所以消耗在IO的时间上很多.如果不考虑随机读取的场景的话,我们可以通过一系列流式处理输入数据,再将数据流式输出到磁盘上,最大程度的减少磁盘IO成本.
所以SSTable的主要用于交换大量的,排好序的数据片段的数据结构,他的使用场景在于
- 需要高效的存储大量KV数据
- 数据需要顺序写入
- 高效的进行顺序读写
- 没有随机读取或者随机读取性能不高
1.3 组成结构
抽象的看,SSTable的结构主要由两部分组成,索引和键值对数据.
- 索引由多个key-offset的键值对组成,每个键值对包含了对应的key在sstable的位置,如果要读取某个键对应的值,需要通过索引获取对应的offset来定位
- 键值对数据是将所有排好序的key和value紧凑的存放在一起
2. 设计
实际设计中,将SSTable分为数据区和索引区, 每个区又主要由多个block组成,block可以看作是存储信息的一个单元。其中数据区由多个data block组成,其中存储了具体的KV数据内容。
索引区是做什么的,为什么数据区要区分多个data block,全部放在一个data block会有问题吗?由于SSTable是存放在磁盘上的数据结构,而其中的数据又是排序好按照顺序存放的。当我们在随即查询一条key的时候,如果没有索引,只有一块完整的数据块,就只能顺序的遍历整个数据块来查询了,因此将数据区进行了分块的操作,并且通过索引区可以获得每个数据块的一些具体信息,查询时可以通过索引信息快速定位到对应范围内的数据块进行查找,能大大减少查询开销。
2.1 block
block是SSTable里面存储信息的一个单元,主要由数据区record和restarts组成,
-
restarts的目的主要是获取每组的起始位置,block一般给一定数量的key-value分组(默认是16),restarts数组记录了每组的起始位置,当进行随机查询的时候,可以通过restart数组来对数据进行二分查询,快速获取对应的key所在的组.
-
基础的record结构可以用key-length|key-data|value-length|value-data这样的结构顺序排布,但是sstable中的key是相邻存储的,实际上有很大的概率key的前缀出现重复,所以存储时采用前缀压缩,后一个key存储与前一个key不同的部分(每组的起始位置key进行完整存储)
-
type表示数据是否压缩,crc32是校验码
2.2 数据区
数据区由多个data block组成,
- data block用于存储key-value记录,分为Data, type, CRC三部分
2.3 索引区
索引由filter block, metaindex block, index block, footer四部分组成
- filter block: 用于快速从data block判断key value是否存在(默认没有使用)
- metaindex block: 记录filter block的相关信息
- index block: 描述一个data block, 存储对应data block的最大key值以及data block在文件中的偏移量和大小
- footer:索引的索引,记录metaindex block和index block在SSTable中的偏移量和大小
2.3.1 index block
index block是data block的索引,记录每个data block最大的key和起始位置以及大小
- 理论上的存储是以每个data block的最大key作为key,起始位置和大小作为value, 这样可以二分定位key所在的block
- 实际上SSTable在存储Key的时候做了优化,存储时不保存最大的key,而是保存能够分割data block的最短key,例如data block1的最大key为abcdefg, datablock2的最大key为abefg, 则index保存abd, 只要能够保证这个串大于data block1中的所有Key,小于data block2中的所有Key, 就能达到优化的目的, 这么做可以有效缩减索引长度,减少比较次数
2.3.2 filter block
filter block是一个bloom filter(布隆过滤器), bloom filter通过对每个值计算多个无偏hash值映射到数组上,然后查询的时候看查询值的所有hash值在数组内是否都被映射,来判断是否key已经出现过
每个bloom filter是对data block的key进行多次hash形成的字节数组, 多少个data block就会对应对少个bloom filter
filter block的作用在于判断查询key是否在block内存在,如果不存在,就不用通过restarts的方式查找,能够提高效率
2.3.3 metaindex block
只有一条记录,key 为filter. + filter_policy的name, value是filter大小和起始位置
2.3.4 footer
footer位于SSTable文件尾部, 占用空间固定为48字节,
- 末尾8字节是一个magic number, 用于校验
- metaindex_block_handle和index_block_handle记录了metaindex block和index block的起始位置和大小,分别以一个不定长64位整数存储,总共4个,因为不定长的64位整数最多需要10个字节来存储,所以最多是40个字节(为什么是10个而不是8个呢,不定长的整数在每个字节增加了一个标志位,导致每个字节只能存储7个比特的信息,这个在后续对LevelDB的源码剖析(2)中有详细解释)
- 因为footer是定位文件的起始索引,所以定位的方式实际是通过访问文件结尾的固定长度的内容,如果因为不定长整数导致footer的长度不固定,自然就无法准确定位footer的开头,所以为了规避这个问题,对于block hndle不到40比特的文件,将在后面通过padding来补齐到40个比特
- magic number用来校验文件的正确性,规定占用8个字节
3. 总结
3.1 特点
- **形成之后数据不可变: **由于所有键值对是有序的存放在一起的,所以SSTable在序列化成文件之后是不可变的,否则进行插入和删除都需要移动一大片数据,开销很大.
- **主要作用于顺序读取场景: **由于随机读取是通过多层索引层层查找的,所以sstable主要作用在完全顺序读取或随机读取不多的场景下
3.2 随机查找流程流程
- 先读取footer
- 根据footer中的meta index block handler记录的布隆过滤器信息,读取布隆过滤器记录的二进制数据
- 根据布隆过滤器的记录的二进制数据,查询key是否存在,如果不存在则直接返回
- 根据footer中的index block handler记录的 index block的起始位置和大小,读取index block
- 通过index block查询key所在的data block
- 在data block内通过restarts进一步确定key所在的group