mini-lsm通关笔记Week2Day4

项目地址:https://github.com/skyzh/mini-lsm

个人实现地址:https://gitee.com/cnyuyang/mini-lsm

Summary

在本章中,您将:

  • 实现一个分级合并策略,并在合并模拟器上进行仿真。
  • 将分级合并策略纳入系统。

要将测试用例复制到启动器代码中并运行它们,

bash 复制代码
cargo x copy-test --week 2 --day 4
cargo x scheck

本节自己有点偷懒,直接看的参考代码,本文只对参考代码做相关代码注释解释。

Task 1-Leveled Compaction

在第2章,您已经实现了简单的分级合并策略。但是,该实现存在以下几个问题:

合并总是包含一个完整的级别。请注意,在完成合并之前,您无法删除旧文件,因此,在合并进行时(如果是完全合并),您的存储引擎可能会使用2倍的存储空间。分层合并也有同样的问题。在本章中,我们将实现部分合并,我们从上层选择一个SST进行合并,而不是全层。

SST可能会在空位上被合并。正如您在合并模拟器中看到的那样,当LSM状态为空,并且存储引擎转储一些L0 SST时,这些SST将首先合并到L1,然后从L1合并到L2,等等。最佳策略是直接将L0的SST放置到可能的最低的层级。以避免不必要的写放大。

在本章中,您将实现一个生产就绪的分级合并策略。该策略与RocksDB的分级合并相同。您需要修改:

src/compact/leveled.rs

要运行合并模拟器,

cargo run --bin compaction-simulator leveled

Task 1.1-Compute Target Sizes

在此合并策略中,您需要知道每个SST的第一个/最后一个键以及SST的大小。合并模拟器将设置一些mock SST供您访问。

您需要计算当前级别的目标大小。假设base_level_size_mb为200MB,级别数(除L0外)为6。当LSM状态为空时,目标大小将为:

[0 0 0 0 0 200MB]

在底层级别超过base_level_size_mb之前,所有其他中间级别的目标大小将为0。有这个想法的原因是,当数据总量很小的时候,创建中间级别是很浪费的。

当底层达到或超过base_level_size_mb时,我们将通过将大小除以level_size_multiplier来计算其他层的目标大小。假设底层包含300MB的数据,level_size_multiplier=10

0 0 0 0 30MB 300MB

此外,还需要满足最多一个级别的目标大小可以低于base_level_size_mb。假设我们现在在最后一级有30GB的文件,则各级目标大小为

0 0 30MB 300MB 3GB 30GB

请注意,在这种情况下,L1和L2的目标大小为0,而L3是唯一一个目标大小低于base_level_size_mb的级别。

在实现后面的触发器逻辑之前,先需要计算每层的目标值。这个值的大小、层数都不是固定的,会随最底层的大小变动。参考代码:

rust 复制代码
// 声明用于存储目标大小结果的数组
let mut target_size = Vec::with_capacity(self.options.max_levels);
// 计算最低一层大小
let mut last_level_size = _snapshot.levels[self.options.max_levels - 1]
    .1
    .iter()
    .map(|x| _snapshot.sstables.get(x).unwrap().table_size())
    .sum::<u64>() as usize;

// 计算每层目标大小,每层递减为1/level_size_multiplier
for num in 0..self.options.max_levels {
    target_size.insert(0, last_level_size);
    if last_level_size < self.options.base_level_size_mb * 1024 * 1024 {
        last_level_size = 0;
    } else {
        last_level_size = last_level_size / self.options.level_size_multiplier;
    }
}

Task 1.2-Decide Base Level

现在,让我们解决在简单的分级合并策略中SST可以跨空层合并的问题。当我们对L0 SST进行合并时,我们不会直接将其放到L1中。相反,我们将其合并到target size > 0的第一个级别。例如,当目标级别大小为:

0 0 0 0 30MB 300MB

如果L0 SST的数量达到level0_file_num_compaction_trigger阈值,我们将对L0 SST进行合并直接放入使用L5缓存(目标大小为30MB)中。

现在,您可以生成L0合并任务并运行合并模拟器。

--- After Flush ---
L0 (1): [23]
L1 (0): []
L2 (0): []
L3 (2): [19, 20]
L4 (6): [11, 12, 7, 8, 9, 10]

...

--- After Flush ---
L0 (2): [102, 103]
L1 (0): []
L2 (0): []
L3 (18): [42, 65, 86, 87, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 61, 62, 52, 34]
L4 (6): [11, 12, 7, 8, 9, 10]

合并模拟器中的层数为4。所以SST应该直接刷新到L3/L4。

需要在刚刚的计算中添加base_level用于计算:

rust 复制代码
// 声明用于存储目标大小结果的数组
let mut target_size = Vec::with_capacity(self.options.max_levels);
// 默认为最低层
let mut base_level = self.options.max_levels;
// 计算最低一层大小
let mut last_level_size = _snapshot.levels[self.options.max_levels - 1]
    .1
    .iter()
    .map(|x| _snapshot.sstables.get(x).unwrap().table_size())
    .sum::<u64>() as usize;

// 计算每层目标大小,每层递减为1/level_size_multiplier
for num in 0..self.options.max_levels {
    target_size.insert(0, last_level_size);
    if last_level_size < self.options.base_level_size_mb * 1024 * 1024 {
        last_level_size = 0;
    } else {
        last_level_size = last_level_size / self.options.level_size_multiplier;
        // 若当层目标大小不为0MB,则使用当层
        base_level -= 1;
    }
}

// 当所有级别的目标大小都不为0MB时,放置在最顶层
if base_level == 0 {
    base_level = 1;
}

println!("target_size {:?}", target_size);

// L0中SST数量超过阈值level0_file_num_compaction_trigger,合并至base_level
if _snapshot.l0_sstables.len() >= self.options.level0_file_num_compaction_trigger {
    println!("flush L0 SST to base level {}", base_level);
    return Some(LeveledCompactionTask {
        upper_level: None,
        upper_level_sst_ids: _snapshot.l0_sstables.clone(),
        lower_level: base_level,
        lower_level_sst_ids: self.find_overlapping_ssts(
            _snapshot,
            &_snapshot.l0_sstables,
            base_level,
        ),
        is_lower_level_bottom_level: base_level == self.options.max_levels,
    });
}

Task 1.3-Decide Level Priorities

现在我们需要处理L0以下的合并。L0合并总是具有最高优先级,因此如果L0达到阈值,则应首先对其进行合并。之后,我们就可以通过current_size/target_size来计算每一层的合并优先级了。我们只合并此比率> 1.0的级别,将选择比值最大的级别用于较低级别的合并。例如,如果我们有:

L3: 200MB, target_size=20MB
L4: 202MB, target_size=200MB
L5: 1.9GB, target_size=2GB
L6: 20GB, target_size=20GB

合并的优先级为:

L3: 200MB/20MB = 10.0
L4: 202MB/200MB = 1.01
L5: 1.9GB/2GB = 0.95

L3和L4需要分别对其下级进行合并,而L5则不需要。而L3的比值更大,因此我们将生成L3和L4的合并任务。合并完成后,我们很可能会调度L4和L5的合并。

首先按照任务要求,我们需要找到优先级最高的级别:

rust 复制代码
// 当前最大优先级
let mut priority = 0.0f64;
// 选择需要合并的级别
let mut select_level = 0;
for num in 0..(self.options.max_levels - 1) {
    // 目标大小为0的,不会放置合并的sst,不用计算
    if target_size[num] == 0 {
        continue;
    }
    // 本层级大小
    let current_size = _snapshot.levels[num]
        .1
        .iter()
        .map(|x| _snapshot.sstables.get(x).unwrap().table_size())
        .sum::<u64>() as usize;
    // 本层级优先级
    let current_priority = current_size as f64 / target_size[num] as f64;
    if current_priority > 1.0f64 && current_priority > priority {
        // 当前层级为优先级最高层级
        select_level = num + 1;
        priority = current_priority;
    }
    println!(
        "L{},current_size:{},target_size:{},priority:{}",
        (num + 1),
        current_size,
        target_size[num],
        current_priority
    );
}

若找到需要合并的层,则建立合并任务:

rust 复制代码
if select_level > 0 {
    let selected_sst = _snapshot.levels[select_level - 1]
        .1
        .iter()
        .min()
        .copied()
        .unwrap();
    return Some(LeveledCompactionTask {
        upper_level: Some(select_level),
        upper_level_sst_ids: vec![selected_sst],
        lower_level: select_level + 1,
        lower_level_sst_ids: self.find_overlapping_ssts(
            _snapshot,
            &[selected_sst],
            select_level + 1,
        ),
        is_lower_level_bottom_level: (select_level + 1) == self.options.max_levels,
    });
}

Task 1.4-Select SST to Compact

现在,让我们解决简单的分级合并策略中合并总是包含当前级别所有SST的问题。当我们决定合并两个级别时,我们总是从较高的级别中选择最老的SST。通过比较SST id可以知道SST产生的时间。

还有其他选择合并SST的方法,例如,通过查看删除墓碑的数量。您可以将其作为奖励任务的一部分实现。

选择上一级SST后,您需要使用上一级SST的重叠键查找下一级SST中的所有SST。然后,您可以生成一个合并任务,该合并任务在上一级包含正好一个SST,在下一级包含重叠的SST。

当合并完成时,您需要从状态中移除SST,并将新的SST插入到正确的位置。请注意,在除L0之外的所有级别中,应保持SST id按第一个键排序。

运行合并模拟器,您应该看到:

--- After Compaction ---
L0 (0): []
L1 (4): [222, 223, 208, 209]
L2 (5): [206, 196, 207, 212, 165]
L3 (11): [166, 120, 143, 144, 179, 148, 167, 140, 189, 180, 190]
L4 (22): [113, 85, 86, 36, 46, 37, 146, 100, 147, 203, 102, 103, 65, 81, 105, 75, 82, 95, 96, 97, 152, 153]

级别的大小应保持在级别乘数比之下。以及合并任务:

Upper L1 [224.sst 7cd080e..=33d79d04]
Lower L2 [210.sst 1c657df4..=31a00e1b, 211.sst 31a00e1c..=46da9e43] -> [228.sst 7cd080e..> =1cd18f74, 229.sst 1cd18f75..=31d616db, 230.sst 31d616dc..=46da9e43]

...应该只有一个来自上层的SST。

注意:这部分我们不提供细粒度的单元测试。您可以运行合并模拟器并与参考解决方案的输出进行比较,以查看您的实现是否正确。

需要实现一个find_overlapping_ssts函数,函数签名如下:

rust 复制代码
fn find_overlapping_ssts(
    &self,
    snapshot: &LsmStorageState,
    sst_ids: &[usize],
    in_level: usize,
) -> Vec<usize>
  • sst_ids:上层选取的SST,若是L0层的合并则是L0中所有SST,否则只会选取单个
  • in_level:在该层级中找与上述SST中key的范围覆盖的SST

函数实现:

rust 复制代码
fn find_overlapping_ssts(
    &self,
    snapshot: &LsmStorageState,
    sst_ids: &[usize],
    in_level: usize,
) -> Vec<usize> {
    // sst_ids中最小的key
    let begin_key = sst_ids
        .iter()
        .map(|id| snapshot.sstables[id].first_key())
        .min()
        .cloned()
        .unwrap();
    // sst_ids中最大的key
    let end_key = sst_ids
        .iter()
        .map(|id| snapshot.sstables[id].last_key())
        .max()
        .cloned()
        .unwrap();
    let mut overlap_ssts = Vec::new();
    for sst_id in &snapshot.levels[in_level - 1].1 {
        let sst = &snapshot.sstables[sst_id];
        let first_key = sst.first_key();
        let last_key = sst.last_key();
        // 若in_level中的SST在范围中,则选取出来用于合并
        if !(last_key < &begin_key || first_key > &end_key) {
            overlap_ssts.push(*sst_id);
        }
    }
    overlap_ssts
}

Task 2-Integrate with the Read Path

在此任务中,您需要修改:

src/compact.rs
src/lsm_storage.rs

实现应该类似于简单的分层合并。记住更改get/scan读取路径和合并迭代器。

compact.rs修改compact函数,实现CompactionTask::Leveled分支,实现内容和之前的CompactionTask::Simple一致。

相关推荐
LuH11241 小时前
【论文阅读笔记】IC-Light
论文阅读·笔记
是小菜呀!1 小时前
实验四 触发器
笔记
悲伤小伞1 小时前
C++_数据结构_详解二叉搜索树
c语言·数据结构·c++·笔记·算法
灰太狼不爱写代码4 小时前
CUDA11.4版本的Pytorch下载
人工智能·pytorch·笔记·python·学习
Aileen_0v010 小时前
【AI驱动的数据结构:包装类的艺术与科学】
linux·数据结构·人工智能·笔记·网络协议·tcp/ip·whisper
Rinai_R12 小时前
计算机组成原理的学习笔记(7)-- 存储器·其二 容量扩展/多模块存储系统/外存/Cache/虚拟存储器
笔记·物联网·学习
吃着火锅x唱着歌12 小时前
PHP7内核剖析 学习笔记 第四章 内存管理(1)
android·笔记·学习
ragnwang12 小时前
C++ Eigen常见的高级用法 [学习笔记]
c++·笔记·学习
胡西风_foxww12 小时前
【es6复习笔记】rest参数(7)
前端·笔记·es6·参数·rest
胡西风_foxww16 小时前
【es6复习笔记】函数参数的默认值(6)
javascript·笔记·es6·参数·函数·默认值