leveldb会按照不同版本组织数据(level-0 -> level-n,从新到旧),这些数据以SSTable格式存储于磁盘上。一个SSTable文件可以看成一个基于磁盘、只读的map,支持顺序扫描,同时可以查找某个key。本文就来探究一下SSTable文件的格式,以及创建过程。
| - - - - - - - - - - - - - - - - |
| Data Blocks |
| - - - - - - - - - - - - - - - - |
| Filter Block |
| - - - - - - - - - - - - - - - - |
| Meta Index Block |
| - - - - - - - - - - - - - - - - |
| Index Block |
| - - - - - - - - - - - - - - - - |
| Footer |
| - - - - - - - - - - - - - - - - |
如上就是SSTable的文件格式,整个文件包括一系列的Block,Block是磁盘io以及内存cache的基本单位,通过Block读写可以均摊每次IO的开销,利用局部性原理。Block的大小是用户可配置的。
1) Data Blocks:存放kv对,即实际的数据。kv对会按照大小划分成Block,无法保证所有的Block大小一致,但基本接近于配置的Block大小,但是当kv对数据大于该值时,会作为一个Block。Block内部还包括其他的一些元数据,后文会深入介绍。
2) Filter Block: Filter用于确定SSTable是否包含一个key,从而在查找该key时,避免磁盘IO。Filter Block用于存储Filter序列化后的结果。
3) Meta Index Block: 用于存放元数据,目前只会kv格式存储Filter block的偏移量,key是filter.${FILTER_NAME},value是filter block在文件的偏移量。
4) Index Block: 对Data block的索引,保存了各个Block中最小key,从而确定了Block的key的范围,加速某个key的搜索。
5) Footer: 元数据的元数据,其中包含Index Block和Meta Index Block的偏移量。Footer之所以在最后,是因为文件生成时是顺序追加的,而Footer的信息又依赖于之前的所有信息,所以只能在最后。由于包含了元数据,所以读取SSTable时首要的就是加载footer。
BlockHandle: 指向文件中的一个Block,有两个属性Block的偏移量(offset_)和大小(size_)。
Footer: 表示SSTable文件的Footer,大小固定。
这两个结构提供到string的序列化和反序列化的方法。
TableBuilder: 构造SSTable的入口。将一系列的kv对构造成SSTable。
BlockBuilder: 构造Block,对添加的kv对进行序列化。
FilterBlockBuilder: 构造Filter Block。
以上便是生成SSTable文件的主要数据结构。
上文提到Block是数据传输的基本单位,在Data Block中通过Block将连续的kv对打包处理,可以利用局部性原理。同时kv按key顺序存储,那么同一个Block中key的重复内容比例会增加,可以通过压缩提高空间利用率。
1) Block格式:
| - - Block Content: var len - - | - - Block Type: 1Byte - - | - - CRC: 4Byte - - |
一个Block包含3部分:
(1)Block Content:kv序列化后的内容,是可变长度的字节数组。
(2)Block Type:指定Block是否压缩,1个字节。
(3)CRC:CRC校验码,用于数据完整测试,4个字节。
2) Block Content格式:
| - - KV Entries: var len - - | - - Restart Point Array: 4nBytes - - | - - Num Restart Point: 4Byte - - |
(1)KV Entries:一系列kv内容,Block的实际内容,是可变长度的字节数组。
(2)Restart Point Array:Restart Point数组,每个元素就是一个整形,4n字节。后文会介绍Restart Point的作用。
(3)Num Restart Point:指定了Restart Point数组的长度,4字节。
3) KV Entry格式:
由于kv对按key顺序存储,所以对key采用前缀压缩以节省空间。
| - - Shared: 4Byte - - | - - Non-shared: 4Byte - - | - - Val Len: 4Byte - - | - - Key Delta: var len - - | - - Value: var len - - |
(1)Shared:与前一个key共同前缀长度。
(2)Non-shared:key非共同前缀长度,Shared + Non-shared等于key的长度。
(3)Val len:value的长度。
(4)Key Delta:存储去除共同前缀的key。
(5)Value:存放Value。
通过上面描述可以知道,在存储一个key时,不会存其与前一个key的共同前缀,只会存不同的部分。那么,从磁盘读取一个key时,就需要先把前一个key读出才能获取完整的可以。这会存在一个问题,如果读取最后一个key,那么需要把[1, n-1]个key都读出才能获得完整内容。
LevelDB通过引入Restart Point来解决上述问题,每个Restart Point相当于前缀压缩的重启点。Restart Point指向一个key,它会存储完整的内容,其Shared等于0。通过在一些kv中平均插入多个Restart Point,可以减少前缀解压缩读取的长度。默认,LevelDB中放置Restart Point的间隔为16,保证最坏情况下最多只要读取15个key就能获取一个key的完整内容。
同时,Restart Point指向的key也是排序的,可以把底层kv序列的二级索引,在进行key搜索时,先进行Restart Point的二分查找框定范围,然后再在指定的key范围内线性查找。
Filter用于加快key搜索,避免无效的磁盘IO。LevelDB本身提供Bloom Filter。可以简单把Filter当做是一个集合,用于判断一个key是否存在于该集合。FilterBlock中存放的是SSTable文件中所有key组成的集合信息(就是Filter根据key进行序列化后的结果)。
1) FilterBlock格式:
| - - Encoded Filter Array: var len - - | - - Filter Offset Array: 4nByte - - | - - Offset of Offset Array: 4Byte - - | - - Base lg: 1Byte -- |
(1)Encoded Filter Array:经过Filter序列化的字节数组,由n个Filter的信息组成。
(2)Filter Offset Array:指定每个Filter在Encoded Filter Array中的偏移量。
(3)Offset of Offset Array:Filter Offset Array的偏移量。
(4)Base lg:2^(Base lg)是对数据块构造Filter的最小size。LevelDB默认是2KB。
2) 流程:
LevelDB对于创建Filter源数据的大小有要求,不能小于2KB,这么做是为了防止源数据过小,导致取Filter的粒度过小,单位Block对应Filter的空间使用率过大,会比较浪费。
Encoded Filter Array与Data Block之间的对应关系稍微复杂些,当Block小于2KB时(比如1KB),那么多个Block会使用一个Filter。如果Block大于2KB(比如4KB),那么一个Block对应一个Filter。
在每次开始一个新的Block时,会调用FilterBuilder.StartBlock()方法,这里会确定Block与Filter之间的对应关系,如果前一个Filter已经完成,会生成这个Filter。在所有Key添加完毕后,会调用FilterBuilder.Finish()方法进行整体序列化,并返回序列化后的结果。
TableBuilder是对外生成SSTable的接口,通过Add方法接收一个个kv,依托于底层的BlockBuidler创建Block,如果当前的Block大小超过预设的值,会调用BlockBuilder的Finish方法进行序列化,然后追加到文件。在所有kv添加完毕后,调用TableBuilder.Finish方法追加元数据,包括Filter Block,Meta Index Block,Index Block以及Footer。
上述介绍的是SSTable的磁盘表现形式,设计的相当精巧,包括但不限于前缀压缩,Restart Point的引入,Filter的源数据块划分等。下一篇会介绍SSTable的读取,即内存表现形式。