“Serverless 是未来的趋势”,这句话你可能已经听了八百遍。但当你真的想在 Cloudflare Workers 或 AWS Lambda 上写个应用时,却发现——状态怎么存?
别慌,今天我们来深入探讨一个颇具颠覆性的新项目:Tonbo。它并非传统数据库,而是一个专为 Serverless 和边缘计算环境 设计的嵌入式数据库。其神奇之处在于:它无需服务器进程,数据直接存储在 S3 对象存储上,同时支持 MVCC 事务和 Apache Arrow 原生查询。
图:Tonbo 项目官网介绍页面,展示了版本、许可证和核心特性。
一、Serverless 的“甜蜜陷阱”:无状态 ≠ 无数据
想象一个场景:需要为电商平台构建一个“秒杀活动监控面板”,每秒接收海量点击事件,并实时统计展示。
第一反应可能是采用典型的 Serverless 架构:AWS Lambda + API Gateway + DynamoDB。然而,上线后高并发写入可能导致 DynamoDB 账单激增,自动扩缩容和读写容量单位配置不当会带来高昂成本。若改用 S3 存储原始日志并用 Athena 查询,则查询延迟又无法满足“实时”需求。
这便陷入了 Serverless 的典型困境:
- 用传统数据库?违背了 Serverless “免运维”的初衷。
- 纯用对象存储?查询效率低下,缺乏结构化数据处理能力。
- 用托管数据库?成本较高,且弹性伸缩能力未必与 Serverless 函数完美匹配。
这就是 Serverless 的“甜蜜陷阱”:计算是无状态、弹性且廉价的,但数据是有状态、持久且可能昂贵的。两者之间存在天然矛盾。而 Tonbo 正是为了破解这一矛盾而生。
二、Tonbo 是什么?一个“没有数据库的数据库”
官方定义简洁明了:Tonbo is an embedded database for serverless and edge runtimes. 换言之,它是一个嵌入到你应用程序代码中的数据库,专为 Serverless 和边缘运行时设计。
其最反直觉的设计在于——它没有独立的数据库服务进程!你的数据直接以 Parquet 列式格式 存储在 S3(或本地磁盘),通过一个名为 Manifest 的元数据文件来协调并发写入。应用代码(如 Cloudflare Worker)直接读写这些文件,全程无需中心化协调器或后台服务。
核心特性一览:
- Async-first:全异步架构,天然适配 Tokio、WASM、Cloudflare Workers 等现代运行时。
- 无服务器(Serverless):数据存于 S3,协调依赖 Manifest,计算完全无状态。
- Arrow 原生:使用 Apache Arrow 定义 Schema,查询返回零拷贝的
RecordBatch。
- 开放格式:底层 Parquet 文件可被 Pandas、Spark、Athena 等任何兼容工具直接读取。
- MVCC 事务:支持快照隔离级别的事务,并具备时间旅行查询能力。
听起来很美好,但它究竟是如何实现的?
三、技术解剖:Tonbo 的三大支柱
理解 Tonbo,需要抓住其三个核心技术支柱:存储模型、并发控制与查询引擎。
1. 存储模型:Merge-Tree 与对象存储的天作之合
Tonbo 的底层存储借鉴了 LSM-Tree 的思想,但针对对象存储(如 S3)的特性进行了关键改造。
对象存储有两大核心特性:不可变性(文件一旦写入无法修改)和最终一致性(列表操作可能无法立即看到新文件)。为此,Tonbo 设计了一个 Manifest 驱动的 Merge-Tree:
-
写入流程:
- 写入先记录到内存的 WAL 和 MemTable。
- 当 MemTable 写满时,会 Flush 成一个不可变的 Parquet 格式 SSTable 文件,并上传至 S3。
- 通过原子操作更新 Manifest 文件,记录新的 SSTable 列表。
-
Manifest 是什么?
- 一个 JSON 或 Protobuf 文件,描述了数据库当前状态的“快照”,包括所有 SSTable 文件列表、时间戳、Schema 版本等信息。
- 所有写操作必须通过 Compare-and-Swap (CAS) 机制更新 Manifest,以确保并发安全。S3 提供的
If-Match / ETag 条件更新能力,在此充当了天然的分布式锁。
这种设计的优势在于:任何无状态函数都可以参与写入,而无需依赖一个中心化的协调服务。
2. 并发控制:MVCC 与时间戳实现快照隔离
Tonbo 支持 多版本并发控制 (MVCC),这意味着:
- 每次写入都会关联一个逻辑时间戳(通常由 Manifest 的版本号隐式提供)。
- 读操作可以指定时间戳,实现快照读,确保读取一致性视图。
- 支持时间旅行查询:你可以查询历史任意时间点的数据状态。
这在 Serverless 场景下非常实用。例如,一个长时间运行的分析任务可以在开始时获取一个数据快照,避免被任务执行期间的新写入干扰;或者在调试时,回溯错误发生前的精确数据状态。
3. 查询引擎:Arrow 原生与零拷贝性能
Tonbo 的查询构建于 Apache Arrow 这一现代数据分析标准之上。
- 使用 Rust 的
#[derive(Record)] 宏定义数据结构,Tonbo 会自动生成对应的 Arrow Schema。
- 查询结果直接返回
RecordBatch,实现零内存拷贝,可以高效地传递给 DataFusion、Polars 或直接序列化为 JSON。
- 支持投影下推:仅读取查询所需的列,大幅减少 I/O 开销。
- 支持基础谓词过滤(
gt, in, is_null 等),未来计划支持更复杂的过滤下推。
来看一段官方示例代码,感受其简洁性:
#[derive(Record)]
struct User {
#[metadata(k = "tonbo.key", v = "true")]
id: String,
name: String,
score: Option<i64>,
}
// 插入数据
let users = vec![User { id: "u1".into(), name: "Alice".into(), score: Some(100) }];
let mut builders = User::new_builders(users.len());
builders.append_rows(users);
db.ingest(builders.finish().into_record_batch()).await?;
// 查询:score > 80
let filter = Predicate::gt(ColumnRef::new("score"), ScalarValue::from(80_i64));
let results = db.scan().filter(filter).collect().await?;
整个过程清晰直观:定义结构体 → 插入数据 → 执行查询,三步即获得完整的数据库操作能力,背后却无需管理任何服务器。
四、实战场景:Tonbo 适合解决哪些问题?
场景 1:Serverless 应用的状态层
假设你有一个运行在 Cloudflare Workers 上的 Discord Bot,需要记录用户交互日志并支持查询。
- 传统方案:连接 PostgreSQL,但需处理 Workers 不支持长连接、连接池管理等问题。
- Tonbo 方案:数据直接存入 Cloudflare R2 (S3兼容),每次请求读写 Parquet 文件。无需维护数据库实例,成本极低。
场景 2:边缘设备的数据采集
IoT 设备在网络边缘收集传感器数据,需定期同步至云端。
- Tonbo 方案:将 Tonbo 嵌入到边缘设备的 WASM 模块中。在本地缓存数据,网络恢复后批量 Flush 至 S3。云端分析系统可直接读取这些 Parquet 文件进行处理。
场景 3:构建自定义数据基础设施
当你需要构建轻量级的“事件存储”或“特征存储”时。
- Tonbo 方案:它提供了 嵌入式 MVCC 事务引擎 和 Parquet 存储格式,你可以基于此快速搭建支持时间旅行的审计日志系统,或按时间切片查询的机器学习特征仓库。
五、快速上手:从零编写一个 Tonbo 应用
让我们通过一个极简示例,体验 Tonbo 的开发流程。
步骤 1:定义数据模型(Schema)
use tonbo::prelude::*;
#[derive(Record, Debug)]
pub struct Event {
#[metadata(k = "tonbo.key", v = "true")]
pub event_id: String,
pub user_id: String,
pub action: String,
pub timestamp: i64,
}
注意 #[metadata(k = “tonbo.key“, v = “true“)] 属性,它告知 Tonbo 将 event_id 字段视为主键。
步骤 2:初始化数据库连接
use tonbo::{db::{ObjectSpec, LocalSpec}, prelude::*};
// 本地开发,使用本地目录
let local = LocalSpec::new(“/tmp/events“);
let db = DbBuilder::from_schema(Event::schema())?
.object_store(ObjectSpec::local(local))?
.open()
.await?;
生产环境可轻松切换至 S3:
let s3 = S3Spec::new(“my-bucket“, “events“, AwsCreds::from_env()?);
let db = DbBuilder::from_schema(Event::schema())?
.object_store(ObjectSpec::s3(s3))?
.open()
.await?;
步骤 3:写入数据
let events = vec![
Event {
event_id: “e1“.into(),
user_id: “u1“.into(),
action: “click“.into(),
timestamp: 1717020000,
},
// ... 更多事件
];
let mut builders = Event::new_builders(events.len());
builders.append_rows(events);
db.ingest(builders.finish().into_record_batch()).await?;
步骤 4:查询数据
// 查询 user_id = “u1“ 的所有事件
let filter = Predicate::eq(
ColumnRef::new(“user_id“),
ScalarValue::Utf8(Some(“u1“.into()))
);
let batches = db.scan().filter(filter).collect().await?;
// 将 RecordBatch 转换回 Vec<Event>
let events: Vec<Event> = batches
.into_iter()
.flat_map(|batch| Event::try_from_record_batch(&batch).unwrap())
.collect();
println!(“{:?}“, events);
整个流程无需 SQL、连接字符串或后台服务,完全通过 Rust 结构体和类型安全的方法调用完成,对于 Rust 开发者而言非常友好。
六、Tonbo 的当前局限与未来展望
当然,Tonbo 并非万能。目前它仍处于 Alpha 阶段,需要注意以下几点:
- 不适合高频随机更新:底层 Parquet 列存更适合追加写入,虽支持 MVCC 删除/更新(实质是写新文件+标记删除),但并非最优。
- 查询能力仍在演进:目前支持基础谓词过滤,复杂的 JOIN、聚合等功能尚在规划中。
- 生态待完善:暂无官方的 Python/JS 语言绑定,与 DataFusion 等生态的深度集成也在进行中。
但其发展路线图清晰且值得期待:
- 远程 Compaction:利用 Serverless 函数自动合并小文件,优化存储。
- 分支功能:类似 Git,支持数据集的分支与合并,便于实验和数据版本管理。
- OPFS 支持:让 Tonbo 能在浏览器环境中运行,拓展应用边界。
- 多语言绑定:计划推出 Python/JS SDK,扩大开发者生态。
七、为什么选择 Rust 作为实现语言?
你可能会问,为何 Tonbo 采用 Rust 编写?答案在于其无可替代的优势:性能、安全性与 WASM 支持。
- Rust 的零成本抽象使 Tonbo 能高效处理 Parquet 编解码和 Arrow 内存操作。
- 其内存安全特性保障了在 Serverless 苛刻环境下的运行稳定性,避免了因垃圾回收(GC)停顿导致的函数超时。
- 对 WebAssembly 的一流支持,让 Tonbo 能够无缝运行在 Cloudflare Workers、Deno 等边缘计算平台上。
更重要的是,Rust 社区对嵌入式数据库领域有着天然的探索热情,从 sled 到 redb,再到如今的 Tonbo,都在共同探索“无服务器数据库”这一新兴范式。
结语:数据库的“隐形”未来
十年前,我们争论是否要上云;五年前,我们讨论是否采用 Serverless;今天,我们开始思考一个更根本的问题:数据库是否还必须以一个“服务器”的形式存在?
Tonbo 给出了一个大胆的答案:或许不必。
当数据以开放的 Parquet 格式静默存储于对象存储,当计算以无状态函数的形式弹性伸缩,当协调通过轻量的 Manifest 文件原子化完成——数据库便从一种“服务”转变为你应用中的一段“代码”。
这或许预演了数据库的某种终极形态:看不见、摸不着,却无处不在。
项目资源:
对 Serverless 架构、嵌入式系统和 Rust 高性能编程感兴趣的开发者,可以关注 云栈社区 上的相关技术讨论,获取更多深度分析和实践案例。