目录

Mini-LSM Week 1 Day3

Mini-LSM Week 1 Day3

Week1 Day3 的内容,实现 SST block 编解码和 iterator

Task 1: Block Builder

前两章实现了 LSM 在内存中的结构,现在实现 on-disk 的结构,即 Block 块。

Blocks 一般 4-KB 大小(可能因存储介质不同),和操作系统以及 SSD 中的页 page 大小一致。块存储排序后的 key-value pairs。

SST 由多个 blocks 组成,当 memtables 的数量超过了 system limit,就会 flush memtables 成为 SST。这一章实现 encoding and decoding of a block.

需要修改的文件:

src/block/builder.rs
src/block.rs

教程的 block 的编码格式为:

----------------------------------------------------------------------------------------------------
|             Data Section             |              Offset Section             |      Extra      |
----------------------------------------------------------------------------------------------------
| Entry #1 | Entry #2 | ... | Entry #N | Offset #1 | Offset #2 | ... | Offset #N | num_of_elements |
----------------------------------------------------------------------------------------------------

每个 Entry 是 key-value 对:

-----------------------------------------------------------------------
|                           Entry #1                            | ... |
-----------------------------------------------------------------------
| key_len (2B) | key (keylen) | value_len (2B) | value (varlen) | ... |
-----------------------------------------------------------------------

key_len 和 value_len 都是 2 个字节,所以最长长度为 $2^16 = 65535$,一般使用 u16 表示

block 具有大小限制 target_size,除非第一个 key-value pair 超过了 block size,不然你需要保证编码后的 block size 小于或等于 target_size (提供的代码里,target_sizeblock_size 本质一样)

build 被调用时,BlockBuilder 会产生 data part 和 unencoded entry offsets。信息会被存在 Block 结构中,key-value entries 使用 raw 格式,offsets 用单独的 vector,这减少了解码数据时不必要的内存分配和处理开销,你只需要简单地拷贝 raw block data 到 data vector 并且每 2 个 bytes 进行 decode entry offsets,而不是创建 Vec<(Vec<u8>, Vec<u8>)> 这样的结构,在一个 block 和内存中去存所有的 key-value pairs。这样 compact memory layout 更高效。

Block::encodeBlock::decode,你需要按照上述的结构 encode/decode block.

Task 1: Solution

查看 src/block/builder.rs 观察 Block 结构体,offsets Vec<u16> 偏移量, data vec<u8> kv 数据等等,具有 new(block_size: usize) -> Self, add(&mut self, key: KeySlice, value: &[u8]) -> bool, is_empty(&self) -> boolbuild(self) -> Block 方法。

/// Builds a block.
pub struct BlockBuilder {
    /// Offsets of each key-value entries.
    offsets: Vec<u16>,
    /// All serialized key-value pairs in the block.
    data: Vec<u8>,
    /// The expected block size.
    block_size: usize,
    /// The first key in the block
    first_key: KeyVec,
}

/// Creates a new block builder.
pub fn new(block_size: usize) -> Self {
    Self {
        offsets: Vec::new(),
        data: Vec::new(),
        block_size,
        first_key: KeyVec::new(),
    }
}

先实现 new(block_size) 方法创建一个 BlockBuilder 结构体,需要注意这里使用了 bytes::BufMut 库,BufMutbytes 库中的一个 trait,它定义了一些方法来操作可变的字节缓冲区。通过实现 BufMut,你可以为自定义类型添加一些方便的方法来操作字节数据。

BufMut trait 中定义了一个 put 方法,用于将数据写入缓冲区。这个方法可以用于多种类型,包括 u8&[u8]&str 等。此时 Vec 使用 put 也更合理,持批量写入缓冲区,减少内存复制。

然后实现 add 函数,向 Block 添加一个 key-value 值,首先需要知道数据的 offset 即存放的位置 self.data.len(),然后按照格式 key_len key value_len value 存入数据。但需要注意判断当前编码后的 Block 是否超过了 block_size(注意:除非第一个 key-value pair 超过了 block size):

/// Adds a key-value pair to the block. Returns false when the block is full.
#[must_use]
pub fn add(&mut self, key: KeySlice, value: &[u8]) -> bool {
    // data: [u8] | offsets: [u16] | num of elements: u16
    if !self.is_empty() && /* can store one large key */
    self.data.len() + self.offsets.len() * 2 + 2 +  /* current encode */
    key.len() + 2 + value.len() + 2 + 2 /* key_len: 2b | key: keylen | value_len: 2b | value: val_len + offset */
    > self.block_size
    {
        return false;
    }

    if self.offsets.is_empty() {
        self.first_key = key.to_key_vec();
    }
    self.data.put_u16(key.len() as u16);
    self.data.put(key.into_inner());
    self.data.put_u16(value.len() as u16);
    self.data.put(value);

    self.offsets.push(self.data.len() as u16);
    true
}

/// Check if there is no key-value pair in the block.
pub fn is_empty(&self) -> bool {
    self.offsets.is_empty()
}

/// Finalize the block.
pub fn build(self) -> Block {
    Block {
        offsets: self.offsets,
        data: self.data,
    }
}

所以检查 Block::is_empty 是否为空就很简单了,只需要检查 offset 数组是不是空的,同时 build 就直接返回当前的 Block 带上 offsetdata

然后实现 Block::encode 编码数据,按照 data offset 这种编码格式 encode,即教程中的 拷贝 raw block data 到 data vector 并且每 2 个 bytes 进行 decode entry offsets,由于此时的 data 是 Vec<u8> 类型,需要转成 Bytes 返回,可以直接使用 .into() 方法。但是教程提到只需要一个 Vec<u8>,所以需要将 offsets 也塞到 data 里去:

impl Block {
    /// Encode the internal data to the data layout illustrated in the tutorial
    /// Note: You may want to recheck if any of the expected field is missing from your output
    pub fn encode(&self) -> Bytes {
        let mut buf = self.data.clone();
        for offset in &self.offsets {
            buf.put_u16(*offset);
        }
        // num of elements
        buf.put_u16(self.offsets.len() as u16);
        buf.into()
    }

    /// Decode from the data layout, transform the input `data` to a single `Block`
    pub fn decode(data: &[u8]) -> Self {
        let offsets_len = (&data[data.len() - 2..data.len()]).get_u16() as usize;
        let offsets = (&data[data.len() - 2 - offsets_len * 2..data.len() - 2])
            .chunks(2)
            .map(|mut chunk| chunk.get_u16())
            .collect();
        let data = data[..data.len() - 2 - offsets_len * 2].to_vec();
        Self { data, offsets }
    }
}

相应的,decode 也是从一个 data: &[u8] 取得所有的 data 和相应的 offset,从缓冲区最后一个 u16 可以获得 offset 数组的长度

代码里的所有 2 都是 u16 的长度,更规范应该使用常量来表示,比如 pub(crate) const SIZEOF_U16: usize = std::mem::size_of::<u16>();

Task 2: Block Iterator

修改 src/block/iterator.rs,这一小节,因为有了 encoded block,需要实现 BlockIterator 接口,使得用户可以 lookup/scan blocks 里的 keys。

BlockIterator 可以被 Arc<Block> 实现,如果 create_and_seek_to_first 被调用,它会放在 block 的第一个 key。如果 create_and_seek_to_key 被调用,iterator 会被放在第一个 >= 大于等于相应 key 的位置,比如 1, 3, 5 在一个 Block 时

let mut iter = BlockIterator::create_and_seek_to_key(block, b"2"); // 创建 key 2
assert_eq!(iter.key(), b"3"); // 此时 iterator 位置在第一个大于等于 2 的位置即 3 的位置

上面的 seek 2 将使迭代器定位在下一个可用键 2,在本例中为 3

iterator 应该从 block 拷贝 key 并且存到 iterator 本身(未来会有 key compression 压缩的内容),对于值 value,必须在 iterator 存储起始/结束 begin/end offset 偏移,并且不能拷贝。

next 被调用,iterator 会移动到下一个位置。如果抵达 block 结束位置,可以设置 key 为空然后从 is_valid 返回 false,这样调用者可以切换到另外的 block。

Task 2: Solution

本节需要实现的函数较多,先观察 BlockIterator::new 构造函数,传入一个 Arc<Block>,但观察测试,基本都没有使用这个构造函数而是使用 create_and_... 等函数。

所以 create_and_seek_to_first(block: Arc<Block>)create_and_seek_to_key(block: Arc<Block>, key: KeySlice) 都接受一个原子计数引用,调用构造函数,调用相应函数,返回迭代器:

pub fn create_and_seek_to_first(block: Arc<Block>) -> Self {
    let mut iter = Self::new(block);
    iter.seek_to_first();
    iter
}

/// Creates a block iterator and seek to the first key that >= `key`.
pub fn create_and_seek_to_key(block: Arc<Block>, key: KeySlice) -> Self {
    let mut iter = Self::new(block);
    iter.seek_to_key(key);
    iter
}

对于 key value 函数,直接返回当前的 entry:

/// Returns the key of the current entry.
pub fn key(&self) -> KeySlice {
    self.key.as_key_slice()
}

/// Returns the value of the current entry.
pub fn value(&self) -> &[u8] {
    &self.block.data[self.value_range.0..self.value_range.1]
}

/// Returns true if the iterator is valid.
/// Note: You may want to make use of `key`
pub fn is_valid(&self) -> bool {
    !self.key.is_empty()
}