mini-lsm通關筆記Week1Day2

余为民同志發表於2024-06-22

在今天的任務中主要是實現下面一層一層的迭代器:

Task 1: Memtable Iterator

在本章中,我們將實現LSM scan介面,scan使用迭代器API按順序返回一系列鍵值對。在上一章中,您已經實現了get API和建立不可變memtable的邏輯,您的LSM state現在應該有多個memtable。您需要首先在單個memtable上建立迭代器,然後在所有memtable上建立合併迭代器,最後實現迭代器的範圍限制。

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

src/mem_table.rs

所有的LSM迭代器都實現了StorageIterator特性。它有4個函式:keyvaluenextis_valid。當迭代器被建立時,它的游標將停止在某個元素上,而key/value將返回memtable/block/SST中滿足開始條件的第一個鍵(即開始鍵)。這兩個介面會返回一個&[u8],以避免複製。請注意,這個迭代器介面不同於Rust風格的迭代器。

next將游標移動到下一個位置。如果迭代器已經到達末尾或出錯,則返回is_valid。你可以假設只有當is_valid返回true時才會呼叫next。Task3中將有一個用於迭代器的FusedIterator包裝器,當迭代器無效時,它會阻止對next的呼叫,以避免使用者誤用迭代器。

回到memtable迭代器。你應該已經發現迭代器沒有任何與之相關的生命週期。假設你建立了一個Vec<u64>並呼叫vec.iter(),迭代器型別將是類似於VecIterator<'a>的東西,其中'avec物件的生命週期。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欄位,您需要實現MemtableIteratorMemtable::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_referencefloat_data有一個可變的借用float_referencetail_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),
)?))

相關文章