#storage-engine #lsm #persistence #rocksdb #embedded-database #embedded

lsmlite-rs

Helsing 为 sqlite3 的 lsm1 扩展编写的 Rust 绑定(独立方式)

3 个不稳定版本

0.2.1 2023年12月18日
0.2.0 2023年12月7日
0.1.0 2023年7月17日

#579数据库接口

Apache-2.0

1MB
18K SLoC

C 14K SLoC // 0.2% comments Rust 4K SLoC // 0.2% comments

lsmlite-rs

Cargo Documentation

Helsing 为 sqlite3lsm1 扩展编写的 Rust 绑定。

lsmlite-rs 以独立方式(不包含整个 sqlite3 栈)暴露了 sqlite3lsm1 扩展。此扩展是日志结构合并树的优秀实现,在原则上与 RocksDBWiredTiger(MongoDB 的存储引擎)类似。与 RocksDB 不同,例如,lsm1 将数据结构化在稳定存储上,作为一组只读 B 树的集合(在 lsm1 的术语中称为“段”),随着数据库的增长而增加大小。因此,lsm1 遵循 bLSM 的基本设计原则,而不是传统 LSM-Tree 的原则 - 其中数据存储在不可变的有序数组中。这带来了提供出色的即插即用读 I/O 的优势;同时,在写入数据方面也很高效。

lsm1 的其他吸引人的特性

  1. 宽松的许可协议.
  2. 工业级(稳健)的实现。
  3. 单文件实现:src/lsm1/lsm.c
  4. 单文件数据库,具有单写多读 MVCC 事务并发模型。
  5. 面对应用程序或系统故障的数据持久性。
  6. 可以添加压缩和/或加密。
  7. 只读支持。在只读模式下打开的数据库的写入将被拒绝。
  8. 针对一次写入多次读取(WORM)工作负载的优化。数据库基本上成为一个密集打包的 B 树。提高磁盘上数据库所需的空间,同时提供最佳的读取 I/O。

当前版本中实现的主要设计决策

  1. LSM 的主内存级别目前限制在总共 32 MiB 的大小。
  2. 为了避免主内存开销,数据库文件目前没有部分是内存映射的。
  3. 从不同进程访问数据库文件目前是禁用的。这加快了来自不同线程对数据库的操作,因为没有使用共享内存和 POSIX 锁。
  4. 在系统崩溃的情况下具有鲁棒性目前被配置为在性能和安全之间提供良好的权衡。系统崩溃可能不会损坏数据库,但在恢复后可能丢失最近提交的事务。这是 lsm1 的默认耐用性设置。
  5. 在写入数据库后,后台线程(在相关操作模式下)会积极调度。为了保持内存使用和数据安全在控制之下,写入数据库可能会在相应的后台线程将数据从主内存刷新到磁盘(从而减少主内存消耗)和/或检查点volatile 数据(在故障期间可能丢失的数据)时产生更高的延迟。

未来工作

我们的路线图中的相关工作包括

  1. 引擎的更多可配置性。大多数参数目前都在绑定中设置,并没有向(经验)用户公开,例如。其中一些将被作为 crate 的功能公开。
  2. 高性能模式。目前,资源的使用非常保守,例如主内存使用和后台线程的调度。通过允许使用更多主内存以及更积极地调度后台线程,读写性能可以显著提高。实际上,还可以有两个后台线程(除主写入线程外)一起工作,一个将承担数据库文件操作,如从主内存刷新和合并段,而另一个则检查点数据库文件。
  3. WebAssembly/JS 支持 (lsmlite-js)。
  4. Python 绑定 (lsmlite-py) 使用 PyO3。我们了解到现有的 lsm1 Python 绑定,如 python-lsm-db,所以目前这个功能的优先级较低。
  5. 静止加密。

lsm1 版本

  • lsmlite-rs 版本 0.1.0 基于在 2023 年 3 月 22 日发布的 lsm1,包含在 sqlite3-3.41.2 中。合并的 lsm1 文件可以在 src/lsm1/lsm1-ae2e7fc.c 下找到,附有关于如何生成此文件以及如何从官方 SQLite3 源代码中本地更新它的简短说明。

入门指南

以下是如何声明和打开数据库的简短示例,然后插入数据并遍历整个数据库提取所有当前包含的键和值。有关特定主题的附加示例(例如事务或压缩)可以在 examples 目录下找到。

use lsmlite_rs::*;

// Make sure that `/tmp/my_db.lsm` does not exist yet.
let db_conf = DbConf::new("/tmp/", "my_db".to_string());

// Let's declare an empty handle.
let mut db: LsmDb = Default::default();
// Let's initialize the handle with our configuration.
let rc = db.initialize(db_conf);
// Let's connect to the database. It is at this point that the file is produced.
let rc = db.connect();

// Insert data into the database, so that something gets traversed.
// Let's persist numbers 1 to 100 with 1 KB zeroed payload.
let value = vec![0; 1024];
let max_n: usize = 100;
for n in 1..=max_n {
    let key_serial = n.to_be_bytes();
    let rc = db.persist(&key_serial, &value).unwrap();
}

// Let's open the cursor once we have written data (snapshot isolation).
let mut cursor = db.cursor_open().unwrap();
// Let's move the cursor to the very first record on the database.
let rc = cursor.first();
assert!(rc.is_ok());

// Now let's traverse the database extracting the data we just added.
let mut num_records = 0;
while cursor.valid().is_ok() {
    num_records += 1;
    // Extract the key.
    let current_key = Cursor::get_key(&cursor).unwrap();
    // Parse it to an integer.
    assert!(current_key.len() == 8);
    let key = usize::from_be_bytes(current_key.try_into().unwrap());
    // Extract the value.
    let current_value = Cursor::get_value(&cursor).unwrap();
    // Everything should match.
    assert!(key == num_records);
    assert!(current_value.len() == 1024);
    // Move onto the next record.
    cursor.next().unwrap();
}
// We did find what we wanted.
assert_eq!(num_records, max_n);

// EOF
assert!(cursor.valid().is_err());

如何贡献

我们很高兴听到您的想法,欢迎提交反馈和拉取请求。

依赖项

~7–13MB
~164K SLoC