작년에 PostgreSQL 18의 비동기 I/O를 다루면서, DB 엔진들이 하나둘 동기 I/O와 결별하는 중이라고 썼어요. 이번엔 DuckDB 차례입니다. 7월 31일 공식 블로그 글이 가을 출시 예정인 v2.0의 비동기 I/O 구조와 벤치마크를 공개했는데, 숫자가 눈에 띄어서 정리합니다.
문제: 대역폭이 아니라 대기가 병목
DuckDB의 기존 실행 모델은 CPU 스레드당 워커 하나입니다. 로컬 NVMe에서는 이걸로 충분합니다. 읽기 지연이 짧아서 워커가 I/O를 기다리는 시간이 얼마 안 되기 때문입니다.
S3 같은 오브젝트 스토리지에서는 사정이 다릅니다. HTTP 요청 하나의 지연이 수십 ms 단위라, 동기 방식으로는 워커가 요청을 보내고 응답을 기다리며 잠들어 있는 시간이 압도적으로 깁니다. 결과적으로 동시 요청 수가 부족해서 네트워크 대역폭을 채우지 못합니다. 실측에서 기존 버전(v1.5.5)은 25 Gbit/s를 쓸 수 있는 인스턴스에서 5 Gbit/s밖에 못 썼습니다. 인프라는 놀고 쿼리는 느린, 돈이 새는 구간입니다.
구조: 워커 풀과 I/O 풀의 분리
v2.0은 스레드 풀을 둘로 나눕니다.
| 풀 | 크기 | 역할 |
|---|---|---|
| REGULAR | CPU 스레드당 1개 (기본) | 디코딩, 조인, 집계 등 실제 연산 |
| ASYNC | 시스템 스레드의 4배, 최대 256개 (기본) | 블로킹 I/O 전담 |
ASYNC 풀을 CPU 수보다 훨씬 크게 잡을 수 있는 이유는 이 스레드들이 대부분의 시간을 HTTP 응답 대기로 보내기 때문입니다. CPU를 거의 안 쓰니 수백 개를 띄워도 부담이 없고, 그만큼 동시 요청 수가 올라가 네트워크 대역폭이 채워집니다.
여기에 read-ahead가 얹힙니다. 정규 워커가 데이터를 소비하는 속도보다 앞서서 fetch 작업을 큐에 넣어 두는 방식입니다. 작업 단위는 Parquet이면 row group, CSV면 고정 바이트 범위입니다.
flowchart TD
A[정규 워커
연산 진행] --> B[read-ahead 큐에
fetch 작업 등록]
B --> C[ASYNC 스레드들이
S3에 동시 요청]
C --> D[미리 받아둔 버퍼]
D --> A
미리 받아 두는 만큼 메모리를 먹으니 제어 장치도 있습니다. read_ahead_depth 설정이 기본 -1(무제한, 메모리 예산으로만 제한)이고, 양수로 제한하거나 0으로 끌 수 있습니다. 메모리가 부족해지면 임시 메모리 관리자와 협상해 큐 크기를 스스로 줄입니다.
숫자: Parquet 3배, CSV 19배
공개된 벤치마크는 EC2 r7i.16xlarge에서 S3의 TPC-H SF100 데이터를 읽는 구성입니다.
| 워크로드 | v1.5.5 | v2.0.0-dev | 배율 |
|---|---|---|---|
| S3 Parquet 읽기 | 8.230초 | 2.844초 | 2.9배 |
| S3 CSV 읽기 (80.89GB) | 877.563초 | 45.264초 | 19.4배 |
| 로컬 디스크 (M4 MacBook Pro) | 1.321초 | 0.883초 | 1.5배 |
CSV의 19배가 특히 극적인데, 뒤집어 보면 기존 CSV 리더가 원격 스토리지에서 그만큼 직렬화되어 있었다는 뜻입니다. 네트워크 사용률은 5 Gbit/s에서 25 Gbit/s 포화로 올라갔습니다.
동시성 수치도 흥미롭습니다. 쿼리 4개를 동시에 돌렸을 때 v1.5.5는 평균 5.9코어(활용률 6%)를 쓰며 35.8초가 걸렸고, v2.0.0-dev는 48.1코어(75%)를 쓰며 15.6초에 끝냈습니다. I/O 대기에 묶여 있던 CPU가 풀려난 그림입니다.
제한도 명시되어 있습니다. 현재 구현된 것은 Parquet과 비압축 UTF-8 CSV뿐이고, JSON과 DuckDB 네이티브 포맷은 추후 예정입니다. row group이 거대해서 파일 안 병렬성이 부족한 경우에는 효과가 줄어듭니다.
PostgreSQL 18과 나란히 놓고 보면
같은 문제를 두 엔진이 어떻게 다르게 푸는지 비교하면 각자의 처지가 보입니다.
| PostgreSQL 18 | DuckDB v2.0 | |
|---|---|---|
| 대상 I/O | 로컬 디스크 (heap 읽기) | 원격 오브젝트 스토리지 |
| 방식 | io_method (worker / io_uring) | ASYNC 스레드 풀 + read-ahead |
| 단위 | 블록 | row group / 바이트 범위 |
| 배경 | 17년 만의 아키텍처 전환 | 분석 엔진의 클라우드 이행 |
PostgreSQL은 커널 인터페이스(io_uring)까지 내려가 로컬 블록 I/O를 비동기화했고, DuckDB는 HTTP 위의 원격 읽기를 스레드 물량으로 병렬화했습니다. 방식은 달라도 결론은 같습니다. 스토리지가 어디에 있든, 워커가 I/O를 기다리며 잠드는 구조는 더 이상 용납되지 않는다는 것입니다.
같은 글에서 DuckDB는 v2.0과 함께 가는 방향도 살짝 내비쳤습니다. 8월 5일 40,000 스타 기념 글에서는 다중 동시 쓰기를 지원하는 원격 프로토콜 Quack까지 언급했는데, 임베디드 분석 엔진이라는 출발점에서 점점 멀어지는 중입니다. 오브젝트 스토리지 위의 분석 스택에서 DuckDB의 자리가 어디까지 커질지, v2.0이 나오면 직접 실행해 보고 후속으로 다루겠습니다.
참고 자료
- Asynchronous I/O in DuckDB: Work, Thread, Work - DuckDB Blog, 2026-07-31
- Thank You for 40 000 Stars on GitHub - DuckDB Blog, 2026-08-05