什么是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) |
|---|---|---|
| 帖子+用户 点查询+JOIN | 77ms | 192ms |
| 多表JOIN(5个JOIN) | 190ms | 524ms |
| 索引搜索+JOIN | 129ms | 340ms |
| 多搜索JOIN | 82ms | 183ms |
| 覆盖索引扫描 | 74ms | 173ms |
| 全表扫描+过滤 | 586ms | 984ms |
核心设计
Turbolite针对S3的限制进行了专门的设计优化:
- 减少请求次数:批量写入, aggressively预读,最小化往返次数
- 最大化带宽利用率:解决带宽瓶颈
- 优化请求效率:64KB的GET和16MB的GET费用相同,因此优先优化请求数量而非字节效率
- 不可变对象设计:从不原地更新,写入新版本后交换指针,避免部分写入损坏
- 存储成本优化:存储空间便宜,无需过度优化空间,旧版本由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中