Turbolite:基于S3的SQLite VFS,冷启动JOIN查询延迟低于250ms

5 阅读1分钟

什么是Turbolite

Turbolite是一个用Rust编写的SQLite虚拟文件系统(VFS),可以直接从S3提供点查询和JOIN查询能力,冷启动延迟低于250ms。它还提供了页面级的zstd压缩和AES-256加密能力,可以独立于S3使用。

⚠️ 注意:Turbolite目前处于实验阶段,可能存在Bug导致数据损坏,请谨慎在生产环境使用。

设计背景

现在对象存储的速度越来越快:AWS S3 Express One Zone可以提供个位数毫秒级的GET响应,Tigris的速度也非常快。本地磁盘和云存储之间的差距正在不断缩小,Turbolite正是为了利用这一趋势而设计的。

它的设计和命名灵感来自turbopuffer围绕云存储限制进行架构优化的思路,项目最初的目标是击败Neon 500ms+的冷启动速度,目前这个目标已经实现。

Turbolite适合的场景:你有数百甚至数千个数据库(每个租户一个、每个工作区一个、每个设备一个),不想为每个数据库单独挂载存储卷,并且可以接受单写入源的限制。

支持的使用方式

Turbolite提供了多种使用形式:

  • Rust库
  • SQLite可加载扩展(.so/.dylib)
  • Python和Node.js语言包
  • Go的Github依赖

支持所有S3兼容的存储服务:AWS S3、Tigris、Cloudflare R2、MinIO等。它是标准的SQLite VFS,工作在页面级别,因此支持大部分SQLite特性:FTS、R-tree、JSON、WAL模式等。

性能表现

测试环境:100万行数据,1.5GB大小,无缓存,所有数据从S3读取。

  • EC2 c5.2xlarge + S3 Express One Zone(同可用区,~4ms GET延迟)
  • Fly performance-8x + Tigris(~25ms GET延迟)
  • 8个专用vCPU,16GB RAM,8个预取线程
查询类型冷启动(S3 Express)冷启动(Tigris)
帖子+用户 点查询+JOIN77ms192ms
多表JOIN(5个JOIN)190ms524ms
索引搜索+JOIN129ms340ms
多搜索JOIN82ms183ms
覆盖索引扫描74ms173ms
全表扫描+过滤586ms984ms

核心设计

Turbolite针对S3的限制进行了专门的设计优化:

  1. 减少请求次数:批量写入, aggressively预读,最小化往返次数
  2. 最大化带宽利用率:解决带宽瓶颈
  3. 优化请求效率:64KB的GET和16MB的GET费用相同,因此优先优化请求数量而非字节效率
  4. 不可变对象设计:从不原地更新,写入新版本后交换指针,避免部分写入损坏
  5. 存储成本优化:存储空间便宜,无需过度优化空间,旧版本由GC清理

关键优化点

  • 页面分组存储:将不同类型的页面(内部B树页、索引叶子页、数据叶子页)分开存储,多个页面打包成一个S3对象(默认256页/组,~16MB大小),减少GET请求次数
  • 大页面设计:默认使用64KB页面(SQLite默认是4KB),减少页面数量,从而减少S3往返次数
  • 可寻址压缩:每个页面组使用多帧zstd编码,清单文件存储每个帧的字节偏移,缓存缺失时只需通过S3范围GET获取包含所需页面的~256KB子块,而非整个组
  • 双层预取机制
    • 查询计划前置预取:查询执行前拦截SQLite查询计划,提取需要访问的表和索引,提前并行预取所有相关页面组
    • 响应式预取:缓存缺失时,并行获取所需子块,同时在后台预取同树的兄弟页面组
  • 预测式预取(实验性):跟踪跨表访问模式,当查询一致地访问相同的表集合时,未来访问任意子集时会触发其余表的后台预取
  • 加密支持:所有数据在压缩后使用AES-256-GCM加密,支持密钥轮换,无需解密即可重新加密所有S3数据

使用示例

pip install turbolite

import turbolite

# 分层数据库 - 从S3兼容存储(Tigris)提供冷查询
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', 'alice@example.com')")
conn.commit()

alice = conn.cursor().execute("SELECT * FROM users").fetchone()
print(alice[1])
# 输出: alice

当前限制

  • 仅支持单写入器:两台机器写入相同前缀会损坏清单文件
  • 暂不支持WAL同步:检查点之间的写入仅存在于本地WAL中