Skip to content

필사 모드: DuckDB 有了客户端-服务器协议 — Quack 改变了什么,又没有改变什么

中文
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

引言 — 进程内数据库拥有服务器意味着什么

DuckDB 的身份认同长期以来可以用一句话概括 — 没有客户端,没有服务器,没有协议,只有底层的 API 调用。自 2019 年首次发布以来,DuckDB 团队一直相当高调地标榜这种进程内(in-process)架构,而这确实带来了价值。在 Python notebook 里分析师摆弄自己数据的工作负载中,在给现有应用附加 SQL 能力的工作负载中,零协议开销都是压倒性的优势。

然而在 2026 年 5 月 12 日,同一个团队发布了 Quack: The DuckDB Client-Server Protocol — 一套让两个 DuckDB 实例跨网络互相通信的协议。一方成为服务器,一方成为客户端。

这件事有趣的地方不在于性能数字,而在于一个把「我们不是客户端-服务器」当作架构信念坚持了 7 年的项目,如今自己给自己装上了这个东西。发布文章的措辞相当坦诚地解释了这一转变 — "In the end we care deeply about user experience and perhaps less about having the last word on architecture."

因此,本文不打算反复重申「DuckDB 现在有服务器了!」,而是看看到底哪些事情变得可能了,哪些依然不行。后者的清单要长得多。

先把事实说清楚

要拨开炒作,最好先把日期和版本钉死。

  • 2026 年 5 月 12 日 — Quack 发布。这个时间点它还位于 core_nightly 仓库中,可在当时的最新版本 DuckDB v1.5.2 上使用。
  • 2026 年 5 月 20 日 — 在 DuckDB v1.5.3 中,Quack 升级为核心扩展。现在首次使用时会透明地自动安装、自动加载。同一版本还让 DuckLake 可以把 Quack 当作目录数据库使用。
  • 状态: 测试版(beta)。 引用 1.5.3 发布文章的原话 — "Quack is still in beta state and breaking changes may happen in the protocol, in function names, etc."
  • 稳定化目标: DuckDB v2.0。 Quack FAQ 写的是「as part of DuckDB v2.0 in September 2026」,而发布日历把 2.0.0 标为「Fall 2026」,并明确注明这些日期是暂定的(tentative)。两个页面的措辞略有出入,所以不要把 9 月当作确定日期。

也就是说,在你读到这篇文章的此刻(2026 年 7 月),Quack 只适合原型验证。FAQ 对「Is Quack production-ready?」的回答就是简简单单的「Not yet」。

长什么样

从用户视角看,形态出人意料地简单。启动两个 DuckDB 实例,在其中一个上启动服务器。

-- DuckDB #1 (服务器)
CALL quack_serve('quack:localhost', token = 'super_secret');
CREATE TABLE hello AS FROM VALUES ('world') v (s);
-- DuckDB #2 (客户端)
CREATE SECRET (TYPE quack, TOKEN 'super_secret');
ATTACH 'quack:localhost' AS remote;
FROM remote.hello;

挂载(attach)之后,远程表看起来就像本地表一样。DDL、写入、事务都会原样传递过去。

CREATE TABLE remote_db.t AS FROM range(10) r (i);  -- 远程 DDL
INSERT INTO remote_db.t VALUES (42);               -- 远程写入
FROM remote_db.t WHERE i = 42;                     -- 过滤也在远程执行
BEGIN; ...; COMMIT;                                -- 事务透传

也可以不挂载,一次性直接扔一条查询过去。

FROM quack_query('quack:localhost', 'SELECT 42', token = 'MY_TOKEN');

协议设计 — 为什么是 HTTP

有几处设计选择很显眼,全部记录在官方文档的 Overview里。

基于 HTTP。 发布文章的逻辑与其说是防御性的,不如说是实用主义的 — "It would be rather misguided not to build a database protocol on top of HTTP in 2026." 理由也很清楚: 负载均衡、身份验证、防火墙、入侵检测 — HTTP 是这整套基础设施都已经知道怎么处理的一层。不需要再新运维一套自定义的传输协议。还有一个附带好处: DuckDB-Wasm 发行版可以原生使用 Quack 通信,于是浏览器里的 DuckDB 能直接挂载到 EC2 上的 DuckDB。

application/duckdb 序列化。 他们创建了一个新的 MIME 类型,编码方式直接复用 DuckDB 内部的序列化机制 — 和写 WAL 文件用的是同一条代码路径。据文档说明,这样一来就不需要经过交换格式,嵌套类型、decimal、interval 这类复杂类型可以无损传输。

客户端驱动的请求-响应。 所有交互都由客户端发起,没有服务器端主动推送(push)的动作。

每个查询一次往返。 完成连接握手之后,一个查询在一对请求-响应中就结束了。较大的结果会通过后续的 FETCH 请求分块流回,并且可以用多线程并行化。

默认端口 9494。 团队给出的解释是: 94 是网景导航者(Netscape Navigator)问世的年份,好记。

这里有个有趣的地方,是 DuckDB 团队在借力自己的历史。这批人在 2017 年写过一篇论文 Don't Hold My Data Hostage — A Case for Client Protocol Redesign(Raasveldt、Mühleisen,VLDB 2017),实测了把大结果集从数据库搬到客户端有多昂贵、既有协议为什么低效。9 年后,这篇论文的作者们获得了从零设计一套协议的机会。

为什么不是 Arrow Flight SQL

这是个自然会被问到的问题,发布文章也专门加了一个附录来回答,理由分两条线。

一条是技术上的。 团队指出 Arrow Flight SQL 有「one fateful design decision」— "every single query requires at least two protocol round trips, CommandStatementQuery and DoGet." 两次往返对小型更新不利,在高延迟环境下尤其如此。这正是 Quack 执着于单次往返的原因。

另一条是政治上的,也更加坦率 — "We feel that in order to be able to keep innovating in data systems, we cannot allow ourselves to be restricted by formats that are controlled externally." 换句话说,他们不想被外部控制的格式束缚住。想加一个新的数据类型或协议消息时,得能在明天就发布出去。

这与其说是技术优劣的问题,不如说是治理层面的偏好。团队也承认 Arrow 和 ADBC 的价值 — 作为 ODBC、JDBC 的继任者,作为减少系统间数据交换摩擦的互操作 API,他们写道它「works pretty well」。只是在 DuckDB 内部使用交换格式这件事上,他们保持警惕,FAQ 把这总结为「clean slate implementation from the core DuckDB team」,并且「does not have any third-party dependencies」。

作为读者,这段说法可以照单接受,但含义得自己算清楚: Quack 是 DuckDB 专属的。客户端和服务器都必须是 DuckDB。Arrow Flight SQL 的优势 — 异构系统用同一套协议对话 — Quack 在设计上就放弃了。填补这个空缺的第三方替代方案依然存在,比如 airport 扩展和 GizmoSQL,发布文章还向 GizmoSQL 的 Philip Moore 致谢,感谢他「先一步开辟了这条路」。

基准测试 — 先说清楚这是厂商自测

现在来看数字。先把话说死: 下面所有的基准测试,都是 DuckDB 团队自己设计、自己跑的,是厂商自测(vendor self-measurement)。 在一篇发布自家协议的文章里,自家协议赢了 — 这不奇怪,本身也不算不诚实,但需要看清楚测了什么、没测什么。

测试环境是写明的: AWS m8g.2xlarge(8 vCPU,32GB 内存,网络「up to 15 Gbps」),Arm 上的 Ubuntu,客户端和服务器是同一可用区内的不同机器,实例间平均 ping 约为 0.280 ms。取 5 次测量的中位数。基准测试脚本也是公开的。作为厂商基准测试,这已经算相当认真的了。

批量传输 — 随着 TPC-H lineitem 表行数增加,传输所花的实际时间(越低越好)。

行数DuckDB QuackArrow FlightPostgreSQL
100k0.07 s0.07 s0.20 s
1M0.24 s0.38 s2.20 s
10M0.89 s2.90 s25.64 s
60M4.94 s17.40 s158.37 s

小规模写入 — 向一张和 lineitem 结构相同的空表中,逐行以各自独立的 INSERT 事务写入,持续 5 秒,随并行线程数增加测得的每秒事务数(越高越好)。

线程数DuckDB QuackArrow FlightPostgreSQL
11,038 tx/s469 tx/s839 tx/s
21,956 tx/s799 tx/s1,094 tx/s
43,504 tx/s1,224 tx/s2,180 tx/s
85,434 tx/s1,358 tx/s4,320 tx/s

这些数字该信到什么程度

有一处,是团队自己披露的注意事项。 批量传输表下面写着 — "In fairness we have to mention that the standard PostgreSQL clients do not parallelize reads over multiple threads, but Quack and Arrow can." 也就是说,PostgreSQL 那 158 秒里掺杂着「标准客户端是单线程读取」这个先天劣势。发布文章自己把这一点写出来了,这一点该给加分。

Arrow Flight 那一侧的条件也得看清楚。 用于对比的 Arrow Flight 是由 GizmoSQL 服务器提供的,而这个服务器内部用的正是 DuckDB。所以这不是「Arrow Flight 协议理论上的最佳表现」,而是「某一个具体实现」。与其说是协议对比,不如说更接近实现对比。

写入基准测试的设定偏宽厚。 逐行以独立事务方式 INSERT,这种工作负载正是 PostgreSQL 生来要解决的事,列式分析引擎没有理由能在这上面占优。可即便如此,8 线程以内 Quack 都是赢的。这里团队自己给自己拆了台 — "Beyond that, we hit a current limitation of DuckDB itself in concurrent insertions per second into the same table. PostgreSQL scales better here, which is something to look into for us in the near future." 他们自己承认,最高约 5,500 tx/s 就撞了天花板,再往上 PostgreSQL 扩展得更好。这也是为什么基准测试表格到 8 线程就停了。

一道算术题。 发布文章把这 60M 行介绍为「76 GB in CSV format!」。可如果 76GB 在 4.94 秒内传完,那就是每秒 15.4GB,约合 123Gbps。而写明的链路是「up to 15 Gbps」,也就是每秒约 1.9GB — 这在物理上是不可能的。把 76GB 原样灌进那条链路,至少要花 40 秒。结论不难得出 — 76GB 是想让人对数据集换算成 CSV 后的大小有个直观感受,而不是链路上实际跑过的字节数。Quack 的二进制表示要比 CSV 紧凑得多。发布文章并没有撒谎,但如果把这两个数字并排放在一起、脑补出「每秒 15GB」,那就是被误导了。

没测什么。 全部都是同一可用区、0.280ms ping。没有 WAN 或高延迟环境下的数字。这一点其实相当重要,因为 Quack 之所以在单次往返上下功夫,理由正是「latency-sensitive environments」。而恰恰是这种设计真正能派上用场的高延迟环境的测量值,缺失了。开启 TLS 的开销也没有(默认是明文,所以基准测试很可能也是明文跑的)。读写混合的工作负载也没有。

总结一下 — 批量传输快这个说法是可信的,甚至有可复现的脚本支撑,方向性也不令人意外(列式二进制传输碾压 Postgres 基于行的协议,这也是那篇 2017 年论文的结论)。但不应该把它读成「Quack 是比 PostgreSQL 更快的数据库」。那些表格测的是协议传输,不是查询引擎的基准测试。

安全默认值 — 这里最需要小心

安全文档的第一句话就吓人。Quack 服务器会暴露底层 DuckDB 实例的整个 SQL 表面 — 包括该会话能看到的所有表的读写权限。

正因如此,默认值设置得比较保守。

  • 服务器启动时会生成一个随机认证令牌,客户端每次连接都必须出示它。
  • 服务器只绑定 localhost。要开放到外部可达的地址,必须显式传入 allow_other_hostname => true
  • TLS 不由服务器自己处理。 理由是,把 TLS 基础设施引入 localhost 通信只会增加依赖,没有实际好处。

发布文章的建议也很干脆 — "We do not recommend opening up a DuckDB Quack endpoint directly to the Internet." 建议在前面放一个像 nginx 这样经过验证的 HTTP 反向代理,在那里终止 TLS。客户端对非本地 URI 默认假定使用 HTTPS(可覆盖)。

身份验证/授权模型,一如 DuckDB 的一贯作风,被推给了扩展来实现。发布文章的措辞很诚实 — "We are likely unable to capture everyone's use case, certainly not in a first release. The smart thing is therefore not to try." 于是它提供了默认的认证函数,以及一个「全部放行」的默认授权函数,但两者都可以用用户代码替换。要查 LDAP、读文本文件,还是掷骰子决定,随你。回调函数写成 SQL 宏也完全可以。

翻译成实务语言就是: 默认设置是安全的,但一旦你把它变得有用,就会一个个地关掉安全开关。 一旦打开 allow_other_hostname => true,你打开的就是一个「对该 DuckDB 会话能看到的所有表都拥有读写权限的明文 HTTP 端点」,而授权函数默认对所有查询都回答「是」。把反向代理和授权回调当成必选项而不是可选项来看待,才是正确的读法。

Quack 不做什么

这是本文的核心部分,是 FAQ 很贴心地明确回答过的几个要点。

不支持分布式查询处理。 FAQ 原文照录 — "Does DuckDB with Quack support distributed query processing? Currently, DuckDB with Quack does not support distributed query processing." Quack 是多个客户端挂载到一台服务器上的结构,而不是多个节点分摊处理同一条查询。「一条查询单机装不下」这个问题,Quack 解决不了。发布后不久,社区里就出现了「这解决了水平扩展问题」的反应(这是 InfoQ 报道里引用的反应之一),但文档说的并不是这个意思。Quack 扩展的是访问,不是计算

容量有天花板。 FAQ 里和 DuckLake 对比的条目给出了数字 — Quack 服务器用的是 DuckDB 原生的 DB 格式,这个格式「scale up to a few terabytes」。要是需要 PB 级规模,文档直接指出那是 DuckLake(或 Iceberg 一类系统)的地盘。

写入并发有天花板。 就是上面看到的约 5,500 tx/s,超过 8 线程就撞上了 DuckDB 自身的限制。团队写了会去改进("we plan to work on greatly increasing the transactions per second achievable"),但这是将来时。如果你现在就需要每秒数万笔写入,这不是答案。FAQ 对「能不能用于 OLTP 工作负载」给出的答案是「Yes」,但附带的条件是「a few thousand writes per second on a server with 8 CPUs and 32 GB RAM」。「几千」符不符合你的数字,只有你自己知道。

目前还是测试版。 协议、函数名、默认设置,全都可能变。

还有反方向同样重要的一点。进程内模型不会消失。 FAQ 也把这钉死了 — "Yes, DuckDB as an in-process database will continue working just as it did before, and we will treat the in-process deployment model as a first-class citizen in the DuckDB ecosystem." Quack 是加法,不是转向。在 notebook 里翻 Parquet 文件的人,不需要读这篇文章。

Quack vs DuckLake — 容易混淆的地方

两者都标榜「多人版 DuckDB」,都是扩展,都允许多个 writer,所以很容易混淆。FAQ 整理的区别是这样的。

QuackDuckLake
仅限 DuckDB?是(DuckDB 之间互相通信)否(也支持 Spark、DataFusion、Trino 等)
存储DuckDB 原生 DB 格式对象存储 + Parquet
规模数 TB可达 PB 级
搭建双端安装扩展中心目录数据库 + 对象存储

发布文章写道: 「part of this could also already be achieved with DuckLake, Quack makes this far simpler and provides far higher performance」。不过这仅限于并发写入访问这个狭窄的维度,两者的目的并不相同。

而且这两者并不互斥。从 v1.5.3 起,DuckLake 可以把 Quack 当作目录数据库来用。FAQ 的建议很清楚 — 如果数据量巨大,或者需要时间旅行(time travel)功能,就用 DuckLake,但通过一个由 Quack 提供服务的 DuckDB 实例来访问它。发布文章预告的下一步也是这个方向: DuckDB 服务器本身成为一个可远程访问的 DuckLake 目录服务器,并预期在数据内联(data inlining)性能上获得提升。

DuckLake 的基本理念 — 把元数据放进一个 SQL 数据库,而不是散落在大量 JSON、Avro 文件里 — 已经在《DuckDB 实战》一文中讨论过。

那么,你该不该用

用了值回成本的情况

  • 多个进程需要同时读写同一个 DuckDB 数据库。 这是 FAQ 列出的第一个场景。发布文章的例子很具体 — 多个采集遥测数据的进程同时向同一个数据库插入,与此同时仪表盘在查询同一张表。到目前为止,这在 DuckDB 上就是做不到的事,人们要么手写自定义 RPC,要么干脆换成 Postgres。
  • 数据在服务器上,你想把计算挪到数据附近。 大服务器负责处理,笔记本电脑上只看结果。
  • 经常把大结果集拉到客户端。上面那些批量传输的数字,正是能派上用场的地方。
  • 需要从浏览器(Wasm)挂载到远程 DuckDB。这是选择基于 HTTP 带来的直接红利。
  • 一直在手写 RPC,想给 DuckDB 硬塞上客户端-服务器功能。那些代码现在可以删了。

目前还不行的情况

  • 生产环境。 还是测试版,团队自己也这么说。
  • 需要分布式查询处理。 设计上就没有。
  • 真正的 OLTP。 每秒数万笔事务、高写入并发 — 请用 Postgres。就连 DuckDB 团队自己都写道,超过 8 线程 Postgres 更好。
  • PB 级规模。 这是 DuckLake 或 Iceberg 的地盘。
  • 进程内已经够用。 大多数 DuckDB 用户都属于这一类,没有理由白白放弃零协议开销。
  • 需要异构客户端接入。 Quack 要求双端都是 DuckDB,Java 应用或 Go 服务没法直接跟它对话。

结语

Quack 里最有意思的地方,不是协议设计本身,而是设计背后的判断。DuckDB 团队一直把进程内架构当作自己的品牌。但人们在它之上硬造了太多客户端-服务器的变通方案 — 自定义 RPC、套了 Arrow Flight 的 GizmoSQL、MotherDuck 自家的协议,甚至还有把 DuckDB 塞进 Postgres 里的 pg_duckdb — 最终团队承认了「这才是大家真正想要的东西」。发布文章把这个过程形容为「we bit the bullet, eventually, finally」。选择用户体验而非架构纯粹性的自我说明,就直接摆在文章里。

说句实话,结果范围很窄。 DuckDB 既没有变成分布式查询引擎,也没有变成 Postgres 的替代品。它带来的,只是多个 DuckDB 客户端可以挂载到一台数 TB 级的单一服务器上同时读写。可要说「只是」,恰恰正是这一点,才是人们造了 7 年变通方案的原因。

而且一如既往 — 该用你自己的工作负载来判断,而不是厂商基准测试。那些表格是 DuckDB 团队自己在同一可用区、0.280ms ping、明文 HTTP、脚本公开可复现的条件下测出来的。测得再认真的厂商基准测试,终究还是厂商基准测试。你的延迟、你的 TLS、你的混合工作负载下会不会得到同样的结果,等 v2.0 发布之后亲自测一遍才是正道。

参考资料

현재 단락 (1/111)

DuckDB 的身份认同长期以来可以用一句话概括 — 没有客户端,没有服务器,没有协议,只有底层的 API 调用。自 2019 年首次发布以来,DuckDB 团队一直相当高调地标榜这种进程内(in-p...

작성 글자: 0원문 글자: 10,932작성 단락: 0/111