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 更快):
| 0.92× | ||
| 1.08× | ||
| 1.20× |
写(这里有两个反直觉的点,全篇最值得看):
| 0.19× | ||
| 0.95× |
这三个数字合起来读,结论很清楚:
- 内存/读为主的负载
:几乎无感(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 到底怎么选
上篇只说了「另一个方向」,这次给决策清单。它们解决的根本不是同一个问题:
| 多引擎 | 只有 DuckDB | |
决策一句话:
数据要被 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,单文件时代结束了》