mini-lsm通关笔记Week1Day1

使用RustRover打开项目,按照如下命令执行,运行第一天的ut检查:

cargo x copy-test --week 1 --day 1
cargo x scheck

有以下报错:

no such command: nextest

在这里插入图片描述

需要先安装nextest

cargo install cargo-nextest

Task1-SkipList Memtable

首先,让我们实现LSM存储引擎的内存结构——memtable。我们选择crossbeam的skiplist实现作为memtable的数据结构,因为它支持无锁的并发读写。我们不会深入讨论skiplist是如何工作的,简而言之,它是一个有序的键值映射,很容易允许并发读写。

crossbeam-skiplist提供了与Rust标准库的BTreeMap类似的接口:insert、get和iter。唯一的区别是修改接口(即插入)只需要对Skiplist的不可变引用,而不是可变引用。因此,在您的实现中,您不应该在实现memtable结构时使用任何互斥锁。

您还会注意到MemTable结构没有删除接口。在mini-lsm实现中,删除被表示为对应空值的键。

在此任务中,您需要实现MemTable::get和MemTable::put以启用对memtable的修改。

我们使用bytes类型来存储memtable中的数据,byte::Byte类似于Arc<[u8]>。当你克隆Bytes,或者获取Bytes的切片时,底层数据不会被复制,因此克隆数据的内存代价是很少的。相反,它只是创建一个对存储区域的新引用,当没有对该区域的引用时,存储区域的内存将被释放。

mini-lsm-starter文件夹下找到mem_table.rs,这个是我们要实现的文件。week1_day1.rs是单元测试文件,可以看到任务1有两个相关的单测

  • test_task1_memtable_get
  • test_task1_memtable_overwrite

用到了MemTable的createfor_testing_put_slicefor_testing_get_slice三个方法。

create

需要实现MemTable的构造函数,其中id是基础数据类型直接使用构造函数的入参_idwalOption类型先赋值为None的变体。mapapproximate_size都是被Arc包裹的智能指针。

MemTable {
    id: _id,
    map: Arc::new(SkipMap::new()),
    wal: None,
    approximate_size: Arc::new(AtomicUsize::new(0)),
}

for_testing_put_slice

实际调用的是put方法。需要在该方法中对map进行insert操作:

self.map
    .insert(Bytes::copy_from_slice(_key), Bytes::copy_from_slice(_value));
Ok(())

返回值Result<T>等价于Result<T, anyhow::Error>。需要注意的是,map的键值都是Bytes类型,且需要获取所有权,需要使用Bytes的构造函数将字符串切片转换为Bytes类型。

for_testing_get_slice

实际调用的是get方法。需要在该方法中对map进行get操作:

match self.map.get(_key) {
    Some(entry) => Some(entry.value().clone()),
    None => None,
}

map.get返回的是一个Entry<Bytes,Bytes>对象,返回的只需要其值的部分。entry.value()返回的是值的引用,因为rust不能返回值的引用避免悬垂引用,所以还需要加上.clone()

第一个任务只是对SkipMap的一个封装。

Task2-SkipList Memtable

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

src/lsm_storage.rs

现在,我们将把我们的第一个数据结构memtable添加到LsmStorageState。在LsmStorageState::create中,你会发现当创建一个LSM结构体时,我们会初始化一个id为0的memtable。这是处于初始状态的可变memtable。在任何时间点,引擎都只有一个可变memtable。memtable通常有大小限制(即256MB),当达到大小限制时,它将被冻结为不可变的memtable。

查看lsm_storage.rs,你会发现有两个结构体表示一个存储引擎:MiniLSM和LsmStorageInner。MiniLSM是LsmStorageInner的一个薄包装器。您将在LsmStorageInner中实现大部分功能,直到第2周压缩。

LsmStorageState存储LSM存储引擎的当前结构。目前,我们将只使用memtable字段,它存储了当前可变的memtable。在此任务中,您需要实现LsmStorageInner::get、LsmStorageInner::put和LsmStorageInner::delete。它们都应该直接将请求调度到当前的memtable。

您的删除实现应该为该键简单地放置一个空切片,我们将其称为删除墓碑。您的get实现应该相应地处理这种情况。

要访问memtable,您需要使用状态锁。由于我们的memtable实现put操作只需要一个不可变的引用,因此您只需要在state上获取读锁即可修改memtable。这允许从多个线程并发访问memtable。

get

先获取读锁再获取数据,如果为空则返回None

match self.state.read().memtable.get(_key) {
    None => { Ok(None) }
    Some(value) => {
        if value.is_empty() {
            return Ok(None);
        }
        Ok(Some(value))
    }
}

put

先获取读锁,再调用memtableput方法

self.state.read().memtable.put(_key, _value)

注意这里只需要获取读锁就行,如任务书所描述的:我们的memtable实现put操作只需要一个不可变的引用,因此您只需要在state上获取读锁即可修改memtable

delete

同上,就是将value改为""

self.state.read().memtable.put(_key, b"")

Task3-Write Path - Freezing a Memtable

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

src/lsm_storage.rs

src/mem_table.rs

memtable的大小不能连续增长,当它达到大小限制时,我们需要冻结它们(并稍后刷新到磁盘)。您可以在LsmStorageOptions中找到memtable大小限制,它等于SST大小限制(而不是num_memtables_limit)。这不是一个硬限制,你应该尽最大努力冻结memtable。

在此任务中,当put/delete键时,您需要计算大约的memtable大小。这可以通过简单地在调用put时将键和值的总字节数相加来计算。如果一个key被放置了两次,尽管skiplist只包含最新的值,但你可以将它计算在近似的memtable大小中两次。一旦一个memtable达到了限制,你应该调用force_freeze_memtable来冻结这个memtable并创建一个新的memtable。

因为可能有多个线程将数据送入存储引擎,force_freeze_memtable可能会从多个线程并发调用。在这种情况下,您需要考虑如何避免竞争条件。

有多个地方可能需要修改LSM状态:冻结可变的memtable、刷新memtable到SST以及GC/compaction。在所有这些修改过程中,可能存在I/O操作。构造锁定策略的直观方法是:

fn freeze_memtable(&self) {
    let state = self.state.write();
    state.immutable_memtable.push(/* something */);
    state.memtable = MemTable::create();
}

…这样你可以修改任何东西,在你获取LSM state的写锁。

这目前工作正常。但是,请考虑您想要为已创建的每个memtable创建一个write-ahead日志文件的情况。

fn freeze_memtable(&self) {
    let state = self.state.write();
    state.immutable_memtable.push(/* something */);
    state.memtable = MemTable::create_with_wal()?; // <- could take several milliseconds
}

现在,当我们冻结memtable时,在几毫秒内没有其他线程可以访问LSM状态,这会产生延迟的尖峰。

为了解决这个问题,我们可以把I/O操作放到锁区域之外。

fn freeze_memtable(&self) {
    let memtable = MemTable::create_with_wal()?; // <- could take several milliseconds
    {
        let state = self.state.write();
        state.immutable_memtable.push(/* something */);
        state.memtable = memtable;
    }
}

然后,我们在状态写锁区域内没有昂贵的操作。现在,考虑memtable即将达到容量限制的情况,两个线程成功地将两个键放入memtable中,它们都在放入两个键后发现memtable达到容量限制。他们都会对memtable进行大小检查,并决定冻结它。在这种情况下,我们可能会创建一个空的memtable,然后立即冻结。

为了解决这个问题,所有的状态修改都应该通过状态锁来同步。

fn put(&self, key: &[u8], value: &[u8]) {
    // put things into the memtable, checks capacity, and drop the read lock on LSM state
    if memtable_reaches_capacity_on_put {
        let state_lock = self.state_lock.lock();
        if /* check again current memtable reaches capacity */ {
            self.freeze_memtable(&state_lock)?;
        }
    }
}

在以后的章节中,你会经常注意到这种模式。例如,对于L0刷新,

fn force_flush_next_imm_memtable(&self) {
    let state_lock = self.state_lock.lock();
    // get the oldest memtable and drop the read lock on LSM state
    // write the contents to the disk
    // get the write lock on LSM state and update the state
}

这确保了只有一个线程能够修改LSM状态,同时仍然允许并发访问LSM存储。

在本任务中,您需要修改put和delete以遵守memtable的软容量限制。当达到上限时,调用force_freeze_memtable冻结memtable。请注意,我们没有针对此并发场景的测试用例,您需要自己考虑所有可能的竞争条件。此外,请记住检查锁定区域,以确保临界区是所需的最小值。

你可以简单地将下一个memtable id赋值为self.next_sst_id()。注意,imm_memtables存储的memtable是按照从最新到最老的顺序存储的。也就是说,imm_memtables.first()应该是最后一个被冻结的memtable。

首先我们要理解各对象直接的关系

在这里插入图片描述

approximate_size计算

就是在对MenTable进行put操作时,需要计算"大约"的大小:

let add_size = _key.len() + _value.len();
self.approximate_size.fetch_add(add_size, Ordering::Relaxed);
self.map
    .insert(Bytes::copy_from_slice(_key), Bytes::copy_from_slice(_value));
Ok(())

因为approximate_size提供了原子操作fetch_add,所以只需要调用即可。

force_freeze_memtable

先放出代码,再对实现进行介绍:

let new_men_table: Arc<MemTable> = Arc::new(MemTable::create(self.next_sst_id()));
{
    let mut guard = self.state.write();
    let mut snapshot = guard.as_ref().clone();
    let old_men_table = std::mem::replace(&mut snapshot.memtable, new_men_table.clone());
    snapshot.imm_memtables.insert(0,old_men_table);
    snapshot.memtable = new_men_table;
    *guard = Arc::new(snapshot)
}
Ok(())
  1. 先创建MemTable以及其Arc指针,是因为如任务书所表达的,这个操作可能很慢,如果在获取写锁后再进行,会有性能影响。这样能是写锁的代价尽可能的小。

在这里插入图片描述

  1. 获取state写锁guard,以及将state复制snapshot因为state中保存的都是指针,所以这个拷贝也是很快的

在这里插入图片描述

  1. 使用new_men_table替换memtable,原内容返回为old_men_table

在这里插入图片描述

  1. old_men_table插入imm_memtables

在这里插入图片描述

  1. 修改guard指针指向snapshot

在这里插入图片描述

Task4-Read Path - Get

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

src/lsm_storage.rs

现在你有了多个memtable,你可以修改你的读取路径获取函数来获取一个键的最新版本。确保从最新的memtable到最早的memtable进行探测。

这个任务就是需要将get函数进行改造,因为此前的get操作只会从memtable里面读取数据。但是现在的数据来源还有可能是被冻结的memtable.所以也需要遍历imm_memtables中的数据:

if let Some(value) = self.state.read().memtable.get(_key) {
    if value.is_empty() {
        return Ok(None);
    }
    return Ok(Some(value));
}
for x in &self.state.read().imm_memtables {
    if let Some(value) = x.get(_key) {
        if value.is_empty() {
            return Ok(None);
        }
        return Ok(Some(value));
    }
}
Ok(None)
  • 15
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值