你在餐馆点菜,如果厨师每叫一次食材,都站在仓库门口等人送来,再回灶台继续炒,厨房再快也快不起来。DuckDB 过去读取本机文件时,等候很短,这种做法问题不大。如今它开始查询 S3 等远程存储中的大型数据集,网络等待变长,工作线程原地空等就成了明显浪费。
DuckDB 官方博客披露,团队正在实现一套异步 I/O pipeline——发出读取请求后,执行查询的线程不用停在那里,可以先去解码数据、做聚合或处理其他任务。目前,这项能力可在 v2.0.0-dev 预览版中试用。不过,关于它是否已正式属于“DuckDB 2.0”、具体启用方式和完整性能数据,现有材料还不足以给出确定结论。以下实现细节也均来自 DuckDB 单一官方信源。
以前为什么不着急改?
DuckDB 是一种可嵌入应用或笔记本的列式数据库,主打本地分析。用户可以直接查询 Parquet、CSV 文件,不必先部署一套数据库服务器。Parquet 是面向分析的列式文件格式:查询需要哪几列,它就尽量只读哪几列;还可以根据统计信息跳过无关的数据块。
DuckDB 过去主要面对本机 SSD。它会使用“谓词下推”和“投影下推”:前者把筛选行提前,后者把选择列提前,让扫描阶段少读数据。文件就在机器里,延迟低、带宽高,读取通常不是主要矛盾。更费时间的往往是子查询、连接和聚合。
使用场景后来变了。官方博客称,DuckDB 已可查询 S3 等远程存储中的大型数据集,也可以通过 Quack protocol 以服务器形式运行。数据与计算不再总在同一台机器上。此时,少读数据仍然重要,但每次远程请求都要经过网络。如果引擎发不出足够多的并发请求,便难以用满可用带宽,线程会把大量时间花在等待上。博客将性能影响概括为可能“显著”下降,但没有在现有材料中给出对应数字。
把“取货”和“做菜”拆开
以扫描一个远程 Parquet 文件为例。DuckDB 会按 row group 划分 job。row group 可以理解为文件内部一批可独立处理的数据;job 则是调度系统分派的一份工作。每个 job 又包含一个或多个 fetch task,也就是实际取数据的任务。它们通过 byte-range request,只请求文件中指定的一段字节。
在同步 I/O 下,worker thread——真正执行查询的工作线程——发出请求后会阻塞。数据抵达之前,它不能继续解码,也不能做聚合。即使 CPU 此时有空,也只能等网络。
异步方案把线程分成两个池。REGULAR 池负责真正的计算,例如解码、连接和聚合;默认按每个可用 CPU 线程配置一个工作线程。ASYNC 池主要承担会被阻塞的 I/O 任务,例如等待 HTTP 响应。因为这些线程多数时间不消耗多少 CPU,DuckDB 默认配置的 ASYNC 线程数是系统线程数的 4 倍,总数最多 256。空闲的 REGULAR 线程也可以协助处理 I/O。
这样一来,ASYNC 线程可以持续把读取请求留在途中,REGULAR 线程则处理已经到手的数据。最初还没有数据可算时,扫描任务会暂时“停放”,把工作线程让给其他 pipeline task。第一个 job 准备好以后,下一批数据的读取便能与当前数据的解码重叠。异步 I/O 的价值不在于让单次网络请求凭空变快,而在于减少等待对整个查询流程的占用。
不能等用到时才去拿
只把读取交给另一组线程还不够。若当前数据处理完才请求下一批,工作线程仍会再次断粮。因此 DuckDB 加入了 read-ahead,也就是预读:REGULAR 线程解码当前 job 时,ASYNC 线程已经开始取后面的 job。
不同文件格式对 job 的划分不同。Parquet 以单个文件的一个 row group 为一个 job;一个 job 可能因查询选择的列、筛选下推结果和列在文件中的物理位置而拆成多个 fetch task。CSV 则按扫描边界划分,通常覆盖文件中的固定字节范围。
预读的目标,是让足够多的请求同时在途,用并发隐藏远程存储的延迟。不过它也有代价:提前拿回的数据必须占用内存。如果网络很快、解码很慢,尚未处理的数据会越积越多,甚至耗尽内存。官方称已为此实现异步内存治理,但现有供稿在展开具体机制前截断,无法说明它如何设定额度、暂停预读或恢复调度。
为什么这次升级值得看
这不是给查询语法增加一个新按钮,而是在改变底层的时间安排。过去的优化重点是“尽量少读”;异步读取处理的是另一个问题:必须读取的数据在路上时,计算资源能否继续工作。
这也对应 DuckDB 使用边界的变化。面向本机 SSD 时,同步读取简单且够用。面对远程对象存储后,网络延迟和并发能力开始决定性能上限。把存储等待与 CPU 计算重叠,意味着 DuckDB 的调度方式正从“读到一份、算一份”转向“有人持续取数,有人持续计算”。对经常直接扫描数据湖文件的用户,这类底层变化可能比单个算子提速更重要。
目前异步 I/O 已支持 Parquet,以及未压缩、可 seek(可以跳到文件指定位置读取)的 UTF-8 CSV。这个限定很重要:它不等于所有 CSV、所有文件格式都已支持。DuckDB 原生格式和 JSON 的支持仍计划后续加入。
局限与未知
- 所有实现与效果描述都来自 DuckDB 官方博客这一个信源,材料没有提供可独立核验的完整 benchmark 数字。
- 现有材料不足以确认正式版发布时间、版本要求和完整启用方式,因此不能把预览能力直接写成已经随 DuckDB 2.0 正式发布。
- 预读的内存治理细节及其在不同网络、文件规模和查询类型下的权衡,供稿未完整披露。