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一致。

相关推荐
诗句藏于尽头12 分钟前
内网使用rustdesk搭建远程桌面详细版
笔记
蜡笔小电芯13 分钟前
【C语言】指针与回调机制学习笔记
c语言·笔记·学习
丰锋ff26 分钟前
瑞斯拜考研词汇课笔记
笔记
DKPT2 小时前
Java享元模式实现方式与应用场景分析
java·笔记·学习·设计模式·享元模式
KoiHeng5 小时前
操作系统简要知识
linux·笔记
巴伦是只猫6 小时前
【机器学习笔记Ⅰ】11 多项式回归
笔记·机器学习·回归
DKPT10 小时前
Java桥接模式实现方式与测试方法
java·笔记·学习·设计模式·桥接模式
巴伦是只猫11 小时前
【机器学习笔记Ⅰ】13 正则化代价函数
人工智能·笔记·机器学习
X_StarX17 小时前
【Unity笔记02】订阅事件-自动开门
笔记·学习·unity·游戏引擎·游戏开发·大学生
MingYue_SSS17 小时前
开关电源抄板学习
经验分享·笔记·嵌入式硬件·学习