← 文章 / 数据与数据库
Hacker News 8小时前 · 2026-08-29 20:44:18 · 1 阅读

TurboKV:速度惊人的 Rust 键值存储

一个用 Rust 编写的、速度极快的嵌入式键值存储

GitHub License Rust

TurboKV 是一个异步嵌入式键值数据库,具备原子批量写入、有序范围扫描、可配置持久化、压缩与后台 compaction 能力。

安装

cargo add turbokv
cargo add tokio --features full

或直接添加依赖:

[dependencies]
turbokv = "0.6"
tokio = { version = "1", features = ["full"] }

TurboKV 持久化的 Bloom filter 格式使用硬件 AES。x86/x86_64 目标请用 RUSTFLAGS="-C target-feature=+aes,+sse2" 构建,ARM/AArch64 目标请用 RUSTFLAGS="-C target-feature=+aes,+neon" 构建。若二进制只会运行在相同 CPU 型号或其指令集超集上,也可以改用 -C target-cpu=native

快速上手

use turbokv::{Db, DbOptions, WriteBatch};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let db = Db::open_with_options("./my-database", DbOptions::durable()).await?;

    db.insert(b"user:1", b"Ada").await?;
    assert_eq!(db.get(b"user:1").await?, Some(b"Ada".to_vec()));

    let mut batch = WriteBatch::new();
    batch.put(b"user:2", b"Grace");
    batch.put(b"user:3", b"Linus");
    batch.delete(b"user:1");
    db.write_batch(&batch).await?;

    for (key, value) in db.scan_prefix(b"user:").await? {
        println!(
            "{} = {}",
            String::from_utf8_lossy(&key),
            String::from_utf8_lossy(&value)
        );
    }

    db.close().await?;
    Ok(())
}

可运行的示例:

API 一览

持久化预设

预设 确认边界 使用场景
DbOptions::fast() 内存中立即可见;不使用 WAL 缓存与可重现数据
DbOptions::durable() 追加写入 WAL,但每次写入不做 sync 进程崩溃可恢复;推荐默认值
DbOptions::paranoid() WAL 组在返回前完成 sync_all 最强模式,受文件系统/设备保证的约束

每个已打开的 DbEngine 都独占地拥有其数据目录。请使用 close()close_with_status() 进行干净关闭;直接丢弃句柄并不构成干净关闭的契约。

数据库操作

键和值是通过 AsRef<[u8]> 传入的任意字节序列;字符串需由调用方自行编码。所有修改类 API 在返回前会复制其输入。点读与收集式读取返回拥有所有权的 Vec<u8> 值。空值是有效数据,与已删除的键不同。

打开与配置

API 参数 结果与行为
Db::open(path) path: AsRef<Path> DbOptions::durable() 打开或创建目录。打开的句柄独占该目录。
Db::open_with_options(path, options) 数据库路径和一个 DbOptions 以显式的持久化、内存、缓存与压缩设置打开。会拒绝相互矛盾的设置,例如 sync_writes = true 与禁用 WAL 并存。
DbOptions::fast() 返回无 WAL 预设。
DbOptions::durable() 返回进程崩溃可恢复的 WAL 预设。
DbOptions::paranoid() 返回同步完成后才确认的预设。
options.with_compression(compression) 一个 Compression 变体 Builder 风格的更新,返回修改后的选项。

所有预设均以 64 MiB memtable、64 MiB block cache 和 LZ4 压缩起步。它们的公开字段可以在打开数据库前调整:

DbOptions 字段 含义
wal_enabled: bool 将变更追加到 WAL。禁用它意味着在成功 flush 或 close 之前,进程崩溃可能导致数据丢失。
sync_writes: bool 在确认每个变更组之前等待 WAL sync 屏障。需要启用 wal_enabled
memtable_size: usize 触发 memtable 轮换与后台 flush 的近似内存字节阈值。
block_cache_size: usize 解压后 SSTable block cache 的字节预算。设为 0 可禁用缓存。
compression: Compression 新写入数据的 SSTable 压缩方式:Lz4SnappyZstdNone。已有的表保留其编码格式。

点操作、批量操作与原子批

API 参数 返回值与语义
insert(key, value) 字节类型的键和值 Result<()>。插入或替换该键。成功返回前已达到所选的持久化边界。
insert_many(entries) 任意 (key, value) 对的迭代器 Result<()>。完整复制迭代器并按顺序应用各条目;重复键以最后一个为准。这是批量 API,不是单次原子可见性切换。
get(key) 字节类型的键 Result<Option<Vec<u8>>>。缺失或已删除的键返回 None,已存储的空值返回 Some(Vec::new())
remove(key) 字节类型的键 Result<()>。写入墓碑(tombstone);允许删除不存在的键。
contains_key(key) 字节类型的键 Result<bool>。判定结果与 get 一致,目前也会产生与 get 相同的值分配开销。
write_batch(batch) &WriteBatch Result<()>。原子地发布全部操作;读者要么看到批次之前的状态,要么看到完整批次。重复键以最后一个操作为准。

启用 WAL 时,单条记录或完整批次必须能装进 WAL 的 u32 载荷长度。失败或被取消的变更可能已经写入 WAL;重试非幂等操作前,请先检查对应键或重新打开数据库。

WriteBatch 持有每个键和值的副本:

API 参数 效果
WriteBatch::new() 创建空批次。
WriteBatch::with_capacity(capacity) 预期操作数量 预分配操作槽位,但不预分配键或值的字节。
batch.put(key, value) 字节类型的键和值 追加一个拥有所有权的 put 操作。
batch.delete(key) 字节类型的键 追加一个拥有所有权的 delete 操作。
batch.ops() 借用有序的 &[BatchOp] 操作列表。
batch.len() / batch.is_empty() 报告当前操作数量。
batch.clear() 清空所有操作,同时保留批次已分配的内存以便复用。

范围扫描与前缀扫描

按键的原始字节进行字典序排序。每次扫描都会捕获一个一致的时间点视图。创建扫描可能冻结非空的活跃 memtable,因此频繁的小型扫描可能增加后续 flush 的工作量。

API 参数 返回值与分配
range(start, end) 含起始键(含)与结束键(不含) Result<Vec<(Vec<u8>, Vec<u8>)>>;急切地为返回的每个键和值分配内存。
scan_prefix(prefix) 字节前缀;空前缀匹配所有条目 按顺序急切收集所有匹配的键值对。
range_iter(start, end) 相同的 [start, end) 边界 创建 RangeIter。迭代项为 Result<EntryGuard, ScanError>,因为在推进过程中可能发现数据损坏。
scan_prefix_iter(prefix) 字节前缀 创建 PrefixIter,它是同一流式实现的别名。

推进流式迭代器是同步操作,可能执行 mmap 读取、校验和验证、解压以及缓存加锁。请及时丢弃迭代器:它会固定持有其快照读取器和数据库目录的所有权。

迭代器或 guard API 参数 结果
iter.count() 消耗迭代器并返回 Result<usize, ScanError>
iter.keys() 消耗迭代器并收集拥有所有权的键,不物化 memtable 中的值。
iter.collect_pairs() 消耗迭代器并收集拥有所有权的键值对。
iter.paginate(offset, limit) 要跳过的条目数和最多返回的条目数 返回惰性迭代器;被跳过的条目会被遍历,但其 memtable 值不会被复制。
guard.key() 借用键而不加载值。
guard.value() / guard.value_len() 借用值或报告其长度;memtable 中的值只在首次调用 value() 时才会被复制。
guard.into_pair() / into_key() / into_value() 消耗 guard 并返回所请求的拥有所有权的字节。

持久化、维护与统计

API 参数 返回值与开销
flush() Result<()>。排空待处理写入,安装 SSTable 与 manifest,同步 WAL,并回收符合条件的 WAL 段。并发开始的写入可能需要之后再 flush 一次。
compact() Result<CompactionResult>。排空所捕获的 compaction 范围,并报告实际处理的文件数、字节数、耗时、回收的墓碑数,以及是否仍有剩余工作。
status() 开销极小的 DatabaseStatus 快照,涵盖维护故障、重试与写反压。
logical_stats() 精确的 Result<LogicalStats>,统计唯一存活键和字节数。它会扫描物理版本,可能产生 I/O。
physical_stats() 开销极小的 PhysicalStats 度量值与进程生命周期计数器,涵盖 WAL、memtable、SSTable、缓存、停顿与写放大。
stats() 已弃用的混合物理计数器,为源码兼容性保留。
close() 消耗 Db 刷写待处理写入,停止维护任务,成功时释放目录所有权。直接丢弃 Db 不构成干净关闭保证。
close_with_status() 消耗 Db 结构化的关闭形式;可区分存储错误与未解决的 flush 或 compaction 健康问题。

大多数数据库方法返回 DbError。流式迭代器的创建返回 DbError,而之后发现的失败则以 ScanError 形式产生。更底层的 Engine 与组件配置类型属于受支持的高级 API;其完整的字段和方法契约见 crate 文档

基准测试

基准测试使用 TurboKV 0.6.0、fjall 2.11.2 和 redb 2.6.3,每组运行三次重复。吞吐量为每秒确认的键数;数值越高越好。

工作负载 TurboKV Fast TurboKV Durable TurboKV Paranoid fjall Buffer redb Eventual
顺序填充(1 键/事务) 2,989,537 1,774,574 213 485,252 1,397(macOS barrier/事务)
随机填充(1 键/事务) 1,217,087 906,806 226 456,924 1,549(macOS barrier/事务)
覆盖写(1 键/事务) 1,278,894 929,340 210 446,733 1,516(macOS barrier/事务)
顺序批量(100 键/事务) 3,856,202 2,277,031 20,670 511,600 80,197
顺序批量(1,000 键/事务) 3,724,635 2,380,390 162,938 572,671 134,636

Fast 模式禁用 WAL。Durable 模式写入可恢复的 WAL 记录,但不为每次确认同步到持久存储。Paranoid 模式在返回前执行该同步;因此其单键吞吐量受存储同步延迟限制,而显式批次可以把一次 barrier 摊薄到多个键上。

测试协议:200,000 个确定性的 20 字节键、400 字节值(84 MB 逻辑输入,超过 64 MiB memtable),单一调用方,所示处使用原子批次,禁用压缩与 block cache,且不清空 OS page cache。redb 2.6.3 的 Durability::Eventual 会为每个事务执行一次 macOS F_BARRIERFSYNC,而 TurboKV Recoverable 与 fjall Buffer 模式止步于各自的进程崩溃可恢复的 OS 缓存边界。批量化能摊薄 redb 那次固定的 barrier;因此其单键行应理解为架构背景,而非同口径的持久化对比。不同引擎之间的最终落盘耗时未做比较。

测量时间为 2026-08-28 至 29,硬件为 Apple M4(Mac16,1)、32 GiB 内存、macOS 15.3.2(24D81)、APFS、rustc 1.88.0。三个 TurboKV 列的完整原始重复次数、延迟百分位、离散度、依赖版本、字节核算与写放大数据见 mode JSON artifact 及其 text report。 fjall 与 redb 列来自对应的留存 cross-engine artifact。 完整方法论与复现命令见 benchmarks/README.md

原始来源: Hacker News

评论 (0)