找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1788

积分

0

好友

241

主题
发表于 2025-12-31 07:42:06 | 查看: 22| 回复: 0

“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

  • 写入流程

    1. 写入先记录到内存的 WAL 和 MemTable。
    2. 当 MemTable 写满时,会 Flush 成一个不可变的 Parquet 格式 SSTable 文件,并上传至 S3。
    3. 通过原子操作更新 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 开销。
  • 支持基础谓词过滤(gtinis_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 高性能编程感兴趣的开发者,可以关注 云栈社区 上的相关技术讨论,获取更多深度分析和实践案例。




上一篇:C语言函数指针详解:从基础语法到嵌入式开发实践与常见面试题
下一篇:CUDA编程实战:详解线程全局索引的计算方法与公式推导
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-10 09:11 , Processed in 0.277561 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表