mvSQLite 是开源的、与 SQLite 兼容的分布式数据库。我们将 SQLite 的存储层解耦到 FoundationDB 上,以提供大规模但“无底”的可扩展性、时间点读取和最严格的一致性级别。
在表面上
已经有很多不错的“多机”SQLite 风格:rqlite、dqlite 和 Litestream。但是我相信 mvSQLite 提供的东西是独一无二的:它不仅是复制的,而且是真正的分布式的,它不仅提供了读取和写入的可扩展性,它提供了最严格的一致性,而且它是 SQLite 的补充。
读写可扩展性
mvSQLite 的底层技术 FoundationDB 为可扩展性提供了坚实的基础。众所周知,FoundationDB 至少可以线性扩展至 500 个内核。但是,mvSQLite 仍然需要做一些事情,尤其是在写入方面。
并发写入。读取可扩展性是微不足道的:您只需将更多机器添加到集群中。不过,在不破坏可串行性的情况下扩展写入是困难的部分,而且 SQLite 本身直到最近才支持并发写入。在分布式系统中正确实现并发写入更是难上加难,这看起来是一个值得我们解决的有趣问题!
在每个事务中,SQLite 读取一组页面,并写入另一组页面。关于可串行化有以下观察:
在同一事务中读取的所有页面在写入之前都没有更改的因果假设下,写入页面的决定是正确的。
因此,如果两个或多个事务的并发执行不破坏彼此的因果假设,它们不会冲突并且可以一起提交。从事务的角度来看,它始终是可序列化和可线性化的。这种检查在 mvSQLite 中通过在提交时检查读取集中页面的版本来实现:如果任何版本大于当前事务的读取版本,则提交被中止并返回冲突错误。
不过有一些警告。例如,SQLite 总是在每次事务后修改第一页中的“文件更改计数器”字段。如果这种更改被持久化,整个并发写入机制将毫无用处——同一数据库上的任何两个并发事务都会发生冲突。mvSQLite 通过在持久版本中用零覆盖更改计数器并在客户端维护每个连接的虚拟更改计数器来解决该问题。
自动分片。其他具有同步复制的系统,如 rqlite 和 dqlite 是复制系统,而不是完全分布式系统,因为它们使用单个共识组复制整个数据库。当集群中有 3 或 5 台机器和一个写入器时,此方法有效,但无法进一步扩展。但在 mvSQLite 案例中,FoundationDB 自动安全地管理分片,并且可以很好地扩展到数百个 CPU 内核。
比ACID更严格
SQLite 是一个 ACID 数据库,而 mvSQLite 实际上提供了比 ACID 更严格的保证!
外部一致性。mvSQLite 是外部一致性,比强一致性更严格,是事务处理系统最严格的一致性级别。
同步复制。FoundationDB,进行同步复制并确保持久性。当 SQLite API 告诉您事务已提交时,它就是。集群中的应用程序崩溃、电源故障或丢失replication_factor - 1存储服务器不会导致数据丢失。
顺便说一句,您仍然可以在需要时选择进行全局最终一致的低延迟读取 - FoundationDB 支持到不同区域的异步复制 (DR)。
时间点读取
顾名思义,mvSQLite 是一个支持 MVCC(多版本并发控制)的数据库。这不仅仅是传统类型的 MVCC,其中并发事务会看到自己的快照 - 实际上您可以在过去的任何时间打开数据库的快照并从中读取。所以像 a 这样的东西DROP TABLE不会再导致数据丢失,并且应用程序可以与该机制集成以提供高级版本控制功能。
插入式添加
通常不需要对基于 SQLite 的应用程序进行任何更改即可使其与 mvSQLite 一起使用。有两种集成方法可用:
LD_PRELOAD. 如果您的应用程序动态链接到libsqlite3.so,则建议使用此方法,因为它更有效并且没有特定于操作系统的依赖项。设置LD_PRELOAD=libmvsqlite_preload.so环境变量,它应该“正常工作”。- 正在进行的工作:FUSE(用户空间中的文件系统)。这种方法更通用,也适用于静态链接的 SQLite3。
但是,如果应用程序尝试以不受支持的方式对数据库进行操作(例如通过设置启用 WAL pragma journal_mode = wal,或者在 SQLite 之外读取/写入数据库),mvSQLite 会感到困惑并拒绝工作。如果是这种情况,请修复应用程序!
系统内部
本节主要包含有关 mvSQLite 的常见问题的解答。
不仅仅是FoundationDB
乍一看,在 FoundationDB 之上粘贴事务块层似乎很容易:只需将 SQLite 事务映射到 FDB 事务,将 SQLite VFS 读/写映射到 FDB 事务,就完成了?
好吧,你可以做到,但是由于 FoundationDB 的一些重要限制,生成的系统可能不是很有用:
- 最长事务持续时间为 5 秒。
- 事务的大小(读取和写入的键 + 写入的值)不得超过 10MB。
- 所有写入的数据在提交之前都保存在内存中。
此外,无法读取过去某个时间的数据库版本,因此我们没有支持时间点读取的原语。
没有本地期刊
SQLite 旨在运行在真正的块设备上。这些设备不支持跨页面的事务性写入,因此应用程序需要想出自己的方式来执行原子提交。SQLite 与大多数其他数据库一样,通过日志实现了这一点。SQLite 支持两种日志模式:回滚日志和 WAL(预写日志)。
mvSQLite 不依赖 SQLite 自己的原子提交机制来确保 ACID 属性。相反,它将 VFS“解锁”操作视为“事务可见性围栏”,并使用 FoundationDB 提供的原语执行原子提交。FDB 的存储服务器上仍然有一个 WAL,但这不再是 SQLite 层需要处理的事情。
高效的数据编码
mvSQLite 通过使用增量编码来最小化存储页面的每个版本的开销。当页面的一小部分发生更改时,仅存储新旧版本之间的差异。
也许你觉得这篇文章中发现了很多细节——我也这么认为!我正在撰写另一篇介绍 mvSQLite 更深层内部结构的博文。订阅我的RSS 提要继续关注。
测试版软件的最坏情况
与 1.0 之前的任何东西一样,我需要在这里提出一个大警告:mvSQLite 是 beta 软件,包含错误。担心数据的可靠性是正常的:虽然目前版本还没有发现数据损坏的BUG,但如果在某些特殊情况下触发了罕见的代码路径并损坏了一些随机数据怎么办?让我们考虑一下最坏的情况:如果 mvSQLite 中有一个错误导致 aclear_range('', '\\xff')被执行并且您丢失了整个集群中的所有数据怎么办?
这就是 FoundationDB 的备份系统发挥作用的地方。您可以对外部对象存储服务(如 S3 )进行连续、实时的备份,并恢复到过去的任何时间点。完全清除后不会丢失任何数据。即使 FDB 集群中的所有磁盘同时损坏,您也只会丢失几秒钟的数据。如果对 S3 访问密钥具有适当的权限,即使拥有密钥,任何人都无法删除已备份的数据!
原文标题:Turning SQLite into a distributed database
原文链接:https://univalence.me/posts/mvsqlite




