来源:https://duckdb.org/2026/05/12/quack-remote-protocol

Quack:DuckDB 客户端-服务器协议

作者: DuckDB 团队
日期: 2026-05-12

摘要: DuckDB 实例现在可以使用 Quack 远程协议相互通信。这让你可以在客户端-服务器设置中运行 DuckDB,并支持多个并发写入者。秉承 DuckDB 的精神,Quack 设置简单,并建立在 HTTP 等成熟技术之上。它速度也很快,能够支持从批量操作到小型事务的各种工作负载。

背景:数据库架构

当数据库最初出现时,并没有"客户端"和"服务器"之分,整个数据库就运行在一台计算机上。在 80 年代的某个时候,Sybase 首次引入了数据库"服务器"和运行在不同计算机上的"客户端"的概念。从那以后,人们就假设每个数据库系统都使用客户端-服务器架构以及用于它们之间通信的协议。这很方便,因为单一的可变状态保持在服务器控制下的单一位置,并且可以有许多客户端同时读写数据。当然,这种方法也有缺点,最明显的是,这些协议可能会增加大量开销。如果你想了解更多,我们之前写了一篇关于数据库协议的研究论文。

当然,一直有反对客户端/服务器架构的人,最著名的是 2000 年无处不在的 SQLite,当然还有 2019 年首次发布的 DuckDB。我们大力宣传了实现进程内架构,即没有客户端/服务器,没有协议,只有底层 API 调用。这对于交互式用例(例如数据科学)非常有效,分析师可以在 Python notebook 等环境中与数据进行交互,而他们的数据在同一个进程中运行的 DuckDB 实例中进行管理。对于许多仅将 DuckDB "粘合"到现有应用程序以对该应用程序中的数据提供 SQL 功能的用例来说,它也非常有效。

当尝试从多个进程同时修改同一个数据库文件时,进程内系统的"效果不佳"。有很多用例与此相关,例如,当从一堆收集遥测数据的进程插入到同一个数据库,同时查询同一个表来驱动仪表板时。有一些很好的技术原因导致我们无法实现这一点,最显著的是,DuckDB 在主内存中保存了大量状态,如果多个进程同时开始进行更改,则必须同步该状态。

是的,有一些变通方法。当然,你可以设计一个自定义的远程过程调用(RPC)解决方案,其中一个进程持有 DuckDB 数据库实例,并向其他进程提供查询和插入数据的服务。也有多个项目为 DuckDB 改装了客户端/服务器能力,例如使用 Arrow Flight SQL 协议。MotherDuck 有自己的自定义客户端-服务器协议。当然,你总是可以(天哪)切换到一个更传统的、支持客户端-服务器的数据库系统,例如同样无处不在的 PostgreSQL。然后你甚至可以通过使用各种启用此功能的扩展之一(例如 pg_duckdb)来运行所谓的"EleDucken",即在上述 PostgreSQL 中运行 DuckDB。

人们为将客户端-服务器解决方案绑定到 DuckDB 而构建的大量变通方法至少让我们相信,这是人们关心的事情。我们将 DuckDB 视为一个通用的数据整理工具。如果这意味着除了进程内功能之外还需要一个客户端-服务器协议——那也没问题。如果这最终解锁了大量新的、DuckDB 可以发挥作用的场景——那太好了!最终,我们深切关心用户体验,也许对架构的最终话语权不那么在意。所以我们咬紧牙关,最终,今天,我们非常高兴地宣布结果:

为 DuckDB 引入 Quack 协议

如果两只(或更多)鸭子想互相交谈,它们会做什么?它们会"嘎嘎"叫!因此,我们很自然地也需要将两个 DuckDB 实例可以用来相互通信的协议称为"Quack"!我们有机会在 2026 年从头开始设计一个数据库协议,而无需考虑任何遗留问题,这是一种奢侈。我们能够从现有协议中学习,包括较新的 Arrow Flight SQL 等。在我们深入探讨 Quack 内部工作原理之前,让我们先从用户的角度看看它是如何工作的。首先,你需要两个 DuckDB 实例。没错,DuckDB 将同时充当客户端和服务器!这两个实例可以位于相隔万里的不同计算机上(或在太空中),或者只是你笔记本电脑上的两个不同终端窗口。首先,我们需要在两个 DuckDB 实例中安装 Quack 扩展。目前,Quack 位于 core_nightly 仓库中,并在当前发布版本 DuckDB v1.5.2 中可用:

-- DuckDB #1
INSTALL quack FROM core_nightly;
LOAD quack;

CALL quack_serve(
    'quack:localhost',
    token = 'super_secret'
);

CREATE TABLE hello AS
    FROM VALUES ('world') v(s);
quack:
-- DuckDB #2
INSTALL quack FROM core_nightly;
LOAD quack;

CREATE SECRET (
    TYPE quack,
    TOKEN 'super_secret'
);

ATTACH 'quack:localhost' AS remote;
FROM remote.hello;

这应该在 DuckDB #2 中显示远程表 hello 的内容,也就是 world。魔法!我们还可以将数据从本地实例复制到远程实例:

-- DuckDB #1

-- 第二步
FROM hello2;
quack:
-- DuckDB #2
-- 第一步
CREATE TABLE remote.hello2 AS
    FROM VALUES ('world2') v(s);

同样,你应该在 DuckDB #1 的输出中看到 world2。显然,这些是我们能想到的最基本的例子。表可以复杂得多,查询可以复杂得多,数据量可以非常大(见下文)。还有一种方法可以使用 query 函数将整个逐字的查询发送到远程端,这对于大型数据集上的非常复杂的查询更好,并且可以更精确地控制远程执行的内容:

-- DuckDB #1
-- 等待提供服务数据
quack:
-- DuckDB #2
FROM remote.query(
    'SELECT s FROM hello'
);

当然,还有很多东西可以看。请参阅我们的文档以获取更多详细信息。

协议设计

基于 HTTP

Quack 直接构建在古老的 HTTP(超文本传输协议)之上。从其在 CERN 的卑微开端开始,HTTP 已成为 TCP 及其下所有协议栈之上的事实上的协议层。整个堆栈都经过优化,可以高效地传输 HTTP 消息流。如果实现得当,该协议的开销出奇地低。每个人和他们的弟弟都知道如何在负载均衡、身份验证、防火墙、入侵检测等方面处理 HTTP。在 2026 年,不将数据库协议构建在 HTTP 之上将是相当不明智的。HTTP 还允许 DuckDB-Wasm 发行版原生地使用 Quack 协议!因此,在浏览器中运行的 DuckDB 可以例如使用 Quack 直接连接到在 EC2 服务器中运行的 DuckDB 实例。

请求-响应模式

Quack 上的交互始终由客户端以请求-响应模式驱动。Quack 消息例如是连接请求,使用令牌进行身份验证,如上所示。有关身份验证和授权如何详细处理,请参见下文。后续消息是执行查询并返回结果第一部分的请求,以及检索大型结果的后续获取消息,可能来自多个并行线程。

序列化

请求和响应使用新的 MIME 类型 application/duckdb 进行编码。这种编码利用 DuckDB 内部高效的序列化原语来处理复杂结构,如数据类型和结果集。例如,我们在预写日志(WAL)文件中已经使用相同的原语多年,这意味着它们经过了相当好的优化和实战测试。

加密

虽然我们希望 Quack 能"正常工作",但我们也警惕直接将数据库连接到邪恶互联网的安全噩梦,这以前发生过。这就是为什么默认情况下 Quack 会在服务器启动时生成一个随机身份验证令牌,然后必须将其提供给客户端。此外,Quack 服务器默认只绑定到 localhost(当然可以被覆盖)。Quack 默认不使用 SSL,因为仅仅为了 localhost 通信而引入所有基础设施并添加依赖项有点愚蠢。我们不建议直接将 DuckDB Quack 端点开放到互联网。相反,我们强烈建议,如果你选择将 Quack 暴露给万维网,则应使用常见的 HTTP 端点(如 nginx),并让该代理终止 SSL(例如,使用 Let’s Encrypt)。Quack 客户端将假定对非本地连接启用了 SSL,这可以被覆盖。我们在文档中提供了相关指南。

往返次数

我们一直很注意优化查询的协议往返次数或请求/响应对的数量。一旦连接,一个查询可以在单次往返中完全处理完毕。这对于延迟敏感的环境来说是一个关键的优化。同时,我们为高效的批量响应传输认真优化了 Quack。据我们所知,Quack 目前是通过套接字推送表的最快方式,数百万行可以在几秒钟内传输完成。下面是一些基准测试结果。

身份验证和授权

数据库查询的身份验证和授权是无穷无尽的欢乐和复杂性之源。我们可能无法捕捉到每个人的用例,尤其是在第一个版本中。因此,明智的做法是不去尝试。对于 Quack,我们选择了一种与 DuckDB 可扩展性理念相结合的身份验证模型。目前已经有数百个 DuckDB 扩展。Quack 附带一个默认的身份验证方法,并且没有授权限制,但两者都可以被用户提供的代码覆盖。正如你上面看到的,Quack 服务器在启动时会生成一个默认的随机身份验证令牌。当客户端连接时,它提供一个身份验证字符串。服务器端将调用一个身份验证回调函数。默认情况下,它会将客户端提供的令牌与之前随机生成的令牌进行比较。但是这个回调可以通过配置来改变!你可以带上你自己的身份验证函数,例如查询 LDAP 目录、读取文本文件,或者只是掷骰子。由你决定。类似地,授权函数也可以改变。默认的授权函数对所有事情都说"是",但你可以检查客户端试图执行的每个查询,将查询与先前使用的身份验证字符串关联起来等等。这些回调甚至可以是普通的 SQL 宏!请参阅我们的文档以获取更多详细信息。

默认端口

默认情况下,Quack 服务器监听 9494 端口,数字 94 很容易记住,因为 Netscape Navigator 在那一年发布。

基准测试

我们设置了两个基准测试来展示 Quack 协议。这些基准测试在运行 Ubuntu on Arm 的 AWS 虚拟机上运行。我们选择了 m8g.2xlarge 实例类型,它有 8 个 vCPU 和 32 GB 内存,重要的是,网络带宽"高达 15 Gbps"。我们重现了一个真实场景,其中客户端和服务器位于同一数据中心,但在不同的机器上。我们确保两个实例都在同一个"可用区"中。实例之间的 Ping 时间平均约为 0.280 毫秒。

批量传输

第一个基准测试测试批量传输,即应该通过数据库协议传输相当大量行的情况。如果你读过我们上面链接的论文,你就会知道这是传统数据库协议难以应对的情况。我们将 Quack 与两个系统进行比较:广泛使用的 PostgreSQL 协议和较新的 Arrow Flight SQL 协议。Arrow Flight 由 GizmoSQL 服务器提供,该服务器也内部使用 DuckDB。我们传输不断增加的行数的 TPC-H lineitem 表,一直到高达 6000 万行(CSV 格式为 76 GB!),并报告 5 次运行的中位墙钟时间。我们期望现代面向批量的协议将远远超过 PostgreSQL 协议。结果如下:

[此处假设有图表,原文标题为:批量传输操作的运行时间(越低越好)和批量传输性能]

想看表格形式的结果吗?点击这里。

我们可以看到 Quack 在批量结果集传输方面表现出色,在 5 秒内传输了 6000 万行!即使是专门构建的 Arrow Flight SQL 协议在这方面也无法与之竞争,而 Postgres 的基于行的协议总体上相当无望。

公平地说,我们必须提到,标准 PostgreSQL 客户端不会在多线程上并行化读取,但 Quack 和 Arrow 可以。厚颜无耻的自我宣传:DuckDB 的 PostgreSQL 客户端在某些情况下也可以做到这一点!

小规模写入

第二个基准测试测试小规模追加。这是一个常见的用例,例如,将可观测性数据集中到一个中央 DuckDB 实例中。这以不同的方式考验数据库协议,例如,完成单个事务需要客户端和服务器之间的多次往返将是一个劣势。我们通过创建一个与 TPC-H lineitem 表具有相同结构的空表来测试这一点,然后插入随机值,每个行都在其自己的 INSERT 事务中。插入的值在一定程度上遵循了通常基准生成器的分布。我们运行了五分钟内不断增加数量的并行线程。我们重复了五次实验,并报告了中位数每秒事务数。

我们期望像 PostgreSQL 这样高度事务优化的系统能够主导这个基准测试。我们也期望针对批量优化的 Arrow Flight 表现得不会特别好。

[此处假设有图表,原文标题为:小规模写入的吞吐量(越高越好)和小规模写入性能]

想看表格形式的结果吗?点击这里。

相当令人惊讶的是,我们看到 Quack 在高达 8 个并行线程时优于 PostgreSQL,达到大约每秒 5,500 个事务的最大事务率。超过这个限度,我们遇到了 DuckDB 本身在同一个表上每秒并发插入的当前限制。PostgreSQL 在这里扩展性更好,这是我们近期需要研究的问题。正如预期的那样,Arrow Flight 表现不佳,大约比 Postgres 慢一半。

基准测试脚本可在线获取。

结论

今天,我们发布了 Quack,一个 DuckDB 的客户端/服务器协议,以及作为 DuckDB 扩展的初始实现。Quack 解锁了 DuckDB 的完整多人体验,多个独立的进程——本地或远程——现在可以并行修改表的内容,而不会互相锁死。虽然这些功能部分已经可以通过 DuckLake 实现,但 Quack 使其简单得多,并提供更高的性能。

用例

有了 Quack,DuckDB 现在可以在广泛的新用例中发挥作用,这些用例中集中化状态比超本地查询更重要。随着数据湖的兴起,我们已经认识到数据并不总是本地的。说到湖,Quack 也将被集成到 DuckLake 中,以便 DuckDB 本身可以成为一个可远程访问的目录服务器。这将解锁新的能力,例如用于数据内联。如果你对此有更多疑问,请查阅 Quack FAQ。

总体而言,DuckDB 正在从其最初的交互式分析进程内数据库的利基市场,进一步发展为现代数据架构的核心构建块。我们使用 Quack 已经有一段时间了,并且非常期待听到你将用它构建什么。如果你对如何改进 Quack 有任何建议,请告诉我们!嘿,《流言终结者》已经证明鸭子的"嘎嘎"叫声会产生回声,所以让我们看看这会引发什么样的噪音。

下一步计划

当然还有很多事情要做。首先,我们将把 Quack 集成到 DuckLake 中,这样就有可能使用远程 DuckDB 服务器作为 DuckLake 的目录!我们期望这将大大提高性能,尤其是在内联方面。接下来,我们将在未来几个月内完善 Quack,并在今年秋天与 DuckDB v2.0 一起发布第一个生产版本。例如,我们计划在需要时启用 Quack 扩展的自动安装和自动加载。我们还将使用新的解析器,改进从 DuckDB 与远程 SQL 数据库通信的语法。在 DuckDB 核心方面,我们计划致力于大幅提高每秒可达成的事务数,以便我们能够将事务扩展到远远超过八个并行线程。

此外,我们正在考虑允许在身份验证和授权之外扩展 Quack 协议,例如,允许 DuckDB 扩展添加新的协议消息和代码来处理它们。我们也在考虑在 Quack 之上添加一个复制协议,以便对 DuckDB 实例的更改可以复制到其他服务器,例如设置一个只读副本集群。

如果你想了解关于 Quack 的更多信息——并了解它的初步采用情况——请于 6 月 24 日参加我们的社区会议 DuckCon #7。DuckCon 将由 DuckDB 的联合创始人发表"State of the Duck"演讲。你可以亲临现场,也可以在 YouTube 上观看在线直播。

PS:我们为 Quack 项目设置了一个单独的页面,请务必访问一下。

致谢

我们要感谢 MotherDuck 的 Boaz Leskes 与我们分享他们在构建 MotherDuck 协议方面的经验教训。我们还要感谢 GizmoSQL / GizmoData 的 Philip Moore,他已经为我们开辟了这条道路,并证明了客户端-服务器 DuckDB 是一件非常有价值的事情。

附录:为什么不选择 Arrow Flight SQL?

我们还需要解决房间里的大象之一:我们到底为什么不使用现有的 Arrow Flight SQL 协议?它就在那里。它是可用的。有现有的实现。我们看到了 Arrow 和相关项目(如 ADBC)的价值:它们是像它们之前的 ODBC 和 JDBC 一样的交换 API,旨在减少系统间交换数据的摩擦。这效果很好。

然而,我们也对在 DuckDB 内部使用像 Arrow 这样的交换格式持谨慎态度。虽然 DuckDB 用于查询中间结果的内部结构在某些方面与 Arrow 接近,但在其他方面却截然不同。我们觉得,为了能够持续在数据系统领域进行创新,我们不能让自己受到外部控制的格式的限制。这就是为什么我们在 Quack 中使用自己的序列化。如果我们想添加一个新的数据类型或协议消息,我们可以明天就发布。

在更深层次上,Arrow Flight SQL 中还有一个致命的设计决策:每个查询至少需要两次协议往返,即 CommandStatementQueryDoGet。这对于像我们上面的第二个实验那样的小规模更新来说并不理想,尤其是在高延迟环境中。如前所述,我们将 Quack 设计为能够对小型查询执行单次往返的查询执行和结果获取。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐