2个不稳定版本
0.2.0 | 2021年3月13日 |
---|---|
0.1.0 | 2020年12月24日 |
#2609 in 数据库接口
120KB
3K SLoC
使用只读B树索引键值对数据集。有关详细信息,请参阅rustdoc链接(上方)。
贡献
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
本身不计算版本增量,但它被视为条目的一部分并持久化。
索引中的每个条目定义为EntryD
可以是NoDiff
,如果应用程序不感兴趣于保留旧版本,或者应与<V as Diff>::D
相同。有关更多详细信息,请参阅[Diff]机制。
现在回到叶子节点,所有条目都存储在叶子节点中。为了便于归档旧版本,增量deltas
在单独的值日志文件中持久化。可选地,为了便于增量构建,值也可以在值日志文件中持久化。当值和增量都在单独的值日志文件中持久化时,叶子节点变得非常紧凑,最终适合缓存、压缩、增量构建、优化IOPS和增量归档。
从索引中读取
所有读取操作都通过[Index]类型完成。使用传递给initial()
或incremental()
构造函数的相同参数来open()
现有索引以进行读取。
为并发克隆索引。尽管应用程序可以使用open()
调用创建所需数量的Index实例,但推荐的方法是在Index上调用try_clone()
。这将共享底层数据结构以避免同一索引多个实例之间的内存膨胀。只有元数据在索引实例(当它被克隆时)之间共享,每个索引实例都将保持对底层文件(的)打开文件描述符。
简单的键值索引
robt
索引在键类型、值类型、增量类型和位图类型上参数化。delta-type
实现值类型的旧版本作为增量差异。bitmap-type
实现布隆过滤器以优化查找缺失项。
在大多数情况下,不需要delta-type
和bitmap-type
。要使用crate根目录中的简单{key,value}
索引[Builder]和[Index]类型。要使用完全参数化的变体,请使用db::Builder和db::Index类型。
索引条目
对于简单索引,key
和value
就足够了。但为了实现数据库功能,如压缩、日志结构合并等,我们需要保留每个条目更多的信息。尽管条目的内部形状没有公开(为了未来兼容性),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