Featured image of post DuckDB 异步 I/O 引擎:Parquet 读取性能提升 1.7 倍的秘密

DuckDB 异步 I/O 引擎:Parquet 读取性能提升 1.7 倍的秘密

深度解析 DuckDB 最新异步 I/O 引擎如何为 Parquet 查询带来 1.5-1.7 倍性能飞跃,包含真实基准测试数据和优化实战建议。

DuckDB 异步 I/O 引擎:Parquet 读取性能提升 1.7 倍的秘密

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

DuckDB 异步 I/O 引擎架构

为什么 I/O 会成为瓶颈?

在现代数据分析中,Parquet 几乎已成为事实上的列式存储格式。它的压缩效率高、支持谓词下推(predicate pushdown)、非常适合大规模数据分析。但当数据量达到几十 GB 甚至上 TB 时,传统的同步 I/O 模式会遇到一个隐蔽的性能瓶颈:

CPU 在等待磁盘或网络 I/O 时处于空闲状态,而磁盘/网络却在等待 CPU 发出读取指令。

这种"等待-执行-等待"的模式,在云环境(S3、GCS、OSS)中尤为严重——因为网络延迟远高于本地 SSD 的访问延迟。

DuckDB 异步 I/O 引擎的工作原理

同步 I/O 的局限

在 DuckDB 的旧有架构中,Parquet 读取器以同步方式工作:

  1. 主线程请求读取 Parquet 文件
  2. 主线程等待数据从磁盘/网络到达
  3. 数据到达后,主线程解析并过滤
  4. 重复以上步骤

这意味着每次 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_GROUPCOLUMN_WISE_EAGERPREFETCH_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 s8.15 s6.08 s1.68×
列wise 预取 (12/17列)9.34 s6.12 s6.34 s1.48×
过滤列预取 (高选择性)7.82 s4.93 s4.51 s1.73×
聚合查询12.45 s8.21 s6.73 s1.85×

关键发现

  1. 32 线程异步 I/O 在全列扫描场景下提升最大(1.68×),因为此时 I/O 带宽利用率最高
  2. 高选择性过滤场景提升最显著(1.73×),因为 PREFETCH_FILTERS 策略能最大限度减少冗余 I/O
  3. 聚合查询受益于 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("""

📺 Watch video tutorials → Olap Studio YouTube

Subscribe for more DuckDB & AI automation tutorials

使用 Hugo 构建
主题 StackJimmy 设计

⚠️ 本站为独立社区项目,与 DuckDB 基金会及 DuckDB 官方项目无任何从属、背书或赞助关系。

"DuckDB" 是 DuckDB 基金会的注册商标,本站仅以事实描述方式使用该名称。

本站内容仅供教育与社区推广用途,不构成任何商业服务。