alitrack

DuckDB 搬进 S3 之后,worker 随便加,数据只一份

本文是DuckDB 数据库整个搬进 S3,单文件时代结束了的续篇

上篇讲了「它是什么、我实测了什么、边界在哪」;这一篇讲三件上篇没展开的事:

1) 它为什么重要——它是分布式 DuckDB 的地基;

2) 换掉磁盘到底要付出什么——官方基准里的性能账;

3) 和 DuckLake/Iceberg 到底怎么选——一个决策清单。

数据说明:性能数字全部来自作者仓库 benchmark.md 的公开基准(Apple M4、DuckDB v1.5.5),

以及官方扩展页数据;我没有在真实 S3 上重跑,S3 部分以作者数据为准。

● ● ●

上篇回顾:30 秒版本

duckdb_object_storage 给 DuckDB 加了一个 duckdb_objfs:// 虚拟文件系统,底层是

SlateDB(一个"对象原生"的嵌入式 LSM 库)+ Apache OpenDAL(存储后端适配层)。

效果:一个完整可写的 DuckDB 库——库文件、WAL、checkpoint 恢复文件——

全部住在对象存储里。索引、约束、视图、扩展行为原封不动,因为作者没动 DuckDB

的存储引擎,只是把底下的"磁盘"换了。

上篇讲了它是什么。这一篇回答一个更值钱的问题:这东西对 DuckDB 生态意味着什么?

● ● ●

一、它真正的大戏:分布式 DuckDB 的地基(Duckherder)

上篇我只讲了「容器明天销毁,库还在 S3 里」。这个场景只是开胃菜。

作者做这个扩展的真正意图,是他另一个项目 Duckherder——一个 DuckDB 的分布式

执行扩展。模型非常简洁:

  • 一个 driver 进程持有唯一的写者,负责规划查询、把表按 row group 切成分区任务;
  • N 个 worker 进程以 READ_ONLY 方式 attach 同一个 S3 数据库;
  • 每个 worker 直接从桶里读自己那个分区,算出部分聚合结果,driver 合并;
  • 加一个 worker = 多起一个只读进程。不搬数据、不分片、不 rebalance。

作者博客里给的真实示例(4 个 worker 跑 GROUP BY):

-- driver 上
SELECT duckherder_start_local_server(8815);
SELECT duckherder_register_worker('worker-1', 'grpc://10.0.0.11:8816');
-- ... 注册 worker-2/3/4

ATTACH 'localhost:8815/sales.db' AS dh (
    TYPE duckherder, DATA_PATH 's3://my-bucket/duckherder', SECRET 'my_s3');
CREATE TABLE dh.orders AS
  SELECT i AS id, 'region_' || (i % 4) AS region, (i % 1000)::DECIMAL(12,2) AS amount
  FROM range(1000000) t(i);

查询计划长这样——本地的 GROUP BY 消失了,只剩一个 DISTRIBUTED_SCAN:

┌───────────────────────────┐
│          ORDER_BY         │  ← 只有最终排序留在客户端
│ dh.main.orders.region ASC │
└───────────────┬───────────┘
┌───────────────┴───────────┐
│       DISTRIBUTED_SCAN    │  ← 下面的全部下推到 4 个 worker
└───────────────────────────┘

执行统计:4 个 worker、4 个任务、ROW_GROUP_PARTITION 切分、GROUP_BY 部分聚合合并。

为什么这比"库搬上 S3"重要得多:DuckDB 过去十年的卖点就是「单机里塞下整个分析栈」,

而它的天花板也恰恰是单机——内存放不下、核数用完、数据必须先在本地。Duckherder

  • duckdb_object_storage 给出的答案是:计算随便水平扩展,数据永远只有一份,住在桶里,

所有节点只读直取,天然一致(上篇讲过的「读者只看到已提交状态,永不读脏」在这里

是关键前提——否则 worker 各读各的半写状态,合并结果就是错的)。

几个边界(作者自己列的):

  • 强一致目前只有"有界滞后"(只读端约 10 秒轮一次 manifest)。作者计划加一个

opt-in 强一致模式:只读事务开始前刷新到写者最新 manifest,代价是一次往返,

配合"快照钉选"让整条分布式查询的每个 worker 读同一个版本。

  • Duckherder 本身也是 0.x 阶段的社区扩展,目前验证到「部分聚合下推」这一档。

一句话:这个扩展不是把 DuckDB 变成「云端 DuckDB」,

而是把 DuckDB 变成「可以水平扩展的 DuckDB」。这才是标题里「单文件时代结束」的完整含义。

● ● ●

二、换掉磁盘要付什么:官方基准里的性能账

上篇我实测了 local 后端"功能都通",但没谈速度。作者仓库里的 TPC-H 基准

(Apple M4、DuckDB v1.5.5、Q1–Q22)把这笔账算得很诚实。

读(ObjFS/原生 倍率,小于 1 说明 ObjFS 更快):

场景
整体
细节
内存后端,SF10
0.92×
22 条里 20 条比原生 in-memory 还快(Q02 0.69×、Q09 0.79×)
本地磁盘,SF10
1.08×
最差的 Q05 是 1.40×,多数查询 1.0–1.2×
S3 冷启动,SF1
1.20×
(0.92×–1.58×)
客户端在台北、桶在 ap-east-2 跨网测的;最差的 Q16 慢 200ms,根因是冷进程要先串行读 manifest/SST 索引

写(这里有两个反直觉的点,全篇最值得看):

场景
倍率
为什么
写本地文件
0.19×
最大短板。根因是「双 WAL」:每次 commit,DuckDB 的 WAL 记一次,SlateDB 的 WAL 又记一次,两次持久化点都付了。作者在路线图上已列为头号要修项——改成「单一日志拥有持久性」
写 S3
0.95×
对比对象是"原生本地写 + AWS CLI 上传"。追平了——因为原生 DuckDB 反正也得把整个文件传上去,绕不开这个成本

这三个数字合起来读,结论很清楚:

  • 内存/读为主的负载
    :几乎无感(0.92× 意味着比原生还快 8%——缓存层起了作用);
  • 本地写重的负载
    :现在不适合换,0.19× 是硬伤,等"单 WAL"落地再评估;
  • 本来就打算写 S3 的负载
    :基本不亏,0.95×,还省掉了「本地临时文件+上传」这段运维。

作者的优化路线图(都写在博客里,不是我的推测):冷读阶段并行加载 manifest 和

SST 索引(砍掉那 200ms)、加大扫描并发和预读(现在 4 并发/4MiB 上限)、持久缓存

默认值调优、单 WAL 重写。目标是「冷读追平 HTTPFS、本地写追平原生」。

还有一个上篇没展开的工程细节,值得懂行的读者看一眼——chunk 对齐 trick:

SlateDB 侧把数据切成 256 KiB 的 chunk(正好等于 DuckDB 的 block size),

并让 chunk 从 12 KiB(DuckDB 文件头大小)之后开始排布。结果:**每个 block

恰好落在一个 chunk 里,写一个 block 就是一次对象 put,读一个就是一次 get**。

作者的原话:对齐之前每个 block 都横跨两个 chunk,每次写都变成两个 chunk 的

read-modify-write。这种「把上层文件格式的几何结构吃透再做下层对齐」的功夫,

是这类基础设施项目里最见水平的部分。

● ● ●

三、和 DuckLake / Iceberg 到底怎么选

上篇只说了「另一个方向」,这次给决策清单。它们解决的根本不是同一个问题:

问题
Iceberg / DuckLake
duckdb_object_storage
数据存在哪
桶里的 Parquet 文件(开放格式)
桶里的 DuckDB 私有格式(经 SlateDB 对象)
元数据/事务
需要一个 catalog 服务(Postgres/DuckDB 文件)
什么都不需要,manifest 就在桶里
谁能读写
多引擎
(Spark/Trino/Flink/…)
只有 DuckDB
(原生 DuckDB 都打不开,桶里是 SlateDB 对象不是 .duckdb 文件)
二级索引/约束
没有(Parquet 没地方放)
完整保留:ART 索引、主键、外键、CHECK、视图、宏、序列、枚举
小写入代价
新数据文件 + 删除文件 + compaction 任务
一次 WAL + 下次 checkpoint,无 compaction 作业
多写者
靠 catalog 事务协调
单写者(fencing 兜底)+ N 只读

决策一句话:

  • 数据要被 Spark/Trino/其他引擎共享,或者你要的是「湖上的一张表」 → Iceberg/DuckLake,没有悬念;
  • 你就是要一个完整的 DuckDB 数据库,只是希望它住在远端、可被多个只读进程共享、

计算节点可以无状态 → 这个扩展;

  • 两者不互斥:同一个 DuckDB 会话可以同时 attach 一个 duckdb_objfs:// 库、

一个 Iceberg catalog、一个 DuckLake,然后跨三者 join。作者的定位是"你不必在

两个世界之间做选择"。

顺带一提生态里的另一条平行线:有个叫 Moraine 的项目(alexpropp 维护)把

SlateDB 用在了完全不同的位置——作为 DuckLake 的 catalog 后端,把 DuckLake

元数据(快照/表/事务提交)映射到 SlateDB 的事务 KV 模型上,干掉湖仓里「唯一有状态

的 catalog 服务」。两个项目一上一下(一个替换磁盘,一个替换 catalog),

共同说明 SlateDB 在 DuckDB 生态里正在长成一个基础设施件。

● ● ●

四、上篇遗留问题的后续

上篇结尾说「2 个 star,很早期」——这周数据变了:官方社区扩展页显示 **25 star、

308 次/周下载;GitHub API 实测已经到 55 star**(10 月 14 日取数),一周涨了一个数量级还多。对一个 0.1.0、9 月底刚合并的扩展,

这个增速本身说明「单机 DuckDB 顶到天花板」的痛是真的。

已知限制清单(上篇已列,这里补一条作者新确认的):

  • Windows 全系列不支持(description.yml 明确排除);
  • 单写者,第二个写者靠 manifest fencing 事后兜底,官方定性为「配置错误」;
  • 只读可见性延迟约 10 秒(manifest 轮询间隔,未来会做成可配置);
  • 桶里的对象不是 .duckdb 文件,原生 DuckDB 打不开,数据进出走 COPY FROM DATABASE;
  • 新增
    :还没有目录列举,所以对 duckdb_objfs:// 路径做 glob 不工作。

作者把下一步排了优先级:性能第一(本地写 0.19×、冷 S3 读 1.2× 是两个缺口),

然后是强一致模式、单 WAL、以及一个架构上的"终局"——从现在挂在 FileSystem 层

(只能看到字节和偏移)上移到 BlockManager 层(直接操作 block)。目前卡在 DuckDB

上游:存储管理器直接 new SingleFileBlockManager,扩展还插不进去,

需要上游开一个小扩展点。这也解释了为什么它是社区扩展而不是核心特性——

FileSystem 层是"不动 DuckDB 就能落地"的最浅切入点,先用它跑通全链路,再谈往深走。

● ● ●

五、结语:单文件时代的结束方式

DuckDB 的"单文件"是它最大的卖点:一个 .duckdb 文件装下整个数据库,

拷贝、备份、分发都极其简单。这次变化没有摧毁这个卖点,而是给它加了个下标——

单文件不再必须是本地文件。

对临时分析、无状态容器、多进程只读服务这些场景,「桶 + 凭证」取代「文件 + 目录」,

运维模型直接对齐整个现代数据栈:对象存储是事实源,计算随便 disposable。

而对 DuckDB 自己,Duckherder 打开了它过去十年刻意不碰的那扇门——分布式。

一个 25 star 的社区扩展,可能正在给"单机之王"装上第一台引擎。

你上篇问的「单机嵌入式 vs 对象存储分布式」这道选择题,

现在变成了:先选前者还是先选后者——因为「库在桶里、worker 随便加」这个选项,第一次真实地出现了。

● ● ●

参考来源

01  作者博客《A Real DuckDB Database in S3, With Nothing Else to Run》(2026-10-09):https://dentiny.github.io/posts/duckdb-on-object-storage/ —— Duckherder 示例、性能结论、路线图均出自此文

02  作者基准数据 benchmark.md(TPC-H Q1–Q22 全表):https://github.com/dentiny/duckdb-object-storage/blob/main/benchmark.md

03  官方社区扩展页(25★、308 次/周下载):https://duckdb.org/community_extensions/extensions/duckdb_object_storage.html

04  SlateDB 官方介绍(对象原生 LSM 设计):https://slatedb.io/blog/introducing-slatedb/

05  Moraine(SlateDB 作 DuckLake catalog 后端,平行项目):https://morainedb.github.io/

06  续篇前作:《DuckDB 数据库整个搬进 S3,单文件时代结束了》