Plain Table 格式
介绍
PlainTable是RocksDB的SST文件格式,针对纯内存或真正的低延迟媒介上的低查询延迟进行了优化。
优点:
- 建立一个内存索引,用散列+二分查找替换普通二分查找
- 绕过块缓存以避免块复制和LRU缓存维护的开销。
- 查询时避免任何内存副本(mmap)
限制:
- 文件大小需要小于31位整数。
- 不支持数据压缩
- 不支持增量编码
- 不支持Iterator.Prev()
- 不支持非基于前缀的Seek()
- 对于构建索引,表加载较慢
- 只支持mmap模式。
我们计划减少一些限制。
用法
您可以在table.h中调用两个工厂函数NewPlainTableFactory()或NewTotalOrderPlainTableFactory()来为带有参数的普通表生成一个表工厂, 并将其传递给选项。table_factory或ColumnFamilyOptions.table_factory。如果使用前一个函数,则需要指定前缀提取器。例子:
options.table_factory.reset(NewPlainTableFactory());
options.prefix_extractor.reset(NewFixedPrefixTransform(8));
或
options.table_factory.reset(NewTotalOrderPlainTableFactory());
有关参数的说明,请参阅include/rocksdb/table.h中的两个函数的注释。
NewPlainTableFactory()使用键前缀为基于散列索引的普通表创建普通表工厂。这就是明文优化的目的。
NewTotalOrderPlainTableFactory()不需要前缀提取器,而是使用完全二进制索引。这个函数主要是为了使明文功能完备。在本例中,我们还没有高度优化查询性能。
### File Format
#### Basic
<beginning_of_file>
[data row1]
[data row1]
[data row1]
...
[data rowN]
[Property Block]
[Footer] (fixed size; starts at file_size - sizeof(Footer))
<end_of_file>
属性块和页脚的格式与基于相同的BlockBasedTable格式 有关每个数据行的格式,请参见Row Format。( https://github.com/facebook/rocksdb/wiki/PlainTable-Format#row-format ) 属性块中的两个属性用于读取数据:
data_size: the end of data part of the file
fixed_key_len: length of the key if all keys have the same length, 0 otherwise
行格式
每个数据行编码为:
<beginning of a row>
encoded key
length of value: varint32
value bytes
<end of a row>
有关已编码key的格式,请参阅key编码。 ( https://github.com/facebook/rocksdb/wiki/PlainTable-Format#key-encoding )
Key编码
key有两种编码类型: kPlain和kPrefix,可以在创建plain表工厂时指定这两种编码类型。
普通编码
如果给定固定密钥长度,则对普通内部密钥进行编码。 如果没有给定固定的密钥长度,则密钥为可变长度,并将被编码为
[length of key: varint32] + user key + internal bytes
有关内部字节的详细信息,请参阅内部字节编码。( https://github.com/facebook/rocksdb/wiki/PlainTable-Format#internal-bytes-encoding )
Prefix编码
kPrefix编码类型是一种特殊的增量编码,如果一行显示的前缀(由用户给出的前缀提取器确定)与前一个key相同,则可以避免重复key的前缀部分。
在这种情况下,有三种情况下编码一个键(记住所有键都是排序的,所以共享相同前缀的键在一起):
前缀的第一个键,其中需要写入完整的键 前缀的第二个键,其中需要记录前缀长度,以及前缀以外的字节(为了简化,这里我们将其称为后缀) 前缀的第三个或更晚的键,其中只需要写入后缀部分。
我们为指示全键定义了三个标志、一个前缀和一个后缀。对于这三种情况,我们都需要一个尺寸。它们以这种格式编码:
第一个字节的8位:
+----+----+----+----+----+----+----+----+
| Type | Size |
+----+----+----+----+----+----+----+----+
前两位表示 全键(00)、前缀(01) 或 后缀(02)。最后6位代表大小。 如果大小位不都是1,则表示键的大小。否则,varint32是在这个字节之后写的。 这个varint值 + 0x3F(所有1的值)将是键大小。这样,较短的键只需要一个字节。
以下是上述三种情况的格式:
(1) 完整的键
+----------------------+---------------+----------------+
| Full Key Flag + Size | Full User Key | Internal Bytes |
+----------------------+---------------+----------------+
(2) 前缀的第二个键
+--------------------+--------------------+------------+----------------+
| Prefix Flag + Size | Suffix Flag + Size | Key Suffix | Internal Bytes |
+--------------------+--------------------+------------+----------------+
(3) 前缀的第三个和后面的键:
+--------------------+------------+----------------+
| Suffix Flag + Size | Key Suffix | Internal Bytes |
+--------------------+------------+----------------+
有关所有三种情况的内部字节的详细信息,请参见内部字节编码。( https://github.com/facebook/rocksdb/wiki/PlainTable-Format#internal-bytes-encoding )
使用这种格式,在不知道前缀的情况下,只能使用全键的文件偏移量来查找行键。 因此,如果一个前缀中有太多的键,plain table builder可能会决定再次重写完整的键,即使它不是前缀的第一个键,以使查找更容易。
下面是一个例子,we for following keys(前缀和后缀由空格分隔):
AAAA AAAB
AAAA AAABA
AAAA AAAC
AAABB AA
AAAC AAAB
将这样编码:
FK 8 AAAAAAAB
PF 4 SF 5 AAABA
SF 4 AAAC
FK 7 AAABBAA
FK 8 AAACAAAB
(其中FK为全键标志,PF为前缀标志,SF为后缀标志)
内部字节编码
在普通编码类型和前缀编码类型中,内部键的内部字节都以相同的方式编码。 在RocksDB中,键的内部字节包括行类型(值、删除、合并等)和序列ID。通常,键的格式如下:
+----------- ...... -------------+----+---+---+---+---+---+---+---+
| user key |type| sequence ID |
+----------- ..... --------------+----+---+---+---+---+---+---+---+
其中类型取一个字节,序列ID取7个字节。 在普通表格式中,这也是优化一个键的常用方法。此外,我们还进行了优化,为一个常见的情况节省了一些额外的字节:序列ID为0的值类型。 RocksDB,我们有一个优化来填补序列ID为0时,一个关键我们确信没有前一个值这个关键系统中(具体来说,第一个关键的最后水平level-style压实或最后一个文件在统一风格),使更好的压缩和编码。 在PlainTable中,我们使用1字节”0x80”代替8字节作为内部字节:
+----------- ...... -------------+----+
| user key |0x80|
+----------- ..... --------------+----+
In-Memory 索引格式
基本思想
内存索引构建得尽可能紧凑。在顶层,索引是一个哈希表,文件中的每个bucket都要偏移,或者是一个二进制搜索索引。在两种情况下需要进行二分查找:
(1)哈希冲突:将两个或多个前缀哈希到同一个桶中 (2)一个前缀键数过多:需要加快前缀内查找
格式
索引由两部分内存组成:一个用于哈希桶的数组和一个包含二进制可搜索索引的缓冲区(下面将其称为”二进制搜索缓冲区”,并将”file”作为SST文件)。
Key根据其前缀的哈希值被哈希到bucket(使用Options.prefix_extractor提取)。
+--------------+------------------------------------------------------+
| Flag (1 bit) | Offset to binary search buffer or file (31 bits) +
+--------------+------------------------------------------------------+
如果Flag = 0, offset字段等于文件末尾数据的偏移量,则表示null—该桶没有数据;如果偏移量更小,这意味着bucket只有一个前缀,从该文件偏移量开始。如果Flag = 1,则表示偏移量用于二进制搜索缓冲区。该偏移量的格式如下所示。
从二进制搜索缓冲区的偏移量开始,二进制搜索索引编码如下:
<begin>
number_of_records: varint32
record 1 file offset: fixedint32
record 2 file offset: fixedint32
....
record N file offset: fixedint32
<end>
其中N = number_of_records。偏移量按升序排列。
只存储31位偏移量并使用1位来确定是否需要二进制搜索的原因是为了使索引紧凑。
索引的一个例子
假设这是一个文件的内容:
+----------------------------+ <== offset_0003_0000 = 0
| row (key: "0003 0000") |
+----------------------------+ <== offset_0005_0000
| row (key: "0005 0000") |
+----------------------------+
| row (key: "0005 0001") |
+----------------------------+
| row (key: "0005 0002") |
+----------------------------+
| |
| .... |
| |
+----------------------------+
| row (key: "0005 000F") |
+----------------------------+ <== offset_0005_0010
| row (key: "0005 0010") |
+----------------------------+
| |
| .... |
| |
+----------------------------+
| row (key: "0005 001F") |
+----------------------------+ <== offset_0005_0020
| row (key: "0005 0020") |
+----------------------------+
| row (key: "0005 0021") |
+----------------------------+
| row (key: "0005 0022") |
+----------------------------+ <== offset_0007_0000
| row (key: "0007 0000") |
+----------------------------+
| row (key: "0007 0001") |
+----------------------------+ <== offset_0008_0000
| row (key: "0008 0000") |
+----------------------------+
| row (key: "0008 0001") |
+----------------------------+
| row (key: "0008 0002") |
+----------------------------+
| |
| .... |
| |
+----------------------------+
| row (key: "0008 000F") |
+----------------------------+ <== offset_0008_0010
| row (key: "0008 0010") |
+----------------------------+ <== offset_end_data
| |
| property block and footer |
| |
+----------------------------+
让我们假设在这个例子中,我们使用2字节固定长度的前缀,并且在每个前缀中,行总是递增1。
现在我们正在为文件构建索引。通过扫描文件,我们知道有4个不同的前缀(“0003”、”0005”、”0007”和”0008”),假设我们选择使用5个哈希桶,根据哈希函数,将前缀哈希到桶中:
bucket 0: 0005
bucket 1: empty
bucket 2: 0007
bucket 3: 0003 0008
bucket 4: empty
Bucket 2不需要二分查找,因为它只有一个前缀(“0007”),并且只有2(<16)行。
Bucket 0需要二进制搜索,因为前缀0005有16多行。
Bucket 3需要二进制搜索,因为它包含多个前缀。
我们需要为bucket 0和3分配二进制搜索索引。结果如下:
+---------------------+ <== bs_offset_bucket_0
+ 2 (in varint32) |
+---------------------+----------------+
+ offset_0005_0000 (in fixedint32) |
+--------------------------------------+
+ offset_0005_0010 (in fixedint32) |
+---------------------+----------------+ <== bs_offset_bucket_3
+ 3 (in varint32) |
+---------------------+----------------+
+ offset_0003_0000 (in fixedint32) |
+--------------------------------------+
+ offset_0008_0000 (in fixedint32) |
+--------------------------------------+
+ offset_0008_0010 (in fixedint32) |
+--------------------------------------+
下面是哈希桶中的数据:
+---+---------------------------------------+
| 1 | bs_offset_bucket_0 (31 bits) | <=== bucket 0
+---+---------------------------------------+
| 0 | offset_end_data (31 bits) | <=== bucket 1
+---+---------------------------------------+
| 0 | offset_0007_0000 (31 bits) | <=== bucket 2
+---+---------------------------------------+
| 1 | bs_offset_bucket_3 (31 bits) | <=== bucket 3
+---+---------------------------------------+
| 0 | offset_end_data (31 bits) | <=== bucket 4
+---+---------------------------------------+
索引查找
要查找键,首先使用选项计算键的前缀。并找到前缀的桶。如果bucket上没有记录(标志=0,偏移量是文件中数据结束的偏移量),则找不到键。否则,
如果Flag=0,则表示bucket只有一个前缀,而前缀没有多少键,所以offset字段指向前缀的文件偏移量。我们只需要从这里做线性搜索。
如果Flag=1,则需要对这个桶进行二叉搜索。可以从偏移量字段检索二进制搜索索引。在二叉搜索之后,从二叉搜索找到的偏移量进行线性搜索。
建立索引
构建索引时,扫描文件。对于每个键,计算它的前缀,记住(前缀的哈希值,偏移量)信息,从第一行开始,每个前缀(n=0,1,2…)的第(16n+1)行。 16是在二叉搜索之后的线性搜索中需要检查的最大行数。通过增加数量,我们可以节省索引的内存消耗,但为线性搜索支付更多的成本。 反之亦然。根据前缀的数量,确定最优桶大小。分配所需的精确桶和二进制搜索缓冲区,并根据桶的大小填充索引
布隆过滤器
可以为查询配置前缀上的bloom过滤器。用户可以配置为每个前缀分配多少位。 当执行查询(Seek()或Get()时,会检查bloom filter并在查找索引之前过滤掉不存在的前缀。
未来计划
- 可以考虑将索引具体化为SST文件的一部分。
- 添加一个选项,通过交换索引的内存消耗来消除文件大小的限制。
- 可以构建额外的更稀疏索引,以启用一般迭代器查找。