DuckDB 异步 I/O 引擎:Parquet 读取性能提升 1.7 倍的秘密
2026 年 6 月,DuckDB 核心团队合并了一项令人瞩目的 PR #23142——为 Parquet 读取器引入异步 I/O 支持,带来高达 1.68 倍的性能提升。本文将深入分析这项技术的工作原理、基准测试结果以及如何在你的数据管道中利用这一改进。

为什么 I/O 会成为瓶颈?
在现代数据分析中,Parquet 几乎已成为事实上的列式存储格式。它的压缩效率高、支持谓词下推(predicate pushdown)、非常适合大规模数据分析。但当数据量达到几十 GB 甚至上 TB 时,传统的同步 I/O 模式会遇到一个隐蔽的性能瓶颈:
CPU 在等待磁盘或网络 I/O 时处于空闲状态,而磁盘/网络却在等待 CPU 发出读取指令。
这种"等待-执行-等待"的模式,在云环境(S3、GCS、OSS)中尤为严重——因为网络延迟远高于本地 SSD 的访问延迟。
DuckDB 异步 I/O 引擎的工作原理
同步 I/O 的局限
在 DuckDB 的旧有架构中,Parquet 读取器以同步方式工作:
- 主线程请求读取 Parquet 文件
- 主线程等待数据从磁盘/网络到达
- 数据到达后,主线程解析并过滤
- 重复以上步骤
这意味着每次 I/O 操作都会阻塞整个 Worker 线程,即使 CPU 此时毫无工作可做。
异步 I/O 的新架构
DuckDB 的异步 I/O 引擎重新设计了整个读取流程:
┌─────────────────────────────────────────────────────┐
│ DuckDB Query Engine │
│ │
│ ┌──────────┐ ┌──────────────┐ ┌──────────┐ │
│ │ Worker │───▶│ Task │───▶│ Result │ │
│ │ Thread │ │ Scheduler │ │ Buffer │ │
│ │ (计算) │◀───│ (调度) │ │ (结果) │ │
│ └──────────┘ └──────┬───────┘ └──────────┘ │
│ │ │
│ ┌──────▼───────┐ │
│ │ Async I/O │ │
│ │ Thread Pool │ │
│ │ (网络/磁盘) │ │
│ └──────────────┘ │
└─────────────────────────────────────────────────────┘
核心创新在于引入了独立的异步 I/O 线程池:
- Task Scheduler:主线程将 I/O 任务调度到异步线程池,然后立即返回
BLOCKED状态 - Async I/O Pool:专用线程池负责所有磁盘/网络读取操作,不阻塞计算线程
- 多策略支持:支持
WHOLE_GROUP、COLUMN_WISE_EAGER、PREFETCH_FILTERS等多种预取策略
三种预取策略详解
| 策略 | 工作模式 | 适用场景 |
|---|---|---|
WHOLE_GROUP | 一次性读取整个行组的所有列 | 全列扫描、聚合查询 |
COLUMN_WISE_EAGER | 按列预取所有需要的列 | 宽表少列查询 |
PREFETCH_FILTERS | 先取过滤列,判断行存活后再取其余列 | 高选择性过滤 |
基准测试结果
让我们使用 DuckDB 官方提供的基准测试数据进行详细分析。测试环境:AWS c6in.4xlarge 实例,从同区域 S3 读取 8.2 GB、16 个 Parquet 文件、640 个行组、17 列、6400 万行数据。
测试 1:全列扫描
-- 测试全列扫描性能
INSTALL httpfs;
LOAD httpfs;
SELECT COUNT(*) AS total_rows
FROM read_parquet('s3://bucket/large_dataset/*.parquet');
-- 全列扫描 + 过滤条件
SELECT category, AVG(price) AS avg_price
FROM read_parquet('s3://bucket/large_dataset/*.parquet')
WHERE region = 'us-east-1'
GROUP BY category
ORDER BY avg_price DESC;
| 工作负载 | main (同步) | async (16线程) | async (32线程) | 提升 |
|---|---|---|---|---|
| 全列扫描 (WHOLE_GROUP) | 10.19 s | 8.15 s | 6.08 s | 1.68× |
| 列wise 预取 (12/17列) | 9.34 s | 6.12 s | 6.34 s | 1.48× |
| 过滤列预取 (高选择性) | 7.82 s | 4.93 s | 4.51 s | 1.73× |
| 聚合查询 | 12.45 s | 8.21 s | 6.73 s | 1.85× |
关键发现
- 32 线程异步 I/O 在全列扫描场景下提升最大(1.68×),因为此时 I/O 带宽利用率最高
- 高选择性过滤场景提升最显著(1.73×),因为
PREFETCH_FILTERS策略能最大限度减少冗余 I/O - 聚合查询受益于 I/O 减少导致的 CPU 缓存效率提升
测试 2:真实数据场景复现
让我们用 DuckDB 内置功能复现一个等效的本地基准测试:
import duckdb
import pyarrow as pa
import pyarrow.parquet as pq
import numpy as np
# 生成测试数据
np.random.seed(42)
n_rows = 10_000_000
n_cols = 10
data = {
'id': range(n_rows),
'category': np.random.choice(['A','B','C','D','E'], n_rows),
'region': np.random.choice(['us-east-1','us-west-2','eu-west-1','ap-south-1'], n_rows),
'timestamp': pd.date_range('2024-01-01', periods=n_rows, freq='s'),
}
# 添加数值列
for i in range(n_cols):
data[f'value_{i}'] = np.random.randn(n_rows)
# 保存为 Parquet
df = pd.DataFrame(data)
table = pa.Table.from_pandas(df)
pq.write_to_dataset(table, '/tmp/test_dataset', partition_cols=['category', 'region'])
# 测试同步读取
con = duckdb.ATTACH (':memory:')
result_sync = con.execute("""