mini-lsm通关笔记Week1Day7

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 bit
    h = 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

SsTableBuilderadd操作中,添加键的hash值:

self.key_hashes.push(farmhash::fingerprint32(key.raw_ref()));

SsTableBuilderbuild操作中,添加完元信息后,再添加编码后的布隆过滤器:

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);

SsTableopen操作中,先读取解码出布隆过滤器:

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)?;

LsmStorageInnerscan扫描函数中,使用布隆过滤器优化代码执行效率:

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开始,先对BlockBuilderadd函数进行改造。

通过下面函数计算出相同的长度:

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没有比较的对象,所以相似的长度是02个字节读取后不使用。

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相似的部分,再拼接不同的部分。

个人理解:这种编码手段是一种简单且有效的编码手段。但是编码不一定有收益,若没有收益的情况强行使用这种编码方式,反而会带来空间的膨胀。

  • 8
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值