#index #b-tree #data-structures #storage

bin+lib robt

只读、不可变的B树,用于索引键值对

2个不稳定版本

0.2.0 2021年3月13日
0.1.0 2020年12月24日

#2609 in 数据库接口

MIT 许可证

120KB
3K SLoC

Rustdoc

使用只读B树索引键值对数据集。有关详细信息,请参阅rustdoc链接(上方)。

贡献

  • 简单的工作流程。Fork - 修改 - 提交请求。
  • 在创建PR之前,
    • 运行 make build 以确认所有构建版本都通过,无警告和错误。
    • 运行 check.sh,无警告、无错误,所有测试用例通过。
    • 运行 perf.sh,无警告、无错误,所有测试用例通过。
    • 安装 并运行 cargo spellcheck 以删除常见的拼写错误。
  • 优先考虑开发者起源证书

lib.rs:

包实现了一个不可变的只读BTree索引。

使用 [Builder] 类型构建一个新的索引。然后使用 [Index] 类型加载索引。可以通过克隆 Index 实例来并发访问索引。请注意,单个 Index 实例不能在多个线程之间共享。一旦使用 Builder 类型构建了索引,就无法对其进行修改。虽然严格的不可变性可能会带来不便,但它们具有某些优点:

  • 它们是完全打包的,因此开销较小,树深度较浅。
  • 易于高效缓存btree-blocks。
  • 可以很容易地与不可变的只读Bloom过滤器配对。
  • 对并发访问友好。

功能清单

  • 索引可以针对键类型和值类型进行参数化。
  • 使用 CBOR 进行序列化和反序列化。
  • 键和值类型可以通过 derive(Cborize) 使其符合 robt
  • 值可以存储在叶子节点中,也可以存储在单独的日志文件中。
  • 此外,构建索引时,传入的迭代器可以使用 [Diff] 机制为值提供旧版本。
  • Bloom过滤器可以帮助优化错误查找。
  • 带有Bloom过滤器支持的 get() 操作。
  • API iter()reverse() 操作用于正向和反向迭代。
  • API iter_version()reverse_version() 操作类似于 iter/reverse,但还可以获取条目的旧版本。请注意,iter/reverse 不获取旧版本。

值日志文件

值及其增量(旧版本)可以存储在单独的日志文件中。这有以下优势:

  • 使叶子节点非常紧凑,并有助于更好的缓存。
  • 在构建多层索引时效率更高。
  • 应用程序通常将旧版本作为归档处理。

虽然存储值在值日志文件中是可选的,但增量始终存储在单独的值日志文件中。

构建索引

与支持 set()write()update() 等操作的可变数据结构不同,robt 索引是从预排序的迭代器构建的。从某种意义上说,每个 btree 索引都可以被视为排序的 {key,value} 数据集的不可变快照。典型的流程是:

use mkit::traits::BuildIndex;

let config = Config::new("/opt/data/", "movies");
// use one or more set_ method to configure the btree parameters.
let builder = Build::initial(config, app_meta);
builder.from_iter(iter, mkit::nobitmap::NoBitmap);

// Subsequently open an index as,
let reader1 = Index::open("/opt/data", "movies").expect("fail");
// create another concurrent reader
let reader2 = reader.clone();
let handle = thread::spawn(|| reader2);

让我们一步一步来看这些步骤

  • 首先创建一个配置。通过 set_ 方法可以获得更多配置。
  • 通过提供 app_meta,调用者还可以持久化快照特定的元数据。
  • 创建构建器后,使用 BuildIndex 特性的 from_iter() 从迭代器构建 btree 索引。预期迭代的条目是预先排序的。
  • 调用者可以可选地传递一个位图实例,该实例用于实现一个 布隆过滤器
  • 位图类型通过 BuildIndex 特性参数化。如果不需要概率位图表,将 NoBitmap 值传递给 from_iter() 方法。

在上面的示例中,我们使用 initial() 构造函数创建构建器实例,也可以通过 incremental() 构造函数逐步构建索引。要了解区别,我们应该更深入地了解数据集如何使用 robt 进行索引。

robt 是一个简单的 btree 索引,由 root-node(根节点)、intermediate-node(中间节点,称为 m-block)和 leaf-node(叶子节点,称为 z-block)组成。整个数据集都维护在叶子节点中,中间节点是使用叶子节点中的第一个键自下而上构建的,直到根节点。根节点的外观和行为与中间节点完全相同。

数据集由条目组成,每个条目由键、值、seqno、一个标志表示节点是否被删除或更新组成。维护seqno和删除标志的原因是为了支持数据库功能,如向量时间戳、日志结构合并等。

版本控制您的值,使用robt索引的一个附加功能是应用程序可以对其值进行版本控制。也就是说,每个条目,包括键、值、seqno等,还维护值的先前版本及其修改seqno。而不是持久化整个值(旧版本),它们以相对于新版本的增量形式持久化,并作为增量保存。这是使用[Diff]机制实现的。请注意,robt本身不计算版本增量,但它被视为条目的一部分并持久化。

索引中的每个条目定义为Entry类型,并在一个通用crate中定义。请注意,索引条目在键类型、值类型和增量类型上是参数化的。这里,增量类型D可以是NoDiff,如果应用程序不感兴趣于保留旧版本,或者应与<V as Diff>::D相同。有关更多详细信息,请参阅[Diff]机制。

现在回到叶子节点,所有条目都存储在叶子节点中。为了便于归档旧版本,增量deltas在单独的值日志文件中持久化。可选地,为了便于增量构建,值也可以在值日志文件中持久化。当值和增量都在单独的值日志文件中持久化时,叶子节点变得非常紧凑,最终适合缓存、压缩、增量构建、优化IOPS和增量归档。

从索引中读取

所有读取操作都通过[Index]类型完成。使用传递给initial()incremental()构造函数的相同参数来open()现有索引以进行读取。

为并发克隆索引。尽管应用程序可以使用open()调用创建所需数量的Index实例,但推荐的方法是在Index上调用try_clone()。这将共享底层数据结构以避免同一索引多个实例之间的内存膨胀。只有元数据在索引实例(当它被克隆时)之间共享,每个索引实例都将保持对底层文件(的)打开文件描述符。

简单的键值索引

robt索引在键类型、值类型、增量类型和位图类型上参数化。delta-type实现值类型的旧版本作为增量差异。bitmap-type实现布隆过滤器以优化查找缺失项。

在大多数情况下,不需要delta-typebitmap-type。要使用crate根目录中的简单{key,value}索引[Builder]和[Index]类型。要使用完全参数化的变体,请使用db::Builderdb::Index类型。

索引条目

对于简单索引,keyvalue就足够了。但为了实现数据库功能,如压缩、日志结构合并等,我们需要保留每个条目更多的信息。尽管条目的内部形状没有公开(为了未来兼容性),robt使用mkit::db::Entry作为默认索引条目。

压缩

压缩是从索引快照(也称为实例)中去重/删除条目和/或旧版本的过程。在robt中,压缩操作消耗一个Index实例,并创建一个新的Index实例,其条目已压缩到所需的cutoff级别。有三种类型的压缩

去重

这基本上适用于不需要保留旧版本或删除条目的快照。

当使用相同的值日志文件来增量构建新的突变批次时,旧值会被重复。这需要定期清理垃圾值以减少磁盘占用。

这种类型的压缩也适用于不需要分布式LSM的索引实例。在这种情况下,最旧的快照可以压缩掉每个条目的旧版本,并删除标记为已删除的条目。

LSM压缩

这适用于将索引存储为多级快照的数据库索引,类似于leveldb。每个快照都可以构建为robt [Index]。大多数基于LSM的存储将它们的根快照作为最旧的和唯一的真实来源,但对于在不同节点上最终具有多个真实来源的分布式索引来说,这是不可能的。为了便于这种设计,在LSM模式下,任何给定节点的根级别也可以保留到指定seqno的旧版本,这个seqno是通过最终一致性计算得出的。

LSM压缩的另一个用例是维护值的旧版本。

墓碑压缩

墓碑压缩与lsm-compaction类似,只有一个主要区别。当应用程序逻辑触发tombstone-compaction时,只有比指定seqno旧的已删除条目将被删除。

依赖

~3MB
~56K SLoC