ToyDB 重写:一个用 Rust 编写的分布式 SQL 数据库,用于教育目的
erikgrinaker/toydb
用 Rust 编写的分布式 SQL 数据库,作为教育项目从头开始构建。主要特性:
- Raft 分布式共识,用于线性化状态机复制。
- ACID 事务,基于 MVCC 的快照隔离。
- 可插拔的存储引擎,带有 BitCask 和 内存 后端。
- 基于迭代器的查询引擎,具有 启发式优化 和时间旅行支持。
- SQL 接口,包括连接(joins)、聚合和事务。
我最初在 2020 年编写 toyDB 是为了更多地了解数据库内部原理。从那时起,我花了几年时间在 CockroachDB 和 Neon 构建真正的分布式 SQL 数据库。基于这些经验,我重写了 toyDB,作为一个分布式 SQL 数据库背后架构和概念的简单说明。
toyDB 旨在简单易懂,并且功能完备且正确。性能、可伸缩性和可用性等方面不是目标,因为它们是生产级数据库中复杂性的主要来源,并且会掩盖基本的底层概念。在可能的情况下,采取了捷径。
文档
- 架构指南:对 toyDB 代码和架构的引导式游览。
- SQL 示例:toyDB 的 SQL 功能演练。
- SQL 参考:toyDB 的 SQL 方言的参考文档。
- 参考资料:构建 toyDB 时使用的研究材料。
用法
在安装了 Rust 编译器 后,可以构建并启动本地的五节点集群,如下所示:
$ ./cluster/run.sh
Starting 5 nodes on ports 9601-9605 with data under cluster/*/data/.
To connect to node 1, run: cargo run --release --bin toysql
toydb4 21:03:55 [INFO] Listening on [::1]:9604 (SQL) and [::1]:9704 (Raft)
toydb1 21:03:55 [INFO] Listening on [::1]:9601 (SQL) and [::1]:9701 (Raft)
toydb2 21:03:55 [INFO] Listening on [::1]:9602 (SQL) and [::1]:9702 (Raft)
toydb3 21:03:55 [INFO] Listening on [::1]:9603 (SQL) and [::1]:9703 (Raft)
toydb5 21:03:55 [INFO] Listening on [::1]:9605 (SQL) and [::1]:9705 (Raft)
toydb2 21:03:56 [INFO] Starting new election for term 1
[...]
toydb2 21:03:56 [INFO] Won election for term 1, becoming leader
可以使用以下命令构建命令行客户端,并将其与 localhost:9601
上的节点 1 一起使用:
$ cargo run --release --bin toysql
Connected to toyDB node n1. Enter !help for instructions.
toydb> CREATE TABLE movies (id INTEGER PRIMARY KEY, title VARCHAR NOT NULL);
toydb> INSERT INTO movies VALUES (1, 'Sicario'), (2, 'Stalker'), (3, 'Her');
toydb> SELECT * FROM movies;
1, 'Sicario'
2, 'Stalker'
3, 'Her'
toyDB 支持大多数常见的 SQL 功能,包括连接、聚合和事务。下面是更复杂的查询的 EXPLAIN
查询计划(获取所有来自电影公司发布的 IMDb 评级为 8 或更高的电影):
toydb> EXPLAIN SELECT m.title, g.name AS genre, s.name AS studio, m.rating
FROM movies m JOIN genres g ON m.genre_id = g.id,
studios s JOIN movies good ON good.studio_id = s.id AND good.rating >= 8
WHERE m.studio_id = s.id
GROUP BY m.title, g.name, s.name, m.rating, m.released
ORDER BY m.rating DESC, m.released ASC, m.title ASC;
Remap: m.title, genre, studio, m.rating (dropped: m.released)
└─ Order: m.rating desc, m.released asc, m.title asc
└─ Projection: m.title, g.name as genre, s.name as studio, m.rating, m.released
└─ Aggregate: m.title, g.name, s.name, m.rating, m.released
└─ HashJoin: inner on m.studio_id = s.id
├─ HashJoin: inner on m.genre_id = g.id
│ ├─ Scan: movies as m
│ └─ Scan: genres as g
└─ HashJoin: inner on s.id = good.studio_id
├─ Scan: studios as s
└─ Scan: movies as good (good.rating > 8 OR good.rating = 8)
架构
toyDB 的架构对于分布式 SQL 数据库来说非常典型:一个由 Raft 集群管理的事务性键/值存储,其上有一个 SQL 查询引擎。有关更多详细信息,请参见架构指南。
测试
toyDB 主要使用 Goldenscripts 进行测试。这些脚本会记录各种场景、捕获事件和输出,并在以后断言行为保持不变。例如:
使用 cargo test
运行测试,或者查看最新的 CI 运行。
基准测试
toyDB 没有针对性能进行优化,但带有一个 workload
基准测试工具,可以针对 toyDB 集群运行各种工作负载。例如:
# 启动一个5节点的 toyDB 集群。
$ ./cluster/run.sh
[...]
# 通过所有 5 个节点运行只读基准测试。
$ cargo run --release --bin workload read
Preparing initial dataset... done (0.179s)
Spawning 16 workers... done (0.006s)
Running workload read (rows=1000 size=64 batch=1)...
Time Progress Txns Rate p50 p90 p99 pMax
1.0s 13.1% 13085 13020/s 1.3ms 1.5ms 1.9ms 8.4ms
2.0s 27.2% 27183 13524/s 1.3ms 1.5ms 1.8ms 8.4ms
3.0s 41.3% 41301 13702/s 1.2ms 1.5ms 1.8ms 8.4ms
4.0s 55.3% 55340 13769/s 1.2ms 1.5ms 1.8ms 8.4ms
5.0s 70.0% 70015 13936/s 1.2ms 1.5ms 1.8ms 8.4ms
6.0s 84.7% 84663 14047/s 1.2ms 1.4ms 1.8ms 8.4ms
7.0s 99.6% 99571 14166/s 1.2ms 1.4ms 1.7ms 8.4ms
7.1s 100.0% 100000 14163/s 1.2ms 1.4ms 1.7ms 8.4ms
Verifying dataset... done (0.002s)
可用的工作负载有:
read
:单行主键查找。write
:单行插入到连续主键。bank
:各个客户和帐户之间的银行转帐。为了使事情变得有趣,这包括连接、二级索引、排序和冲突。
有关工作负载和参数的更多信息,请运行 cargo run --bin workload -- --help
。
下面列出了示例工作负载结果。由于 fsync 和 Raft 层中缺少写入批处理,因此写入性能非常差。禁用 fsync 或使用内存引擎可以显着提高写入性能(以牺牲持久性为代价)。
Workload | BitCask | BitCask w/o fsync | Memory
---|---|---|---
read
| 14163 txn/s | 13941 txn/s | 13949 txn/s
write
| 35 txn/s | 4719 txn/s | 7781 txn/s
bank
| 21 txn/s | 1120 txn/s | 1346 txn/s
调试
VSCode 和 CodeLLDB 扩展可用于调试 toyDB,调试配置位于 .vscode/launch.json
下。
在“运行和调试”选项卡下,选择例如“Debug executable 'toydb'”或“Debug unit tests in library 'toydb'”。
鸣谢
toyDB 的 Logo 由 @jonasmerlin 提供。