mini-lsm通关笔记Week1Day2
Summary
在本章中,您将:
- 实现memtable迭代器。
- 实现合并迭代器。
- 对memtable进行LSM读路径
scan
函数。
要将测试用例复制到starter目录下中并运行它们,
cargo x copy-test --week 1 --day 2
cargo x scheck
在今天的任务中主要是实现下面一层一层的迭代器:
Task 1: Memtable Iterator
在本章中,我们将实现LSM
scan
接口,scan
使用迭代器API按顺序返回一系列键值对。在上一章中,您已经实现了get
API和创建不可变memtable的逻辑,您的LSM state现在应该有多个memtable。您需要首先在单个memtable上创建迭代器,然后在所有memtable上创建合并迭代器,最后实现迭代器的范围限制。在此任务中,您需要修改:
src/mem_table.rs
所有的LSM迭代器都实现了
StorageIterator
特性。它有4个函数:key
、value
、next
和is_valid
。当迭代器被创建时,它的游标将停止在某个元素上,而key
/value
将返回memtable/block/SST中满足开始条件的第一个键(即开始键)。这两个接口会返回一个&[u8]
,以避免复制。请注意,这个迭代器接口不同于Rust风格的迭代器。
next
将游标移动到下一个位置。is_valid
函数会在迭代器已经到达末尾或出错返回false
。你可以假设只有当is_valid
返回true
时才会调用next
。Task3中将有一个用于迭代器的FusedIterator
包装器,当迭代器无效时,它会阻止对next
的调用,以避免用户误用迭代器。回到memtable迭代器。你应该已经发现迭代器没有任何与之相关的生命周期。假设你创建了一个
Vec<u64>
并调用vec.iter()
,迭代器类型将是类似于VecIterator<'a>
的东西,其中'a
是vec
对象的生命周期。SkipMap
也是如此,它的iter
API返回一个具有生命周期的迭代器。然而,在我们的情况下,我们不希望在迭代器上有这样的生命周期,以避免使系统过于复杂(并且难以编译...)。如果迭代器没有生命周期泛型参数,我们应该确保每当使用迭代器时,底层的skiplist对象都不会被释放。实现这一点的唯一方法是将
Arc<SkipMap>
对象放入迭代器本身。要定义这样的结构,pub struct MemtableIterator { map: Arc<SkipMap<Bytes, Bytes>>, iter: SkipMapRangeIter<'???>, }
好了,问题来了:我们想表达迭代器的生命周期和结构体中的map是一样的。我们怎么能做到呢?
这是你在本教程中遇到的第一个也是最棘手的Rust语言的东西——自引用结构。如果可以这样写:
pub struct MemtableIterator { // <- with lifetime 'this map: Arc<SkipMap<Bytes, Bytes>>, iter: SkipMapRangeIter<'this>, }
那么问题就解决了!你可以在一些第三方库的帮助下实现这一点,比如ouroboros。它提供了一种简单的方法来定义自引用结构。使用不安全的Rust也可以做到这一点(事实上,我们自己内部使用不安全的Rust...)
我们已经为您定义了自引用
MemtableIterator
字段,您需要实现MemtableIterator
和Memtable::scan
API。
进行本章的内容,需要先对ouroboros
三方工具库有一定了解:https://docs.rs/ouroboros/latest/ouroboros/attr.self_referencing.html
对于以下定义的自引用结构体:
#[self_referencing]
struct MyStruct {
tail_field: i32,
int_data: i32,
float_data: f32,
#[borrows(int_data)]
int_reference: &'this i32,
#[borrows(mut float_data)]
float_reference: &'this mut f32,
}
其中int_data
有一个不可变的借用int_reference
,float_data
有一个可变的借用float_reference
,tail_field
是没有被借用的字段。
如果需要获取字段的值可以使用borrow_{field_name}()
方法:
my_value.borrow_tail_field()
my_value.borrow_int_reference()
my_value.borrow_int_data()
my_value.borrow_float_reference()
如果需要获取到字段的可变引用然后修改值,需要使用with_mut
或者with_{field_name}_mut
:
my_value.with_mut(|fields| {
**fields.float_reference = (**fields.int_reference as f32) * 2.0;
*fields.tail_field = 12;
});
my_value.with_float_reference_mut(|float_ref| {
**float_ref = 32.0
});
MemtableIterator
value方法:
结构体的属性不能直接访问,需要通过self.borrow_{field_name}
访问。先通过self.borrow_item()
获取到item
这个元组,再通过self.borrow_item().1
获取到第二个元素,因为返回的是字符串切片所以变成self.borrow_item().1[..]
,为了不产生拷贝最终变成:
&self.borrow_item().1[..]
借助ouroboros::self_referencing
方法,能简单的实现返回引用。
is_valid方法:
判断元组第一个元素是否为空:
!self.borrow_item().0.is_empty()
key方法:
KeySlice::from_slice(&self.borrow_item().0[..])
next方法:
可以使用with_iter_mut
方法获取到iter
的可变引用,通过with_item_mut
可以获取到item
的可变引用。
let entry = self.with_iter_mut(|iter| {
let entry = iter.next();
match entry {
None => { (Bytes::new(), Bytes::new()) }
Some(entry) => {
(entry.key().clone(), entry.value().clone())
}
}
});
self.with_item_mut(|item|{
*item = entry;
});
Ok(())
scan
通过ouroboros::self_referencing
的构造函数生成对象,注意iter
字段的复制需要使用闭包的方式赋值给iter_builder
。
let (lower, upper) = (map_bound(_lower), map_bound(_upper));
let mut iterator = MemTableIteratorBuilder {
map: self.map.clone(),
iter_builder: |map| map.range((lower, upper)),
item: (Bytes::new(), Bytes::new()),
}
.build();
iterator.next().unwrap();
iterator
Task 2-Merge Iterator
在此任务中,您需要修改:
src/iterors/merge_iterator.rs
现在你有了多个memtable,你将创建拥有多个memtable的迭代器。您需要合并memtables中的结果,并将每个键的最新版本返回给用户。
MergeIterator在内部维护了一个二叉堆(BinaryHeap)。请注意,您需要处理错误(即当迭代器无效时),并确保键值对的最新版本。
例如,如果我们有以下数据:
iter1: b->del, c->4, d->5 iter2: a->1, b->2, c->3 iter3: e->4
合并迭代器输出的顺序应该是:
a->1、b->del、c->4、d->5、e->4
合并迭代器的构造函数接受迭代器的数组Vec。我们假设索引较小的那个(即第一个)拥有最新的数据。
一个常见的陷阱是错误处理。例如:
let Some(mut inner_iter) = self.iters.peek_mut() { inner_iter.next()?; // <- will cause problem }
如果next返回错误(即,由于磁盘故障、网络故障、校验和错误等),则不再有效。但是,当我们走出if条件并将错误返回给调用者时,PeekMut的drop将尝试在堆中移动元素,这导致访问无效的迭代器。因此,您需要自己完成所有错误处理,而不是使用?在PeekMut的范围内。
我们希望尽量避免动态分派,因此我们在系统中不使用
Box<dyn StorageIterator>
。相反,我们更倾向于使用泛型进行静态分派。另请注意,StorageIterator
使用泛型关联类型(GAT),因此它可以同时支持KeySlice
和&[u8]
作为键类型。我们将更改KeySlice
以在第3周中包含时间戳,现在为它使用单独的类型可以使过渡更加平滑。从本节开始,我们将使用
Key<T>
来表示LSM
键类型,并将它们与类型系统中的值区分开来。你应该使用Key<T>
提供的API,而不是直接访问内部值。在第3部分中,我们将为这个键类型添加时间戳,使用键抽象将使转换更加平滑。目前,KeySlice
等价于&[u8]
,KeyVec
等价于Vec<u8>
,KeyBytes
等价于Bytes
。
二叉堆(BinaryHeap)其实就是一个优先队列,先查看其比较规则:
match self.1.key().cmp(&other.1.key()) {
cmp::Ordering::Greater => Some(cmp::Ordering::Greater),
cmp::Ordering::Less => Some(cmp::Ordering::Less),
cmp::Ordering::Equal => self.0.partial_cmp(&other.0),
}
先比较其StorageIterator
当前元素的key
值,如果相同再比较Vec
索引下标的值。再验证一下,修改BinaryHeap
中的元素,BinaryHeap
是否会重新排列:
use std::collections::BinaryHeap;
#[derive(Debug, Eq, PartialOrd, PartialEq)]
#[derive(Ord)]
struct MyStruct {
x: i32,
y: String,
}
fn main() {
let mut heap = BinaryHeap::new();
let xiaoming = MyStruct {
x: 16,
y: String::from("xiaoming"),
};
let xiaohong = MyStruct {
x: 17,
y: String::from("xiaohong"),
};
heap.push(xiaoming);
heap.push(xiaohong);
println!("{:?}", heap.peek());
let xiaohong = heap.peek_mut();
xiaohong.unwrap().x = 1;
println!("{:?}", heap.peek());
}
输出:
Some(MyStruct { x: 17, y: "xiaohong" })
Some(MyStruct { x: 16, y: "xiaoming" })
可知BinaryHeap
的堆顶元素永远是key
值最小且最新的那个。
create
构造函数实现:
let mut iter = MergeIterator {
iters: BinaryHeap::new(),
current: None,
};
if iters.iter().all(|x| !x.is_valid()) && !iters.is_empty() {
let mut iters = iters;
iter.current = Some(HeapWrapper(0, iters.pop().unwrap()));
return iter;
}
for (index, storage_iter) in iters.into_iter().enumerate() {
if storage_iter.is_valid() {
iter.iters.push(HeapWrapper(index, storage_iter));
}
}
if !iter.iters.is_empty() {
iter.current = Some(iter.iters.pop().unwrap())
}
iter
-
创建一个默认对象
-
判断
Vec
数组里面的元素是否都为非法值,如果都是非法值则将current
随便赋值一个。因为在使用MergeIterator
也会先调用is_valid
方法 -
将
Vec
数组里面有合法元素的迭代器都加到堆中 -
取堆顶的元素赋值给
current
key/value
current
元素中迭代器的key
/value
方法:self.current.as_ref().unwrap().1.key()
、self.current.as_ref().unwrap().1.value()
。
is_valid
先判断是否为None
,再调用current
中迭代器的is_valid
方法:
if let None = self.current {
return false;
}
self.current.as_ref().unwrap().1.is_valid()
next
let current = self.current.as_mut().unwrap();
while let Some(mut inner_iter) = self.iters.peek_mut() {
if inner_iter.1.key() != current.1.key() {
break;
}
if let e @ Err(_) = inner_iter.1.next() {
PeekMut::pop(inner_iter);
return e;
}
if !inner_iter.1.is_valid() {
PeekMut::pop(inner_iter);
}
}
current.1.next()?;
if !current.1.is_valid() {
if let Some(iter) = self.iters.pop() {
*current = iter;
}
return Ok(());
}
if let Some(mut inner_iter) = self.iters.peek_mut() {
if *current < *inner_iter {
std::mem::swap(&mut *inner_iter, current);
}
}
Ok(())
-
先把和当前
current
迭代器key相同的迭代器移除。 -
当前
current
迭代器取下一个元素 -
当前
current
迭代器非法,从堆顶pop
出一个迭代器 -
当前
current
迭代器合法,和堆顶迭代器比较,若小于则交换
Task 3-LSM Iterator + Fused Iterator
在此任务中,您需要修改:
src/lsm_iterator.rs
我们使用LsmIterator结构来表示内部的LSM迭代器。当系统中添加了更多的迭代器时,您将需要在整个教程中多次修改此结构。目前,因为我们只有多个memtable,所以它应该定义为:
类型
LsmIteratorInner=MergeIterator<MemTableIterator>
;您可以继续实现LsmIterator结构,它调用相应的内部迭代器,并且也跳过删除的键。
我们不在此任务中测试LsmIterator。在任务4中,将有一个集成测试。
然后,我们希望在迭代器上提供额外的安全性,以避免用户误用它们。当迭代器无效时,不应调用key、value或next。同时,如果next返回错误,则不应该再使用迭代器。FusedIterator是一个围绕迭代器的包装器,用于规范化所有迭代器的行为。你可以自己去实现它。
LsmIterator
主要还是直接调用inner
,就是在空value
的时候需要跳到下一个键值对:
impl LsmIterator {
pub(crate) fn new(iter: LsmIteratorInner) -> Result<Self> {
let mut lsm = Self { inner: iter };
if lsm.is_valid() && lsm.value().is_empty() {
lsm.next();
}
Ok(lsm)
}
}
impl StorageIterator for LsmIterator {
type KeyType<'a> = &'a [u8];
fn is_valid(&self) -> bool {
self.inner.is_valid()
}
fn key(&self) -> &[u8] {
self.inner.key().raw_ref()
}
fn value(&self) -> &[u8] {
self.inner.value()
}
fn next(&mut self) -> Result<()> {
self.inner.next();
if self.inner.is_valid() && self.inner.value().is_empty() {
return self.next();
}
Ok(())
}
}
FusedIterator
判断是否有异常、是否合法,否则报错
impl<I: StorageIterator> StorageIterator for FusedIterator<I> {
type KeyType<'a> = I::KeyType<'a> where Self: 'a;
fn is_valid(&self) -> bool {
!self.has_errored && self.iter.is_valid()
}
fn key(&self) -> Self::KeyType<'_> {
if !self.is_valid() {
panic!("invalid access to the underlying iterator");
}
self.iter.key()
}
fn value(&self) -> &[u8] {
if !self.is_valid() {
panic!("invalid access to the underlying iterator");
}
self.iter.value()
}
fn next(&mut self) -> Result<()> {
if self.has_errored {
bail!("the iterator is tainted");
}
if self.iter.is_valid() {
if let Err(e) = self.iter.next() {
self.has_errored = true;
return Err(e);
}
}
Ok(())
}
}
Task 4-Read Path - Scan
在此任务中,您需要修改:
src/lsm_storage.rs
我们终于实现了——有了所有已经实现的迭代器,您终于可以实现LSM引擎的扫描接口了。你可以简单地用memtable迭代器构造一个LSM迭代器(记得把最新的memtable放在merge迭代器的前面),然后你的存储引擎就可以处理扫描请求了。
将此前写的迭代器用于scan接口:
let snapshot = {
let guard = self.state.read();
Arc::clone(&guard)
};
let mut memtable_iters = Vec::with_capacity(snapshot.imm_memtables.len() + 1);
memtable_iters.push(Box::new(snapshot.memtable.scan(_lower, _upper)));
for memtable in snapshot.imm_memtables.iter() {
memtable_iters.push(Box::new(memtable.scan(_lower, _upper)));
}
Ok(FusedIterator::new(LsmIterator::new(
MergeIterator::create(memtable_iters),
)?))