SQLite驱动的BoringDB:高性能键值存储解决方案

发表时间: 2023-09-14 08:30

目前市面上有很多Key/Value的Nosql数据库可供我们选择,但是实际应用中往往需要能满足严格数据完整性,而又能实现支撑高性能大吞吐量的事务要求。本文虫虫给大家介绍一个开源的、用Rust编写的嵌入式健值数据库BoringDB。键值存储。



概述

BoringDB由Themelio构建并开源的Rust撰写的嵌入式健值数据库。它被 Themelio节点广泛使用,最初用于其内部持久化区块链信息,目前Themelio将其开源为一个Rust板条箱库,可以作为通用的Key/Value数据库使用

BoringDB 有一个相当奇特的设计——它是功能非常齐全的SQLite ,但它提供了一个简单的键值API。处理索引、ACID事务等所有繁重工作。SQLite以其著名的极高可靠性,但BoringDB添加了一个缓存层和写入批处理 ,使得每秒操作数较高的键值任务(例如处理稀疏Merkle树分支)相当快。

需求

为要什么重复造轮子

目前市面上有无数的键值NoSql数据库,其中许多似乎对于区块链应用来说已经“足够好”。

例如Bitcoin Core和以太坊的Geth客户端都使用LevelDB;Monero使用了LMDB;

而其他几个区块链项目则使用Facebook的RocksDB。

Themelio开始也没想到要自己写一个,他们刚开始时尝试使用sled,这是一个纯用Rust编写的最先进的键值数据库。

但是,上述这些Nosql包括sled都没有满足其完整性和高性能的要求。

完整性

与大多数使用嵌入式数据库的服务相比,区块链应用需要严格保证其数据的完整性。在区块链中如果存在数据偶尔不一致,很容易导致严重的后果,比如无效的区块得到确认。Themelio的设想是一个完全不受治理的,但是能严格保证数据完整性的服务。

大多数键值数据库在保护完整性方面的记录很差。不论是LevelDB、RocksDB还是sled都存在许多已知的损坏错误;LevelDB损坏经常出现在不正确关闭的以太坊节点上。这可能是由于不可避免的权衡,因为这些数据库通常使用高度复杂的技术来实现最大性能,而这些数据库的大多数消费者更关心性能而不是完整性。

高性能

如果想要最大程度的完整性,可以使用以稳健性和完整性而闻名的数据库,例如LMDB或SQLite。但这往往会导致性能不佳,尤其是写入性能。这是因为缺少像LSM树这样的技巧,事务写入需要将所有数据持久化同步到磁盘的时候,需要相当长的时间。

然而,在处理大量交易时,Themelio节点通常会向磁盘发送真正大量的小型写入——稀疏Merkle树中的单次写入可能会导致将数十个节点写入磁盘,而处理一个交易可能会导致多达数百次写入。如果这些写入安全地完成,一切都将永远进行。

“正确”的做法是将写入批处理为较大的事务,但如果不将事务句柄传递到可能不关心底层存储引擎的代码中,通常很难做到这一点。

在区块链中,总是希望数据库上的所有事务都可序列化,并保证数据库完整性 ——也就是说,事务要么完全发生,要么不全部发生,崩溃/电源故障不能导致不一致——但是不需要很强的耐用性。如果系统立即崩溃,不需要确保保存到磁盘的信息仍然存在,因为通常可以容忍几秒钟的数据丢失

强大的可序列化性和完整性与最终耐用性的结合是很难找到的。SQLite的WAL 模式与PRAGMA synchronous = NORMAL可以很大程度上满足要求,但直接使用SQLite的性能会相对差一点,Themelio节点创建的读写碎片极其严重。LMDB及其他Nosql同步每个事务或在每次可能发生电源故障时并不能完全保证数据库完整性。

架构

BoringDB数据库有三层构成,其底层(数据存储层)是一个功能非常齐全的SQLite,由SQLite表来保存和持久化所有的数据信息。应用接口层则提供了一个简单的键值API,用来处理索引、ACID事务等所有繁重工作。

API层

BoringDB提供一个简单的API,每个数据库只能由一个进程打开,并且由零个或多个映射组成,每个映射都公开一个有序的键/值接口。键和值都是原始字节向量,也支持范围查询(获取两个键之间的所有键,按字典顺序排序),保证可序列化、完整性和最终的耐用性。

数据存储层

BoringDB数据库的磁盘表示非常无聊。在底层,每个数据库都由单个SQLite数据库支持,在EXCLUSIVE模式排除其他进程。每个映射对应一个SQLite表,并以明显的方式进行编码。例如,名为BoringDB的映射exampl将表示为以下 SQLite 表:

CREATE TABLE example (key BLOB PRIMARY KEY,value BLOB NOT NULL);

写回缓存层

BoringDB并不直接查询该SQLite表,而是维护一个写回缓存层。所有的读取和写入都会经过这个缓存;特别是,当写入新的键值映射时,它仅写入缓存,而不会直接写入支持的SQLite表。

刷新线程

会定期将脏键(发生修改的键)同步到底层表。过程涉及积极的批处理,其中许多键都写入同一个SQLite事务中,从而大大提高了吞吐量。

顺序事务

较早的写入始终比较晚的写入安排在更早的事务中,从而在崩溃时保持完整的可串行性。

这样满足了完整性和高性能两方面的要求。SQLite具有强大的ACID事务,由 SQLite对正确性近乎疯狂的强调提供支持,包括可能是任何嵌入式数据库所经历的最密集的测试。

通过让SQLite完成所有艰苦的工作,实现了无可置疑的完整性和可序列化性,而缓存层则为“病态”情况(如大批量小写入)实现了数量级的加速,实现高性能的事务能力。

总结

BoringDB是一个用Rust写的,高可靠、高性能的嵌入式健值数据库。其发布是在cratesio上作为一个小型、独立的板条箱(Rust的类库),采用非常宽松的ISC 许可证,可以在各种应用(商业产品)都可以免费使用。

对于需要具有极高可靠性和合理写入性能的单进程嵌入式键值数据库时,BoringDB无疑个是完美的选择。