turbolite 是一个用 Rust 实现的 SQLite VFS,能够直接从 S3 提供点查询和连接,冷启动延迟低于 250 毫秒。
本仓库是一个 Cargo 工作空间,包含两个 crate:
turbolite — 纯 Rust 库。提供页面级压缩、加密和 S3 分层的 SQLite VFS。turbolite-ffi — C FFI / 可加载扩展 + 语言绑定(Python、Node.js、Go)。它还提供页面级压缩(zstd)和加密(AES-256),以实现静止状态下的效率和安全性,这些功能可以独立于 S3 使用。
实验性。 turbolite 正在积极开发中,包含错误。请小心使用。
对象存储正在变得快速。S3 Express One Zone 提供个位数毫秒级 GET 请求,Tigris 也非常快。本地磁盘与云存储之间的差距正在缩小,turbolite 正是利用了这一点。
其设计和命名灵感来自 turbopuffer 围绕云存储限制进行激进架构的方法。该项目的初始目标是超越 Neon 的 500 毫秒以上冷启动。目标已达成。
如果你每个服务器使用一个数据库,请使用卷。turbolite 探索的是如何拥有数百或数千个数据库(每个租户一个、每个工作区一个、每个设备一个),而不想为每个数据库都挂载一个卷,并且你接受单一写入源。
turbolite 以 Rust 库、SQLite 可加载扩展 (.so/.dylib) 以及 Python 和 Node.js 的语言包形式发布,并支持 Go 的 Github 依赖。任何兼容 S3 的存储均可使用(AWS S3、Tigris、R2、MinIO 等)。它是一个标准的 SQLite VFS,工作在页面级别,因此大部分 SQLite 特性应该都能工作:FTS、R-tree、JSON、WAL 模式等。
turbolite 是更广泛的 hadb 生态的一部分。独立的 turbolite 是一个带一个安全写入者的存储 VFS;如果你需要 HA 领导选举加上持续 WAL 复制,请通过 haqlite-turbolite 使用它,该库在其上层叠了 HaQLite 和 walrust。该 HA 路径仍非常实验性。
如果你想为 turbolite 做贡献或找 bug,请创建 pull request 或打开 issue。
1M 帖子 / 100K 用户(约 1.5GB 存储),无任何缓存,所有字节来自 S3。EC2 c5.2xlarge + S3 Express One Zone(同一可用区,~4ms GET 延迟)。Fly performance-8x + Tigris(~25ms GET 延迟)。两者:8 个专用 vCPU、16GB RAM、7 个预取工作线程。参见 基准测试 和 存储后端很重要。
基准测试按 缓存级别(查询运行时本地磁盘已缓存的内容)组织:
内部节点 是最现实的冷启动基准:内部页在连接打开时被积极加载,因此当你运行第一个查询时,它们已被缓存。索引页在首次访问时会在后台积极预取,可能尚未就绪。
100K 行,Fly.io performance-2x(专用 vCPU、NVMe、IAD):
点查询的每页开销最高(约 2 倍)。其他操作接近或达到持平。无锁缓存架构意味着并发读取永远不会阻塞写入。
| 之后 | 本地 | S3(同区域 RustFS) |
|---|---|---|
| 1K 次插入 | 19ms | 38ms |
| 10K 批量 | 17ms | 114ms |
| 1K 次更新 | 9ms | 36ms |
写入始终是本地速度。S3 成本仅在检查点时产生。数据基于同一 Fly 区域(~2ms RTT)的 RustFS。S3 Express One Zone 可能类似。
pip install turbolite
如果你喜欢使用像 `ssh-keygen` 和 `ssh-add` 这样的工具,以及 `~/.ssh` 中的 SSH 密钥,那么你不需要太多其他东西就可以开始。
### tl;dr
```shell
$ drone sftp --clone --copy ...
你还需要先创建 ~/.ssh/id_rsa、~/.ssh/id_rsa.pub。并且需要将公钥注册到远程 SFTP 服务器上作为授权登录。
Travis CI 是 Drone 的 CI 任务的自然选择吗?嗯,这并不正确。实际上,Drone 提供了一个完整的 CI/CICD 服务。Drone 的早期版本基于 Travis。最近,Drone 更新为云原生 CI/CD 服务。因此,Drone 与其他云原生工具配合得很好。好吧,Drone 并不是唯一的选择。但它是一个不错的选择。
使用上面定义的最小工具链,Drone 的 CI 功能是有效的。对于实际生产场景,你至少还需要使用像 Drone 这样的健壮 CI 系统(尽管 Drone 本身就是一个 CI 系统)来管理你的 SFTP 部署。
用于 SFTP 部署的 Drone 流水线如下所示:
pipeline:
sftp:
image: appleboy/drone-sftp
host: 1.2.3.4
port: 22
username: ubuntu
password:
from_secret: ssh_password
target: /home/ubuntu/app/
source: dist/
strip_components: 2
这提供了非常好的集成。你也可以使用 SSH 密钥代替密码。如果你想在部署后运行任意命令,可以使用 appleboy/drone-ssh 或 drone-ssh 插件。```python
import turbolite
conn = turbolite.connect("my.db", mode="s3", bucket="my-bucket", endpoint="https://t3.storage.dev")
conn.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)") conn.execute("INSERT INTO users VALUES (1, 'alice', '[email protected]')") conn.commit()
alice = conn.cursor().execute("SELECT * FROM users").fetchone() print(alice[1])
"alice"
See [Installation](#installation) for Node, Go, Rust, local-only mode, and using the `.so` loadable extension directly
## 设计
turbolite是专为S3而非文件系统的约束而设计的。每一项决策均源自此模型:
| S3 约束 | 影响 |
|---------------|-------------|
| **往返延迟高** | 最小化请求数量。批量写入,积极预取读取。 |
| **带宽是瓶颈** | 最大化带宽利用率。 |
| **PUT和GET按操作计费** | 一个64KB的GET与一个16MB的GET成本相同。优化请求数量,而非字节效率。 |
| **对象不可变** | 永不原地更新。写入新版本,交换指针。不会出现部分写入损坏。 |
| **存储成本低** | 不要为空间优化。过度预分配,保留旧版本,让垃圾回收后续处理。 |
### 架构
turbolite在SQLite和S3之间添加了内省和间接层,能够高效地对页面进行分组、压缩、跟踪和获取。
SQLite使用B树索引,每次请求一个页面。它知道页面N位于字节偏移量`N * page_size`处。这些页面在页映射中随机分布,以实现高效的随机访问。但在S3上,每个请求获取一个页面意味着每个查询可能会有数千次潜在的随机GET请求。
但页面并非同等重要。SQLite有不同类型的页面。turbolite**按类型分离页面组**:内部B树、索引叶节点和数据叶节点页面。
内部页面在每次查询中都会被访问,以将查找路由到叶节点页面。turbolite检测到它们,将其以压缩包形式存储在S3中,并在VFS打开时热加载。之后,每次B树遍历都是缓存命中。
索引叶节点页面得到相同处理:单独的压缩包、惰性后台预取、防止被逐出。冷查询只需获取数据页面。
turbolite利用**B树内省**来了解页面属于*哪个树(表或索引)*,并智能地将这些页面一起存储在S3中,作为**页面组**:多个页面分块到一个S3对象中。足够大以在预取时满载带宽,又足够小以支持点查询。默认:每组256页,64KB页面时约16MB。
将同一个表/索引存储在一起意味着我们为冷查询尽可能少地发送GET请求。
turbolite**通过清单文件间接进行页面查找**,该清单是每个页面所在位置的唯一真实来源。它用显式指针替换了SQLite的隐式`offset = page * size`。旧的页面组版本永远不会被覆盖;清单PUT是原子提交点。旧版本变成垃圾,由`gc()`清理。
SQLite默认使用4KB页面以匹配文件系统磁盘页大小。在S3上,磁盘页大小无关紧要。重要的是最小化请求数量和最大化B树扇出。答案是**大页面**:turbolite默认使用64KB页面。页面越少,到达叶节点所需的S3往返次数越少。
为了加快点查询速度,turbolite使用**可寻址压缩**:每个页面组被编码为多个**zstd帧**(每帧约4页)。清单存储每帧的字节偏移量,因此缓存未命中时只需通过S3范围GET获取包含所需页面的约256KB子块,而不是整个组。
预取有两层:**主动预取**(查询计划抢先)和**被动预取**(基于缓存未命中的自适应)。
**查询计划抢先**首先运行。在查询执行之前,turbolite通过`EXPLAIN QUERY PLAN`拦截SQLite查询计划,提取查询将涉及的确切表和索引,并在读取第一个页面之前将所有它们的页面组提交到预取池。一个原本会触发五次顺序(未命中-获取)循环的五表连接,在查询开始时并行发起所有五次获取。对于`SCAN`查询,这意味着整个表被预先预取。
> 注意:SQLite每个连接只支持一个跟踪回调。如果另一个扩展先占用了该槽位,抢先预取会静默地回退到被动预取。
**被动预取**处理抢先预取未覆盖的情况,并作为备用。在缓存未命中时,两件事同时发生:
1. **内联范围GET**:获取包含所需页面的特定子块,立即返回给SQLite。
2. **后台预取**:根据计划,将同一树的兄弟组提交到预取池。
未命中计数器按**每个B树**而非全局跟踪。一个先命中`users`(未命中1)再命中`posts`(未命中1)的性能分析查询正确地将每个树跟踪为1,而不是2。这防止了多表连接仅仅因为涉及多个树而意外地升级每个树上的预取。
每次连续未命中都会推进一个**预取计划**,该计划控制预取同一树中组的比例。turbolite根据查询计划自动选择计划:
- **搜索计划** `[0.3, 0.3, 0.4]`:用于扫描索引未知部分的`SEARCH ... USING INDEX`查询。从第一次未命中开始就激进,因为我们不知道索引会被扫描多少。
- **查找计划** `[0.0, 0.0, 0.0]`:用于每树命中1-2页的点查询和索引查找。任何预取之前有三次免费跳转。在S3 Express和Tigris上,零密集计划优于早期斜坡计划。
你可以在打开时通过设置`TurboliteConfig`上的`prefetch.search`/`prefetch.lookup`来调整预取计划——你知道预期的工作负载形态,因此VFS不必猜测。请参见[配置预取](#configuring-prefetch)。
两种计划都利用了B树内省:每个预取组保证包含来自正确树的页面。例如:如果SQLite从`users`表请求一个页面,然后又从同一个表请求另一个页面,turbolite会假定将要进行扫描,并在后台预取`users`表的其余部分,而不预取其他内容。如果没有B树内省,它会因为数据在磁盘上相邻而意外地获取一半的用户表和一半的文章表。
**索引叶先行预取**对索引`SEARCH`做同样的处理。索引叶已经列出了SQLite将要请求的表rowid——因此turbolite通过缓存的内部页面解析它们,并一次批量预取那些表帧,而不是逐个获取,从而减少请求数量。
### 内存页面缓存
turbolite有自己的内存页面缓存,取代了SQLite内置的页面缓存。SQLite的页管理器在内部缓存页面,并且对于缓存的页面不会从VFS重新读取。这对于单写入者数据库来说没问题,但对于读副本(HA从节点、轮询清单的读取者),当底层数据通过复制发生变化时,SQLite的缓存会变得过时。
turbolite的缓存是**感知清单**的:当`set_manifest()`触发(来自复制的新数据)时,它会使磁盘缓存和内存缓存中受影响的页面失效。写入操作也会使其页面在内存缓存中失效。这保证了复制或写入后的读取是新鲜的。
**架构:**```
SQLite (PRAGMA cache_size=0)
-> turbolite VFS xRead
-> in-memory page cache (64MB default, AtomicPtr, zero-lock reads)
-> disk cache (NVMe pread)
-> S3 (on miss)
配置:
TurboliteConfig 上的 cache.mem_budget(字节)。默认值:64MB。TURBOLITE_MEM_CACHE_BUDGET 环境变量(例如 128MB、1GB)。0 可完全禁用内存缓存。turbolite.connect()(Python/Go/TypeScript)会自动禁用 SQLite 的页面缓存,转而使用 turbolite 的缓存。直接使用 Connection::open_with_flags_and_vfs 的 Rust 使用者应设置 PRAGMA cache_size=0 以获得相同行为。
所有数据在存储前都会经过 zstd 压缩。页面组使用可寻址的多帧编码,独立压缩每一帧(约 4 页,约 256KB),因此点查询仅解压相关帧,而非整个页面组。自定义 zstd 字典可进一步提高压缩比。
本地(非 S3)模式也在页面级别使用 zstd 压缩。有关字典训练工具,请参阅 CLI。
如果启用了加密,turbolite 会加密所有内容:S3 对象、本地缓存、WAL、元数据。S3 数据使用 AES-256-GCM,每帧带随机 nonce(经过身份验证,可检测篡改)。本地数据使用 AES-256-CTR,零大小开销。加密在压缩之后进行:明文 → zstd → 加密 → S3。
密钥轮换: rotate_encryption_key(config, new_key) 可在不解压缩的情况下重新加密、添加或移除所有 S3 数据上的加密。Some 到 Some 轮换密钥,Some 到 None 移除加密,None 到 Some 添加加密。崩溃安全:旧对象永远不会被覆盖,清单上传是原子提交点,并且在提交前会通过验证步骤确认新数据可读。部分运行产生的孤立对象由 gc() 清理。
点查询是理想场景。 在缓存级别为 index 时,点查询通过 S3 范围 GET 获取 1-2 个子块(每个约 100KB)。内部页和索引页已被缓存。在缓存级别为 none 时,增加约 120ms 用于重新获取内部页和第一个数据页。这适用于任何规模的机器。
拥有足够核心的扫描。 预取池通过每树自适应调度使 S3 带宽饱和。搜索查询从第一次未命中开始激进地预取;计划感知的 SCAN 查询提前批量预取整个表。足够的线程可以在 2-3 个预取批次内同步数 GB 的数据库,只需数秒。
小型机器上的扫描。 使用 1 个预取线程扫描 1.46GB 需要数秒而非毫秒。瓶颈是 S3 往返:每次跳转串行获取组。如果你的第一个查询是在 1 vCPU 机器上全表扫描,预计启动会很痛苦。
线程调优不当。 预取线程过少会导致扫描因等待 S3 而停滞;过多则前台 SQLite 工作会与下载发生争用。默认值(max(num_cpus - 1, 1))为前台工作留出一个核心,但大型数据库上的扫描密集型工作负载仍需要足够的 CPU。
首次查询惩罚。 缓存级别为 none 时的首次查询需要约 50-200ms 用于内部页面加载,外加至少一次数据获取。如果查询在后台预取完成前需要索引页,则会回退到内联范围 GET。
haqlite-turbolite 中。 该栈结合了 HaQLite 租约、turbolite 页面分层和 walrust 持续 WAL 复制。这是多节点部署的预期路径,而不是直接多写入者访问单个 turbolite 前缀。wal 特性标志 + walrust。参见持久性。SQLite 特性中确实可用的:FTS、R-tree、JSON、WAL 模式、DELETE 日志模式、VACUUM、自动 VACUUM。
查询计划前置运行(参见架构)是主要的预取机制。以下反应性调度在前置运行不可用或查询访问未在计划中的页面时作为回退。
每个元素是在第 N 次连续的每树缓存未命中时预取兄弟组的比例。当未命中次数超过数组长度时,比例=1.0(全部剩余)。
为什么有两种反应性调度? SEARCH 查询扫描索引/表的未知部分,需要激进预热。Lookup 查询每树仅命中 1-2 页,几乎不需要预取。每树未命中计数器确保独立跟踪:一个概要分析查询先命中用户(未命中 1)再命中帖子(未命中 1),会分别跟踪每棵树。
在 VFS 构造时,在 TurboliteConfig 上设置 prefetch.search 和 prefetch.lookup:```rust
use turbolite::tiered::{TurboliteConfig, PrefetchConfig};
let config = TurboliteConfig { prefetch: PrefetchConfig { search: vec![0.4, 0.3, 0.3], lookup: vec![0.0, 0.0, 0.2], query_plan: true, ..Default::default() }, ..Default::default() };
对于无需重新打开连接即可进行按查询重新调优的操作,请使用
`turbolite_config_set` SQL 函数(Cirrus c 阶段)。每次推送的作用域限定于调用连接的句柄,并持续生效,直到您再次更改它:```sql
SELECT turbolite_config_set('prefetch_search', '0.5,0.5,0.0');
SELECT turbolite_config_set('prefetch_lookup', '0.0,0.0,0.0');
SELECT * FROM posts WHERE created_at > ?; -- runs with the new schedule
当一个查询使用索引来查找表行时(SEARCH ... USING INDEX),SQLite 读取的索引叶已经指明了即将获取的表 rowid。前瞻解析这些 rowid,通过缓存的内部页将它们解析为对应的表叶帧,并一次性预取这些帧——这样,表行会一起到达,而不是每次一个 S3 往返。
它默认开启,并且仅对需要进入表的索引 SEARCH 生效。扫描、按 rowid 逐点读取以及完全热查询则走正常路径不变,因此很少有必要将其关闭。它需要查询计划预取(plan_aware,默认为 true)。
唯一需要禁用它的情况是完全热且对 CPU 敏感的工作负载,此时解析每个索引叶会消耗少量资源,而预取则因页面已缓存而无所获:```sql SELECT turbolite_config_set('lookahead', 'false');
或者在打开时,在`TurboliteConfig`上设置`lookahead` / `TURBOLITE_LOOKAHEAD`环境变量。
Rust调用者可以通过`turbolite::tiered::settings::set`调用相同的路径。
### 推荐配置
| 工作负载 | 配置 | 原因 |
|----------|--------|-----|
| 混合OLTP | 默认值 | 计划感知处理扫描,搜索调度预热索引,查找调度保持保守。 |
| 点查询密集型(代理数据库) | `prefetch.lookup: vec![0.0, 0.0, 0.0]` | 查找几乎从不需要预取。 |
| 扫描密集型分析 | `prefetch.search: vec![0.5, 0.5]`, `prefetch.query_plan: true` | 激进的搜索预热加上计划感知的批量预取。 |
| 保守(突发无服务器) | `prefetch.search: vec![0.1, 0.2, 0.3]`, `prefetch.lookup: vec![0.0, 0.0, 0.1]` | 最小化预取噪声。 |
**注**:预取是每个连接的。每个新连接从每个树的冷缺失计数器开始。缓存是共享的,因此第二个连接受益于第一个连接缓存的页。
### 存储后端的重要性
最优的预取调度取决于你的S3后端的延迟-带宽权衡。我们在S3 Express(~4ms GET)和Tigris(~25ms GET)上测试了10个调度对,涉及6个查询:
| 后端 | GET延迟 | 最佳点查询 | 最佳总体表现 | 调优增益 |
|---------|-------------|-------------------|-------------|-------------|
| **S3 Express** | ~4ms | 74ms (off/off: 96ms) | 188ms (off/off: 212ms) | 5-23% 优于无预取 |
| **Tigris** | ~25ms | 192ms (off/off: 231ms) | 524ms (off/off: 616ms) | 8-34% 优于无预取 |
在S3 Express上,`off/off`(完全不预取)对于点查询来说出人意料地有竞争力,因为每个子块范围GET只有约4毫秒。"无预取"和"最优预取"之间的差距很小(点查询为23%),因为单个GET很便宜。在Tigris上,相同的查询从预取中受益更多(在idx-filter上高达39%),因为每次浪费的往返成本为25毫秒。
实际效果:在高延迟后端上,更积极地推动搜索调度,并保持查找调度有更多的前导零。在S3 Express上,默认值工作良好,调优带来的收益较小。在两个后端上,全扫描性能对调度不敏感,因为查询计划预运行会提前批量预取整个表。
使用`tiered-tune`(见下文)为你的特定后端和查询找到最优调度。
### 调优工具
`tiered-tune`连接到现有的turbolite数据库,并在你的实际查询上扫描预取调度。与其猜测调度,不如运行你的实际工作负载,让工具找到最佳对:```bash
# Connect to existing database, test your queries
cargo run --release --features cloud,zstd --bin tiered-tune -- \
--prefix "databases/tenant-123" \
--query "SELECT * FROM users WHERE id = ?1" \
--query "SELECT p.*, u.name FROM posts p JOIN users u ON p.user_id = u.id WHERE p.id = ?1" \
--iterations 10
# Custom schedule grid
cargo run --release --features cloud,zstd --bin tiered-tune -- \
--prefix "databases/tenant-123" \
--query "SELECT * FROM orders WHERE user_id = ?1 ORDER BY created_at DESC LIMIT 20" \
--search-schedules "0.3,0.3,0.4;0.5,0.5;1.0" \
--lookup-schedules "0;0,0,0.1;0,0,0,0.1,0.2" \
--iterations 10
输出是一个按查询的对比表(类似于 tiered-bench --matrix),显示每个调度对的 p50、p90、GET 计数和字节数。该工具推荐一个调度方案,并打印应用该方案的 TurboliteConfig 赋值。
turbolite 是一个存储层,而不是复制系统。持久性取决于数据何时到达 S3。
检查点之后:页面组 + 清单已存在于 S3 中。S3 提供 11 个九的持久性。这些数据能经受机器故障。
检查点之间:写入仅存在于本地磁盘上的本地 WAL 中。如果机器在下一个检查点之前崩溃,这些写入将丢失。
检查点频率控制权衡:更频繁的检查点 = 更小的数据风险窗口,但更多的 S3 PUT 操作。默认值是 SQLite 的自动检查点(每 1000 个 WAL 帧)。
turbolite 通过 TurboliteConfig 中的 sync_mode 支持两种检查点模式:
SyncMode::Durable(默认)。检查点在持有 SQLite 的 EXCLUSIVE 锁的同时将页面组上传到 S3。简单,每个检查点完全持久。在上传完成之前,无法进行任何写入或读取。适用于大多数工作负载。
SyncMode::LocalThenFlush。检查点仅写入本地磁盘缓存(约 1ms 持有锁),然后释放锁。调用者通过 flush_to_s3() 单独上传到 S3,在此期间读取和写入正常进行。这对于写入密集型工作负载非常有用,因为在这些工作负载中,在 S3 上传期间阻塞读取器是不可接受的。
在检查点和刷新之间,数据仅存在于本地磁盘缓存中。进程崩溃是安全的(数据在本地磁盘上,且暂存日志捕获了确切的页面内容供上传)。在刷新前的机器故障会导致这些写入丢失。缓存驱逐是安全的:turbolite 自动保护待处理的页面不被驱逐。
崩溃恢复:如果进程在检查点和刷新之间崩溃,暂存日志会保留在磁盘上。在下一次调用 TurboliteVfs::new() 时,它们会被自动恢复并排队等待下一次 flush_to_s3() 调用。读取会立即从本地缓存中提供,无需等待刷新。
启用了 wal 特性标志后,turbolite 通过 walrust 将 WAL 帧发送到 S3,从而弥补了个别写入与检查点之间的持久性差距。```toml
turbolite = { version = "0.5", features = ["cloud", "zstd", "wal"] }
(无内容,输入为空,因此输出也为空。)```rust
let config = TurboliteConfig {
wal_replication: true, // enable WAL shipping
..Default::default()
};
turbolite 和 walrust 通过存储在 manifest.change_counter 中的重放游标保持同步。导入/检查点路径从 SQLite 的文件更改计数器中为该游标提供种子;直接页面重放可以将其推进到最新的已提交变更集序列。在冷启动时,turbolite 从页面组中物化数据库,然后 walrust 重放 txid > change_counter 的 WAL 段,以恢复上次检查点之后发生的写入。
使用 WAL 传输的持久性模型:每个提交的事务都会在同步间隔(默认 100ms)内作为 WAL 段传输到 S3。如果机器宕机,最多丢失一个同步间隔的写入。检查点后,txid <= change_counter 的 WAL 段会被自动垃圾回收。
WAL 传输是 SyncMode 的补充:SyncMode 控制检查点如何到达 S3,WAL 传输使单个写入在检查点之前保持持久性。
单写入者,快照读取者。一个进程写入;读取者看到打开时的最新已提交清单。turbolite 不是一个分布式数据库,也不协调多个写入者之间的事务。
turbolite 也可以作为纯本地的压缩/加密 VFS 使用:
压缩:zstd(默认)、lz4、snappy、gzip。使用 zstd,您可以训练并嵌入自定义压缩字典,并自动轮换以获得更高效的压缩。更大的页面大小压缩效果更好。训练工具见 CLI。
加密:每页 AES-256-GCM。
页面级操作意味着大多数 SQLite 特性仍然有效:FTS、R-tree、JSON、WAL 模式。大多数其他 SQLite 压缩/加密扩展在文件级别工作或需要自定义构建。
此仓库是一个 Cargo 工作区。turbolite crate 是位于工作区根目录的纯 Rust 库。语言绑定和可加载扩展位于 turbolite-ffi/。
Python:pip install turbolite — 参见 turbolite-ffi/packages/python/```python
import turbolite
conn = turbolite.connect("my.db")
conn = turbolite.connect("my.db", mode="s3", bucket="my-bucket", endpoint="https://t3.storage.dev")
import sqlite3 conn = sqlite3.connect(":memory:") turbolite.load(conn) conn.close() conn = sqlite3.connect("file:my.db?vfs=turbolite", uri=True) # local
**Node.js**: `npm install turbolite` — 参见 [turbolite-ffi/packages/node/](https://github.com/russellromney/turbolite/blob/HEAD/turbolite-ffi/packages/node/)
**Rust**:```toml
[dependencies]
turbolite = "0.5" # local VFS
turbolite = { version = "0.5", features = ["cloud"] } # + S3 storage
turbolite = { version = "0.5", features = ["encryption"] } # + encryption
Go (cgo, 链接共享库):```bash make lib-bundled # build libturbolite.{so,dylib}
```go
// #cgo LDFLAGS: -L/path/to/target/release -lturbolite
// #include <stdlib.h>
// extern int turbolite_register_local_file_first(const char* name, const char* db_path, int level);
// extern void* turbolite_open(const char* path, const char* vfs_name);
// extern int turbolite_exec(void* db, const char* sql);
// extern char* turbolite_query_json(void* db, const char* sql);
// extern void turbolite_close(void* db);
import "C"
推荐使用 turbolite_register_local_file_first(name, db_path, level),它以用户可见的数据库路径为键。底层函数 turbolite_register_local(name, cache_dir, level) 仍然导出,供希望自行管理缓存目录的嵌入者使用。完整HTTP服务器示例请参见 examples/go/。
使用 SQLite 的 load_extension 为任何语言构建可加载扩展:```bash
make ext # produces target/release/turbolite.{so,dylib}
**帮助** (`-help`)
* `-help`:显示此帮助信息。
* `-help <主题>`:显示特定主题的帮助。例如:`-help hashcat` 或 `-help topics`。
* `-help request`:请求缺失内容的帮助。
**命令**(主脚本参数)
这些参数用于配置和运行 `corc.py` 编排器。
| 参数 | 描述 |
|----------|-------------|
| `--max-pending <int>` | 开始限流前的最大待处理任务数(默认 10)。 |
| `--no-install` | 不运行安装脚本(默认:如果存在安装脚本则运行)。 |
| `--no-progress` | 禁用进度条显示。 |
| `--plugin <名称>` | 按名称加载特定插件(可多次使用)。 |
| `--run <插件>` | 初始化后在插件中运行特定命令。 |
| `--script <路径>` | 用于启动后设置/配置的自定义脚本路径。 |
| `--secrets <路径>` | 包含环境变量的密钥文件(YAML 或 JSON)路径。 |
| `--seed <URL>` | 在主配置之后加载的辅助 Corc 配置文件 URL。 |
| `--setup <路径>` | 在*启动后*运行的设置脚本路径(例如 `/usr/share/distro-setup.sh`)。 |
| `--skip <插件>` | 跳过加载特定插件(可多次使用)。 |
| `--verbose` | 启用详细日志记录(默认:false)。 |
| `--version` | 显示 Corc 版本信息并退出。 |```c
sqlite3_enable_load_extension(db, 1);
sqlite3_load_extension(db, "path/to/turbolite", NULL, NULL);
// "turbolite" VFS (local) is always registered
// "turbolite-s3" is a single-volume convenience VFS when TURBOLITE_BUCKET is set
对于文件优先的用户故事,注册一个拥有调用者app.db的每数据库VFS:```sql
SELECT turbolite_register_file_first_vfs('app', '/data/app.db');
-- now open /data/app.db via vfs=app; turbolite stores its sidecar
-- metadata at /data/app.db-turbolite/.
要为扩展加载时配置默认的`"turbolite"` VFS 以使用文件优先模式,请在加载扩展之前在环境中设置 `TURBOLITE_DATABASE_PATH=/data/app.db`。则 sidecar 为 `/data/app.db-turbolite/`,而较低级别的 `TURBOLITE_CACHE_DIR` 旋钮将被忽略。
### Node.js```bash
npm install turbolite
const { connect } = require("turbolite");
// File-first: /data/app.db is the local page image. // /data/app.db-turbolite/ holds hidden implementation state. const db = connect("/data/app.db"); db.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)"); db.prepare("INSERT INTO users VALUES (?, ?)").run(1, 'alice');
const rows = db.prepare("SELECT id, name FROM users").all(); // [{ id: 1, name: 'alice' }] db.close();
`db` 是一个标准的 better-sqlite3 数据库。`connect()` 为你注册了一个按数据库的文件优先 VFS。
要导出一个标准 SQLite 文件(例如使用 `sqlite3` CLI 进行检查),请使用 better-sqlite3 备份 API:`await db.backup('export.sqlite')`。参见 [turbolite-ffi/packages/node/](https://github.com/russellromney/turbolite/blob/HEAD/turbolite-ffi/packages/node/) 获取完整文档。
### Rust(本地,文件优先)```rust
use turbolite::tiered::{TurboliteVfs, TurboliteConfig};
// `app.db` is the user-visible local page image.
// `app.db-turbolite/` holds hidden implementation state.
let config = TurboliteConfig::for_database_path("/data/app.db");
let vfs = TurboliteVfs::new_local(config)?;
turbolite::tiered::register("turbolite", vfs)?;
let conn = rusqlite::Connection::open_with_flags_and_vfs(
"/data/app.db",
rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE | rusqlite::OpenFlags::SQLITE_OPEN_CREATE,
"turbolite",
)?;
底层表单让您直接选择缓存目录:```rust let config = TurboliteConfig { cache_dir: "/path/to/data".into(), // turbolite owns this dir ..Default::default() };
在这种情况下,本地镜像为`/path/to/data/data.cache`而不是调用者命名的`app.db`。新的嵌入器应优先使用文件优先形式。
### Rust (S3 cloud)```rust
use turbolite::tiered::{TurboliteVfs, TurboliteConfig};
use hadb_storage::StorageBackend;
let config = TurboliteConfig::for_database_path("/data/app.db");
let storage: Arc<dyn StorageBackend> = /* your S3 backend */;
let vfs = TurboliteVfs::with_backend(config, storage, tokio::runtime::Handle::current())?;
turbolite::tiered::register("turbolite", vfs)?;
let conn = rusqlite::Connection::open_with_flags_and_vfs(
"/data/app.db",
rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE | rusqlite::OpenFlags::SQLITE_OPEN_CREATE,
"turbolite",
)?;
app.db 是 turbolite 的压缩页面镜像。它不保证能被原版 sqlite3 直接打开。对于普通的 SQLite 文件(例如用于 sqlite3 CLI),请使用 SQLite 在线备份 API 或特定绑定的导出辅助函数(Python 中的 conn.iterdump(),Node 中的 db.backup())。
turbolite 附带了用于检查、管理和与 turbolite 数据库交互的 CLI,无需编写 Rust。```bash cargo install turbolite --features cloud,zstd
### 命令```bash
# Inspect a database manifest
turbolite info --db my.db
turbolite info --db my.db --bucket my-bucket --endpoint https://t3.storage.dev
# Interactive SQLite shell (with turbolite VFS)
turbolite shell --db my.db
turbolite shell --db my.db --bucket my-bucket --read-only
# Download entire database from S3 into local cache
turbolite download --db my.db --bucket my-bucket --threads 8
# Export to plain SQLite (for migration or backup)
turbolite export --db my.db --output plain.db
# Import a plain SQLite file into turbolite S3 format
turbolite import --input plain.db --bucket my-bucket --prefix databases/my-db
所有S3命令均接受--bucket、--prefix、--endpoint和--region标志,或从TURBOLITE_BUCKET、TURBOLITE_PREFIX、AWS_ENDPOINT_URL和AWS_REGION环境变量中读取。
在SQLite-over-network领域有许多项目。turbolite借鉴了所有这些项目的思想。
最常用的方法:将未修改的.db文件放在S3或CDN上,当SQLite读取页面时发起HTTP Range GET请求。
.dbi索引文件,该文件预收集B-tree内部节点用于预取——与turbolite的内部页面捆绑包思路相同。设计上与sqlite_zstd_vfs配合使用。这些方案都是只读的,并从原始文件中获取未压缩的页面。每次点查询传输一个原始4KB(或64KB)页面。
这些方案将对象存储视为真实来源,并复制单个页面或变更集,从而支持部分副本和无离线优先/边缘部署。
orbitinghail/graft):一种事务性存储引擎,用于通过S3进行惰性、部分、强一致性复制。libgraft SQLite扩展实现了一个VFS,通过Graft卷读取和写入4KB页面。使用帧式zstd压缩和基于splinter的变更集。在“复制页面而非WAL帧”的领域中,turbolite最接近的架构表亲,侧重于多写入者边缘同步而非冷读延迟。这些方案将本地写入复制到S3以进行备份或恢复。
wa-sqlite的TypeScript/浏览器端口。同样的一对象一页面模型,适配WASM/客户端使用。所有基准测试位于benchmark/。请参见benchmark/README.md了解部署场景(本地、Fly.io、EC2)。
tiered-bench二进制程序生成一个社交媒体数据集(用户、帖子、点赞、好友关系),并针对S3在每个缓存级别对查询进行基准测试。
另一个benchmark/bench_s3vfs.py测试工具对sqlite-s3vfs运行相同的查询以进行头对头比较。它通过benchmark/fly-s3vfs.toml部署,并使用与tiered-bench相同的确定性数据集生成器。```bash
TIERED_TEST_BUCKET=my-bucket AWS_ENDPOINT_URL=https://t3.storage.dev
cargo run --features zstd,cloud --bin tiered-bench --release --
--sizes 100000
cargo run --features zstd,cloud --bin tiered-bench --release --
--sizes 1000000 --prefetch-threads 8 --queries post --modes interior
cargo run --example quick-bench --features encryption --release
关键标志:`--sizes` (行数)、`--ppg` (每组页数)、`--prefetch-threads`、`--prefetch-search`(搜索调度)、`--prefetch-lookup`(查找调度)、`--grouping`(位置或btree)、`--queries`(帖子/个人资料/谁喜欢/互相关注)、`--modes`(无/内部/索引/数据)、`--skip-verify`(在小型机器上跳过 COUNT(*))、`--iterations`、`--plan-aware`(启用前瞻预取)、`--matrix`(扫描调度对)。每个查询的调度:`--post-prefetch`/`--post-lookup`、`--profile-prefetch`/`--profile-lookup` 等(搜索和查找是每个查询独立的)。```bash
# Matrix mode: test 10 schedule pairs x 6 queries at cold level
cargo run --features zstd,cloud --bin tiered-bench --release -- \
--sizes 1000000 --import auto --plan-aware --matrix --iterations 10
# Tune schedules for your own database and queries
cargo run --features zstd,cloud --bin tiered-tune --release -- \
--prefix "databases/my-db" \
--query "SELECT * FROM users WHERE id = ?1" --param 42 \
--plan-aware --iterations 10
cargo test --features zstd # local VFS tests cargo test --features zstd,cloud # + S3 integration tests cargo test --features zstd,encryption # + encryption tests
## Notes
turbolite 之前的名称是 `sqlite-compress-encrypt-vfs`,简称 `sqlces`。
### 安全模型细节
S3 数据采用 AES-256-GCM 加密,并为每个帧使用唯一的随机 nonce(经过认证、防篡改)。本地文件使用 AES-256-CTR 加密,并采用确定性 nonce(页面号/字节偏移量),从而为静态磁盘攻击者提供机密性。CTR 的确定性 nonce 意味着多快照攻击者可以在已复用的偏移量处恢复明文的异或结果,这与 SQLite 自身 SEE 扩展的权衡一致。本地缓存是临时性的,且可从 S3 重新创建。
## License
Apache-2.0
| 查询 | 类型 | 冷启动 (S3 Express) | 冷启动 (Tigris) |
|---|
| 帖子 + 用户 | 点查询 + 连接 | 86ms | 172ms |
| 个人资料 | 多表连接 (5 个 JOIN) | 251ms | 479ms |
| 谁点了赞 | 索引搜索 + 连接 | 206ms | 302ms |
| 共同好友 | 多搜索连接 | 19ms | 49ms |
| 带索引筛选 | 覆盖索引扫描 | 79ms | 88ms |
| 全表扫描 + 筛选 | 全表扫描 | 476ms | 532ms |
| 缓存级别 | 已缓存的内容 | 从 S3 获取的内容 | 何时发生 |
|---|
| 无 | 无 | 所有内容 | 全新启动,空缓存 |
| 内部节点 | B-tree 内部页 | 索引 + 数据页 | 连接打开后的首次查询 |
| 索引 | 内部节点 + 索引页 | 仅数据页 | 正常的 turbolite 操作 |
| 数据 | 所有内容 | 无 | 等同于本地 SQLite |
| 操作 | SQLite | turbolite | 开销 |
|---|
| 点查询 | 145K/s | 73K/s | 2.0x |
| 范围扫描 | 8.8K/s | 8.3K/s | 持平 |
| 全表扫描 | 56/s | 60/s | 持平 |
| INSERT | 19K/s | 23K/s | 持平 |
| 按主键 UPDATE | 40K/s | 27K/s | 1.5x |
| 批量 INSERT(事务内) | 685K/s | 740K/s | 持平 |
| 参数 | 控制内容 | 默认值 |
|---|
prefetch.threads | 用于并行 S3 获取的工作线程数 | max(num_cpus - 1, 1) |
cache.pages_per_group | 每个 S3 对象的页面数,越大 = 更少的 PUT 操作,每次获取更多字节 | 256 |
cache.gc_enabled | 在检查点后删除旧的页面组版本 | true |
sync_mode | 检查点持久性:Durable(检查点中上传 S3)或 LocalThenFlush(延迟上传) | Durable |
| 策略 | 何时触发 | 默认调度 | 发生行为 |
|---|
| SCAN(前置运行) | EQP 显示 SCAN table | 所有组提前 | 首次读取前批量预取整个表。无需跳转调度。 |
| SEARCH(反应性) | EQP 显示 SEARCH ... USING INDEX | [0.3, 0.3, 0.4] | 从第一次未命中开始激进预取;扫描未知的索引部分。 |
| Lookup(反应性) | 点查询,无 EQP 信息 | [0.0, 0.0, 0.0] | 三次空闲跳转,零预取。点查询很少受益于预取。 |
| turbolite | 原始文件范围GET | Litestream VFS | sqlite_web_vfs + zstd_vfs | mvsqlite | Graft | sqlite-s3vfs |
|---|
| 从S3读取 | 在压缩页面组上进行可寻址范围GET | 对原始页面进行范围GET | 对LTX文件进行范围GET | 对压缩外部数据库进行范围GET | 对FoundationDB进行KV查找 | 惰性获取4KB页面/变更集 | 每个页面一次GetObject |
| 写入S3 | 检查点(每组一次PUT) | 无 | 无 | 无 | 是(MVCC) | 是(异步变更集复制) | 每个页面一次PUT |
| 压缩 | 可寻址多帧zstd | 无 | 无 | zstd(嵌套数据库) | zstd增量编码 | 帧式zstd | 无 |
| 加密 | 每页AES-256-GCM | 无 | 无 | 无 | 无 | 未列出 | 无 |
| 预取 | 前瞻 + 跳跃调度 | 无或基本预读 | LRU缓存 | 自适应合并 | 客户端缓冲区 | 惰性/按需 | 无 |
| 内部页面优化 | 检测、固定、单独捆绑 | 无 | 来自LTX尾部的页面索引 | 可选的.dbi文件 | 无 | 未列出 | 无 |
| 每次点查询字节数(缓存:索引) | 约100KB(一个压缩帧) | 4-64KB(一个原始页面) | 不定 | 不定 | 不定 | 4KB(一个页面) | 4KB(一个页面) |
| 每4096页写入成本 | 约0.000005美元(一次PUT) | 不适用 | 不适用 | 不适用 | FoundationDB操作 | 批量变更集 | 约0.02美元(4096次PUT) |