io_uring을 사용해 cp보다 4배 빠른 파일 복사기 만들기
Source: Dev.to
소개
저는 Linux io_uring을 사용하여 머신러닝 데이터셋용 고성능 파일 복사기를 만들었습니다. 적절한 워크로드에서는 cp -r보다 4.2× 빠릅니다. 아래는 비동기 I/O가 도움이 되는 경우와 그렇지 않은 경우에 대한 교훈입니다.
일반적인 ML 데이터셋 크기
| 데이터셋 | 파일 수 | 일반적인 크기 |
|---|---|---|
| ImageNet | 1.28 M | 100–200 KB JPEG |
| COCO | 330 K | 50–500 KB |
| MNIST | 70 K | 784 bytes |
| CIFAR‑10 | 60 K | 3 KB |
cp -r 로 복사하면 각 파일마다 여러 시스템 콜(open, read, write, close)을 순차적으로 처리해야 하기 때문에 매우 느립니다. 100 000개의 파일을 복사하면 400 000+ 시스템 콜이 연속적으로 실행되는 셈입니다.
io_uring이 도움이 되는 이유
- 배치된 제출 – 수십 개의 작업을 큐에 넣고 하나의 시스템 콜로 제출합니다.
- 비동기 완료 – 작업이 순서와 상관없이 완료되어 CPU가 계속 작업을 수행할 수 있습니다.
- 제로‑카피 – 커널 파이프를 통해 파일 디스크립터 간에 데이터를 직접 splice하여 사용자 공간 버퍼를 사용하지 않습니다.
대신에:
open → read → write → close → repeat
우리는 이렇게 합니다:
submit 64 opens → process completions → submit reads/writes → batch everything
아키텍처 개요
┌──────────────┐ ┌─────────────────┐ ┌─────────────────────┐
│ Main Thread │────▶│ WorkQueue │────▶│ Worker Threads │
│ (scanner) │ │ (thread‑safe)│ │ (per‑thread uring) │
└──────────────┘ └─────────────────┘ └─────────────────────┘
각 파일은 상태 머신을 따라 진행됩니다:
OPENING_SRC → STATING → OPENING_DST → SPLICE_IN ⇄ SPLICE_OUT → CLOSING
핵심 설계 결정
- 워커당 동시에 64개의 파일을 처리합니다.
- 스레드별 io_uring 인스턴스(잠금 경쟁을 방지).
- 연속 디스크 접근을 위한 inode 정렬.
- 데이터 전송을 위한 splice 제로‑카피 (
source → pipe → destination). - 4 KB 정렬 할당을 갖는 버퍼 풀(
O_DIRECT와 호환).
벤치마크
NVMe (빠른 로컬 스토리지)
| 작업량 | cp -r | uring‑sync | Speedup |
|---|---|---|---|
| 100 K × 4 KB 파일 (400 MB) | 7.67 s | 5.14 s | 1.5× |
| 100 K × 100 KB 파일 (10 GB) | 22.7 s | 5.4 s | 4.2× |
클라우드 SSD (예: GCP Compute Engine)
| 작업량 | cp -r | uring‑sync | Speedup |
|---|---|---|---|
| 100 K × 4 KB 파일 | 67.7 s | 31.5 s | 2.15× |
| 100 K × 100 KB 파일 | 139.6 s | 64.7 s | 2.16× |
큰 파일일수록 빠른 스토리지에서 io_uring의 이점이 더 커집니다. 이는 CPU가 I/O를 기다리는 시간이 줄어들고 작업을 겹쳐 수행하는 시간이 늘어나기 때문입니다.
enum class FileState {
OPENING_SRC, // Opening source file
STATING, // Getting file size
OPENING_DST, // Creating destination
SPLICE_IN, // Reading into kernel pipe
SPLICE_OUT, // Writing from pipe to dest
CLOSING_SRC, // Closing source
CLOSING_DST, // Closing destination
DONE
};
완료가 상태 전환을 구동합니다: 완료가 도착하면 해당 파일 컨텍스트를 찾아 그 상태를 진행합니다.
제로‑카피와 splice
// Splice from source into pipe
io_uring_prep_splice(sqe, src_fd, offset,
pipe_write_fd, -1,
chunk_size, 0);
// Splice from pipe to destination
io_uring_prep_splice(sqe, pipe_read_fd, -1,
dst_fd, offset,
chunk_size, 0);
데이터가 사용자 공간에 절대 닿지 않으며, 커널이 파일 디스크립터 간에 페이지를 직접 이동합니다.
인오드 정렬
std::sort(files.begin(), files.end(),
[](const auto& a, const auto& b) { return a.inode
벤치마크는 로컬 NVMe 드라이브와 GCP Compute Engine VM에서 Ubuntu 24.04, 커널 6.14를 사용하여 실행되었습니다.