erikgrinaker/toydb

用 Rust 编写的分布式 SQL 数据库,作为教育项目从头开始构建。主要特性:

我最初在 2020 年编写 toyDB 是为了更多地了解数据库内部原理。从那时起,我花了几年时间在 CockroachDBNeon 构建真正的分布式 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 architecture

测试

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)

可用的工作负载有:

有关工作负载和参数的更多信息,请运行 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

调试

VSCodeCodeLLDB 扩展可用于调试 toyDB,调试配置位于 .vscode/launch.json 下。

在“运行和调试”选项卡下,选择例如“Debug executable 'toydb'”或“Debug unit tests in library 'toydb'”。

鸣谢

toyDB 的 Logo 由 @jonasmerlin 提供。