Summary
在上一章中,您已经构建了一个具有get/scan/put支持的存储引擎。在本周末,我们将实现SST存储格式的一些简单但重要的优化。欢迎来到Mini-LSM的第1周零食时间!
在本章中,您将:
- 在SST上实现布隆过滤器,并集成到LSM读路径get中。
- 以SST块格式实现对key存储的压缩。
要将测试用例复制到启动器代码中并运行它们,
cargo x copy-test --week 1 --day 7
cargo x scheck
Task 1-Bloom Filters
布隆过滤器是维护一组键的概率数据结构。您可以将键添加到布隆过滤器中,并且您可以知道在添加到布隆过滤器中的键集合中,哪些键可能存在/一定不存在。
为了构造布隆过滤器,通常需要有一个散列函数,一个键可以有多个散列。让我们看看下面的例子。假设我们已经有了一些键的哈希,并且布隆过滤器有7位。
【注:如果你想更好的理解布隆过滤器,可以看这里】
hash1 = ((character - a) * 13) % 7 hash2 = ((character - a) * 11) % 7 b -> 6 4 c -> 5 1 d -> 4 5 e -> 3 2 g -> 1 3 h -> 0 0
如果我们将b, c, d插入到7位布隆过滤器中,我们将得到:
bit 0123456 insert b 1 1 insert c 1 1 insert d 11 result 0101111
在探测布隆过滤器时,我们为key生成哈希,并查看是否设置了相应的位。如果它们都设置为true,那么key可能存在于bloom过滤器中。否则,该键一定不存在于布隆过滤器中。
对于
e -> 3 2
,由于位2
位置上的bit
位没有设置为true
,所以它一定不存在于原始集合中。对于g -> 1 3
,由于两个位都被设置为true
,它可能存在于集合中,也可能不存在于集合中。对于h -> 0 0
,两个位(实际上它是一个位)都没有设置,因此它不应该在原始集合中。b -> maybe (actual: yes) c -> maybe (actual: yes) d -> maybe (actual: yes) e -> MUST not (actual: no) g -> maybe (actual: no) h -> MUST not (actual: no)
还记得在上一章的最后,我们实现了基于key范围的SST过滤。现在,在get读取路径上,我们还可以使用布隆过滤器来忽略不包含用户想要查找的key的SST,从而减少从磁盘读取文件的数量。
在此任务中,您需要修改:
src/table/bloom.rs
在实现中,您将从键散列(是u32数字)构建一个布隆过滤器。对于每个哈希,您需要设置k位。这些位的计算方式如下:
let delta = (h >> 17) | (h << 15); // h is the key hash for _ in 0..k {// TODO: use the hash to set the corresponding bith = h.wrapping_add(delta); }
我们提供了执行神奇数学的所有框架代码。您只需要实现构建布隆过滤器和探测布隆过滤器的步骤。
参考资料:
https://leveldb-handbook.readthedocs.io/zh/latest/bloomfilter.html#
https://blog.csdn.net/jiaomeng/article/details/1495500
Task 2-Integrate Bloom Filter on the Read Path
在此任务中,您需要修改:
src/table/builder.rs src/table.rs src/lsm_storage.rs
对于布隆过滤器编码,您可以将布隆过滤器附加到SST文件的末尾。您需要在文件末尾存储布隆过滤器偏移量,并相应地计算元偏移量。
----------------------------------------------------------------------------------------------------- | Block Section | Meta Section | ----------------------------------------------------------------------------------------------------- | data block | ... | data block | metadata | meta block offset | bloom filter | bloom filter offset | | | varlen | u32 | varlen | u32 | -----------------------------------------------------------------------------------------------------
我们使用
farmhash
来计算键的哈希值。在构建SST时,您还需要通过使用farmhash::Factory32
计算密钥哈希来构建布隆过滤器。您需要在block的元信息中对布隆过滤器进行编码/解码。你可以为你的布隆过滤器选择误报率0.01。根据需要,您可能需要向结构中添加新的字段,而不是在启动器代码中提供的字段。之后,可以修改
get
读取路径,根据bloom filter过滤SST。我们没有对此部分进行集成测试,您需要确保您的实现仍然通过之前的所有章节测试。
本任务就是借助任务1实现的布隆过滤器,优化性能。在SsTableBuilder
中添加成员变量key_hashes
。
在SsTableBuilder
的add
操作中,添加键的hash值:
self.key_hashes.push(farmhash::fingerprint32(key.raw_ref()));
在SsTableBuilder
的build
操作中,添加完元信息后,再添加编码后的布隆过滤器:
let bloom = Bloom::build_from_key_hashes(&self.key_hashes,Bloom::bloom_bits_per_key(self.key_hashes.len(), 0.01),
);
let bloom_offset = buf.len();
bloom.encode(&mut buf);
buf.put_u32(bloom_offset as u32);
在SsTable
的open
操作中,先读取解码出布隆过滤器:
let bloom_offset = (&file.read(len - 4, 4)?[..]).get_u32() as u64;
let raw_bloom = file.read(bloom_offset, len - 4 - bloom_offset)?;
let bloom_filter = Bloom::decode(&raw_bloom)?;
在LsmStorageInner
的scan
扫描函数中,使用布隆过滤器优化代码执行效率:
if table.bloom.is_some()&& !table.bloom.as_ref().unwrap().may_contain(farmhash::fingerprint32(_key))
{continue;
}
Task 3-Key Prefix Encoding + Decoding
在此任务中,您需要修改:
src/block/builder.rs src/block/iterator.rs
由于SST文件是按顺序存储键的,所以有可能用户存储的是相同前缀的键,我们可以在SST编码中对前缀进行压缩,以节省空间。
我们将当前键与块中的第一个键进行比较。我们按如下方式存储密钥:
key_overlap_len (u16) | rest_key_len (u16) | key (rest_key_len)
key_overlap_len
表示块中的第一个key有多少字节相同。例如,如果我们看到一条记录:5|3|LSM,其中块中的第一个key是mini-something
,那么我们可以将当前key恢复为mini-LSM
。完成编码后,还需要在块迭代器中实现解码。根据需要,您可能需要向结构中添加新字段,而不是在启动器代码中提供的字段。
从数据添加进Block
开始,先对BlockBuilder
的add
函数进行改造。
通过下面函数计算出相同的长度:
fn compute_overlap(first_key: KeySlice, key: KeySlice) -> usize {let mut i = 0;loop {if i >= first_key.len() || i >= key.len() {break;}if first_key.raw_ref()[i] != key.raw_ref()[i] {break;}i += 1;}i
}
编码结果如下,先写入相同部分的长度,再写入剩下部分的长度,最后写入差异部分的数据:
let overlap = compute_overlap(self.first_key.as_key_slice(), key);
// Encode key overlap.
self.data.put_u16(overlap as u16);
// Encode key length.
self.data.put_u16((key.len() - overlap) as u16);
// Encode key content.
self.data.put(&key.raw_ref()[overlap..]);
再对BlockIterator
的解码函数seek_to_index
进行改造。
通过下面函数解码出first_key
,因为第一个key
没有比较的对象,所以相似的长度是0
,2
个字节读取后不使用。
impl Block {fn get_first_key(&self) -> KeyVec {let mut buf = &self.data[..];buf.get_u16();let key_len = buf.get_u16();let key = &buf[..key_len as usize];KeyVec::from_vec(key.to_vec())}
}
对原来的key
的读取进行改造:
let overlap_len = entry.get_u16() as usize;
let key_len = entry.get_u16() as usize;
let key = &entry[..key_len];
self.key.clear();
self.key.append(&self.first_key.raw_ref()[..overlap_len]);
self.key.append(key);
先拼接与first_key
相似的部分,再拼接不同的部分。
个人理解:这种编码手段是一种简单且有效的编码手段。但是编码不一定有收益,若没有收益的情况强行使用这种编码方式,反而会带来空间的膨胀。