这一小节实现基于之前的Block
完成SST
的编解码、迭代器。
代码仓库:https://github.com/ToniXWD/toni-lsm
1 SST设计
上一小节已经介绍了Block
和SST
的关系, 以及SST
和Immutable MemTable
的关系。不过缺失了SST
的编码方式。
参考Minni LSM
的编码方式: SST
文件由多个Block
组成,每个Block
的格式如下:

这里的每个Block_x
就是上一节介绍的编码格式, 其后会追加32位的哈希值,用于校验, 这也是为什么之前的Block::decode
需要一个参数来判断Block
是否包含哈希值的原因。每个Block
对应一个Meta
, 每个Meta
记录这个Block
在SST
文件中的偏移量、第一个和最后一个key
的元数据(长度和大小)。
对每个Block
的索引和Block
不同, 需要尤其注意。因为Block
的数据是已经加载到内存中的, 而SST
则是存在磁盘上的, 所以这里需要根据Meta Section Offset
定位到Meta Section
并将其读取到内存,然后根据索引在内存中读取对应的Meta
, 再从文件系统重读取真正的Block
, 读取的字节序列交由我们之前实现的Block::decode
处理。也就是说, 第一次读取Meta Section
之后其就常驻于内存中, 之后的Block
的元数据读取都是在内存中进行的。(这里还没有实现缓存池管理, 因此后续的代码可能会有变化)。
2 SST实现
根据之前描述的编码方式和过程,如下设计:
1 2 3 4 5 6 7 8 9 10 11
| class BlockMeta { friend class BlockMetaTest; public: size_t offset; std::string first_key; std::string last_key; static void encode_meta_to_slice(std::vector<BlockMeta> &meta_entries, std::vector<uint8_t> &metadata); static std::vector<BlockMeta> decode_meta_from_slice(const std::vector<uint8_t> &metadata); BlockMeta(); BlockMeta(size_t offset, const std::string& first_key, const std::string& last_key); };
|
这里的重要函数只有encode_meta_to_slice
和decode_meta_from_slice
,分别用于将BlockMeta
编码到字节序列和从字节序列解码出BlockMeta
。这里的实现也就是比较繁琐, 倒没有特别易错的地方和技术难度, 这里就不列出了, 有兴趣的可以看代码。
2.2 SST
这里把SST
分为SSTBuilder
和SST
两个类,SSTBuilder
用于构建SST
文件,SST
用于读取SST
文件。SSTBuilder
执行刷盘后会返回SST
描述类, SST
描述类中对SST
文件的读取和解析需要是惰性的(后续实现缓存池管理后代码可能会有变化)。
这里的SST
类的设计如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| class SST : public std::enable_shared_from_this<SST> { friend class SSTBuilder;
private: FileObj file; std::vector<BlockMeta> meta_entries; uint32_t meta_block_offset; size_t sst_id; std::string first_key; std::string last_key;
public: static std::shared_ptr<SST> open(size_t sst_id, FileObj file); static std::shared_ptr<SST> create_sst_with_meta_only(size_t sst_id, size_t file_size, const std::string &first_key, const std::string &last_key); std::shared_ptr<Block> read_block(size_t block_idx);
size_t find_block_idx(const std::string &key);
SstIterator get(const std::string &key);
size_t num_blocks() const;
std::string get_first_key() const;
std::string get_last_key() const;
size_t sst_size() const;
size_t get_sst_id() const;
SstIterator begin(); SstIterator end(); };
class SSTBuilder { private: Block block; std::string first_key; std::string last_key; std::vector<BlockMeta> meta_entries; std::vector<uint8_t> data; size_t block_size; std::vector<uint32_t> key_hashes;
public: SSTBuilder(size_t block_size); void add(const std::string &key, const std::string &value); size_t estimated_size() const; void finish_block(); std::shared_ptr<SST> build(size_t sst_id, const std::string &path); };
|
这里为了后续实现迭代器, SST
类也继承了std::enable_shared_from_this<SST>
,这样在SstIterator
中可以通过shared_from_this
获取SST
的shared_ptr
。
最后这里理清下SST
的构建和读取流程:
构建流程
- 当
MemTable
的大小超过阈值后,准备将MemTable
中最旧的Immutable MemTable
刷出为SST
。
- 先创建一个
SSTBuilder
, 按照迭代器的顺序遍历Immutable MemTable
,将key-value
对添加到SSTBuilder
中:
SSTBuilder
会有一个当前的block
, 首先会调用Block::add_entry
将迭代器的kv
对插入
- 如果当前的
block
容量超出阈值block_size
, 就调用finish_block
将其编码到data
, 并清楚当前block
相关数据
- 遍历完成迭代器的所有
kv
对的插入后, 调用build
将所有的数据刷到文件系统, 并返回一个SST
描述类
读取流程
SST
构造函数会绑定一个文件描述符(这里是我自定义封装的文件读取类FileObj file
)
SST
中的meta entries
从第一次读取后就常驻内存(第一次读取可以是构造函数, 也可以是第一次get
)
- 上层调用
get
时, 会从元数据meta_entries
中进行二分查找, 找到对应的block
的偏移量, 然后调用文件描述对象file
从磁盘中读取
- 读取后的字节流交由
Block::decode
解码得到内存中的Block
- 内存中的
Block
调用之前实现的查询函数完成二分查询
这里介绍完逻辑后, 关键代码给出add
, build
:
add
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| void SSTBuilder::add(const std::string &key, const std::string &value) { if (first_key.empty()) { first_key = key; }
uint32_t hash = static_cast<uint32_t>(std::hash<std::string>{}(key)); key_hashes.push_back(hash);
if (block.add_entry(key, value)) { last_key = key; return; }
finish_block();
block.add_entry(key, value); first_key = key; last_key = key; }
|
build
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| std::shared_ptr<SST> SSTBuilder::build(size_t sst_id, const std::string &path) { if (!block.is_empty()) { finish_block(); }
if (meta_entries.empty()) { throw std::runtime_error("Cannot build empty SST"); }
std::vector<uint8_t> meta_block; BlockMeta::encode_meta_to_slice(meta_entries, meta_block);
uint32_t meta_offset = data.size();
std::vector<uint8_t> file_content = std::move(data);
file_content.insert(file_content.end(), meta_block.begin(), meta_block.end());
file_content.resize(file_content.size() + sizeof(uint32_t)); memcpy(file_content.data() + file_content.size() - sizeof(uint32_t), &meta_offset, sizeof(uint32_t));
FileObj file = FileObj::create_and_write(path, file_content);
auto res = std::make_shared<SST>();
res->sst_id = sst_id; res->file = std::move(file); res->first_key = meta_entries.front().first_key; res->last_key = meta_entries.back().last_key; res->meta_block_offset = meta_offset; res->meta_entries = std::move(meta_entries);
return res; }
|
至于get
, 需要先实现迭代器
3 SST迭代器实现
要实现SST
的迭代器, 需要记录当前的Block
索引, Block
中的Entry
索引, 因此也需要原SST
类的this
指针, 之前已经介绍过enable_shared_from_this
了, 不再赘述。
首先看迭代器的定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| class SST;
class SstIterator { friend SST;
private: std::shared_ptr<SST> m_sst; size_t m_block_idx; std::shared_ptr<BlockIterator> m_block_it;
public: using value_type = std::pair<std::string, std::string>;
SstIterator(std::shared_ptr<SST> sst); SstIterator(std::shared_ptr<SST> sst, const std::string &key);
void seek_first(); void seek(const std::string &key); bool is_end(); std::string key(); std::string value();
SstIterator &operator++(); SstIterator operator++(int) = delete; bool operator==(const SstIterator &other) const; bool operator!=(const SstIterator &other) const; value_type operator*() const; };
|
这里使用m_sst
, m_block_idx
和m_block_it
分别记录原始的SST
类对象、当前Block
在SST
中的位置、当前迭代器在Block
中的位置。
这里比较重要的是定位迭代器的函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| void SstIterator::seek_first() { if (!m_sst || m_sst->num_blocks() == 0) { m_block_it = nullptr; return; }
m_block_idx = 0; auto block = m_sst->read_block(m_block_idx); m_block_it = std::make_shared<BlockIterator>(block); }
void SstIterator::seek(const std::string &key) { if (!m_sst) { m_block_it = nullptr; return; }
try { m_block_idx = m_sst->find_block_idx(key); auto block = m_sst->read_block(m_block_idx); if (!block) { m_block_it = nullptr; return; } m_block_it = std::make_shared<BlockIterator>(block, key); } catch (const std::exception &) { m_block_it = nullptr; throw std::runtime_error("could not read a block from m_sst"); } }
|
这里的逻辑也很简单, 就是先使用记录在sst
中的meta_entries
找到包含要查找的key
的Block
(find_block_idx
), 从文件中读取这个Block
(read_block
), 然后再读取的Block
中调用获取指定key
的迭代器的构造函数, 通过BlockIterator
实现在Block
中的定位。
4 下一步是什么?
现在已经有了Blcok
, SST
, MemTable
。我们是不是可以完成基础的CRUD
了呢?
并不是, 首先是我们目前很没有实现文件加载的FileObj
类。这个下一节介绍。
然后就是,SST
的迭代器SstIterator
只包含当前的SST
内容, 但如果不同的SST
中有标记删除的键值对(value
为空), 后续的SST
迭代器会出现bug
, 这个也需要使用之前的堆完成排除。