folly::AsyncIO — io_uring·Linux AIO
한 줄 요약:
folly::AsyncIO는 Linux의 io_uring(우선) 또는 libaio(폴백)을 wrap한 async disk I/O 인터페이스다. thread-per-IO 모델 없이 수만 동시 I/O를 다룬다.
#동기
전통적인 disk I/O는 blocking — read/write가 thread를 잠근다. 한 번에 수천 파일을 다루려면 thread를 수천 개 만들거나, 별도 threadpool로 던지거나, epoll+nonblock(disk엔 적용 안 됨)을 흉내내야 한다.
Linux는 두 가지 kernel async I/O를 제공.
- libaio (kernel AIO) — 2003년경 도입. O_DIRECT만 지원, 일부 fs에서 fallback to blocking.
- io_uring — 2019년 5.1+. unified sqe/cqe ring, O_DIRECT 불필요, regular file 지원, network/disk 모두.
folly::AsyncIO는 두 backend를 같은 인터페이스로 노출한다.
folly::IoUringBackend backend(folly::IoUringBackend::Options{}.setCapacity(4096));
folly::AsyncIO::Op op;op.pread(fd, buffer, length, offset);op.setNotificationCallback([](folly::AsyncIO::Op* o) { std::cout << "done, bytes: " << o->result() << "\n";});backend.submit(&op);
backend.pollCompleted(); // 또는 EventBase에서 자동#API
#include <folly/experimental/io/AsyncIO.h>
class AsyncIO { public: struct Op { void pread(int fd, void* buf, size_t count, off_t offset); void pwrite(int fd, const void* buf, size_t count, off_t offset); void preadv(int fd, const iovec* iov, int iovcnt, off_t offset); void pwritev(int fd, const iovec* iov, int iovcnt, off_t offset); void fsync(int fd); void fdatasync(int fd); ssize_t result() const; // 완료 후 값 };
void submit(Op* op); void pollCompleted(); Range<Op**> wait(size_t minRequests);};Op가 한 I/O 요청. submit 후 pollCompleted 또는 wait로 결과 수확.
#Coroutine 통합
folly::coro::Task<size_t> ReadAsync(folly::IoUringBackend& backend, int fd, void* buf, size_t len, off_t off) { folly::AsyncIO::Op op; op.pread(fd, buf, len, off);
folly::coro::Baton done; op.setNotificationCallback([&](auto*) { done.post(); });
backend.submit(&op); co_await done;
if (op.result() < 0) throw std::system_error(-op.result(), std::system_category()); co_return op.result();}Baton으로 코루틴이 I/O 완료를 await. folly::coro::IoUringExecutor 같은 wrapper가 이 패턴을 자동화.
#내부 — io_uring 동작
io_uring은 링 두 쌍으로 동작한다. SQE(Submission Queue Entry) ring은 user 영역이 채우고 kernel이 poll하며, CQE(Completion Queue Entry) ring은 kernel이 채우고 user가 poll한다.
submit 경로는 다음과 같다.
- SQE에 op 정보를 채운다.
io_uring_entersyscall을 호출한다 (SQPOLL mode면 kernel이 자동으로 poll).- kernel이 SQE를 처리하고, 완료되면 CQE에 결과를 기록한다.
poll 경로는 다음과 같다.
- user가 CQE ring을 poll한다.
- 각 CQE의
user_data로Op*를 복원한다. - callback을 호출한다.
ring 한 쌍이 batch 제출과 batch 수확을 가능하게 한다. syscall 한 번에 수십 op 처리. context switch 비용이 결정적으로 줄어든다.
// folly/experimental/io/IoUring.cpp 약식void IoUringBackend::submit(Op* op) { io_uring_sqe* sqe = io_uring_get_sqe(&ring_); if (!sqe) { io_uring_submit(&ring_); // ring 가득 → flush sqe = io_uring_get_sqe(&ring_); } setupSqe(sqe, op); // pread / pwrite / fsync 등 io_uring_sqe_set_data(sqe, op); pending_.insert(op);}
void IoUringBackend::pollCompleted() { io_uring_cqe* cqe; while (io_uring_peek_cqe(&ring_, &cqe) == 0) { auto* op = static_cast<Op*>(io_uring_cqe_get_data(cqe)); op->result_ = cqe->res; io_uring_cqe_seen(&ring_, cqe); op->complete(); // callback }}#libaio fallback
io_uring이 없는 (5.1 미만) kernel에서는 libaio.
// folly/experimental/io/Aio.cpp 약식class AioBackend { void submit(Op* op) { io_event ev; iocb* cb = &op->cb_; io_prep_pread(cb, op->fd_, op->buf_, op->len_, op->offset_); io_submit(ctx_, 1, &cb); }
void pollCompleted() { io_event events[64]; int n = io_getevents(ctx_, 0, 64, events, nullptr); for (int i = 0; i < n; ++i) { auto* op = static_cast<Op*>(events[i].data); op->result_ = events[i].res; op->complete(); } }};libaio는 O_DIRECT 강제, regular fs에서 fallback to blocking이 있어 모든 I/O가 진짜 async 아닐 수 있음. io_uring이 압도적 우위.
#사용 패턴 — high-throughput read
folly::IoUringBackend backend(folly::IoUringBackend::Options{} .setCapacity(8192));
std::vector<folly::AsyncIO::Op> ops(1000);std::vector<std::vector<char>> bufs(1000, std::vector<char>(4096));
for (size_t i = 0; i < 1000; ++i) { ops[i].pread(fd, bufs[i].data(), 4096, i * 4096); backend.submit(&ops[i]);}
size_t completed = 0;while (completed < 1000) { auto done = backend.wait(/*min=*/1); for (auto* op : done) { ++completed; // process result }}1000개 4KB read를 한 번에 제출. io_uring이라면 4-5 syscall로 완료. thread-per-IO 모델은 1000 thread 필요.
#std와의 비교
| 항목 | 표준 (없음) | folly::AsyncIO | std::async | Boost.Asio |
|---|---|---|---|---|
| disk async | N/A | io_uring/libaio | thread per call | 같음 (asio도 io_uring backend 추가) |
| network async | N/A | folly::AsyncSocket | N/A | yes |
| throughput | N/A | 매우 높음 (kernel polling) | 낮음 | 높음 |
| coroutine | N/A | folly::coro | N/A | asio::awaitable |
std::async는 disk I/O 해법이 아니다. thread를 spawn해 blocking read하는 정도. folly::AsyncIO는 진짜 kernel async.
io_uring backend는 Boost.Asio도 추가했다 — 같은 kernel feature를 다른 wrapper가 채택.
#코드 리뷰 포인트
- io_uring 사용 여부가 kernel version에 좌우 → runtime check 후 fallback.
- ring capacity가 부족하면 submit이 spin/block. capacity tuning.
- buffer가 op 수명보다 짧으면 UAF — buffer 보장.
- syscall mode (
io_uring_enter직접 vs SQPOLL) — SQPOLL은 kernel thread를 점유. high-throughput 환경에서만. - coroutine wrap이 없으면 callback boilerplate.
folly::coro::IoUringExecutor또는 직접 Baton wrap.
#자주 보는 안티패턴
// 1. op buffer가 stack에 있는데 callback이 늦게{ folly::AsyncIO::Op op; char buf[4096]; op.pread(fd, buf, 4096, 0); backend.submit(&op);} // op과 buf 모두 destroy — kernel은 아직 작업 중// → heap에 두거나 callback이 완료될 때까지 살아있어야
// 2. fsync를 매 write마다for (auto& blk : blocks) { op.pwrite(fd, blk.data(), blk.size(), off); backend.submit(&op); op.fsync(fd); // 매 write마다 fsync — throughput 박살}// → batch 후 한 번 fsync
// 3. O_DIRECT 없는 fd에 libaiofd = open(path, O_RDONLY); // O_DIRECT 아님AioBackend backend;// → libaio가 blocking으로 fallback. io_uring 사용 또는 O_DIRECT.
// 4. pollCompleted를 한 thread에서, submit을 다른 thread에서// → io_uring ring은 multi-threaded SQ submit 위험. SQ lock 또는 single submitter.#정리
folly::AsyncIO는 io_uring/libaio를 통합한 async disk I/O wrapper.- io_uring이 우선 — kernel 5.1+에서 사용.
- batch 제출/수확으로 syscall 비용 분산.
- coroutine wrap으로 callback boilerplate 제거 가능.
- thread-per-IO 모델 대비 throughput·resource 효율 결정적.
#다음 편
Part 20-04: CancellationToken에서 코루틴/Future 취소 전파를 본다.
#관련 항목
Folly Code Review · 86 of 89
- 1 Folly Code Review — Meta의 production-grade C++ 라이브러리 코드 분석
- 2 Folly 개요 — Meta가 production에서 검증한 utility 모음 분석
- 3 Folly vs Abseil 철학 비교 — performance-first vs std-compatible
- 4 Folly 빌드와 fbcode 환경 — monorepo의 그림자
- 5 Folly API stability 정책 — 어떤 보장도 없다는 솔직함
- 6 Folly production validation 문화 — peta-scale에서 단련된 코드
- 7 folly::Future 분석 — std::future의 한계를 넘는 composable async
- 8 folly::Promise·makeFuture — Future를 만드는 두 길
- 9 folly::SemiFuture vs Future — executor binding의 명시화
- 10 folly::Future thenValue·thenError·thenTry — continuation 체인 분석
- 11 folly::collect·collectAll·collectAny — fan-in 패턴 분석
- 12 folly::Future retry·window·via — 제어 흐름 조합자
- 13 folly::fibers 분석 — M:N stackful coroutine
- 14 folly::InlineExecutor — 호출자 thread에서 즉시 실행
- 15 folly::CPUThreadPoolExecutor — CPU-bound 작업의 표준 thread pool
- 16 folly::IOThreadPoolExecutor — libevent 기반 I/O pool
- 17 folly::ManualExecutor — 결정적 테스트를 위한 수동 진행
- 18 folly::EventBase 분석 — libevent 이벤트 루프의 핵심
- 19 folly::IOBuf 분석 — zero-copy buffer chain의 기본 단위
- 20 folly::IOBufQueue — chain의 push/pull 추상화
- 21 folly::io::Cursor·RWCursor — chain 위의 stream
- 22 folly Zero-copy 패턴 — IOBuf로 ScatterGather I/O 표현
- 23 folly::IOBuf shared semantics — clone·unshare·takeOwnership
- 24 folly::FBString 분석 — SSO + COW 구현
- 25 folly의 fmt::format 통합 — 모던 포맷팅 채택
- 26 folly::StringPiece — string_view 호환 분석
- 27 folly Join·Split utilities — 문자열 분해와 결합
- 28 folly::to·tryTo — text↔num 변환 분석
- 29 folly Conv Customization — 사용자 타입 지원
- 30 folly Conv 성능 비교 — sprintf·stringstream 대비
- 31 folly::F14ValueMap vs std::unordered_map
- 32 folly::F14NodeMap — stable pointer가 필요할 때
- 33 folly::F14VectorMap — cache-friendly iteration
- 34 folly::F14FastMap — auto-select 동작
- 35 folly F14 internals — SIMD probing 메커니즘
- 36 folly::small_vector — inline storage 분석
- 37 folly::FixedString — compile-time string
- 38 folly::AtomicHashMap — lock-free read 분석
- 39 folly::ConcurrentHashMap — sharded 동시 해시 맵
- 40 folly::EvictingCacheMap — LRU 구현 분석
- 41 folly::Synchronized — lock wrapper 패턴
- 42 folly::SharedMutex 분석
- 43 folly::Baton — one-shot wait 동기화
- 44 folly::RWSpinLock 분석
- 45 folly::PicoSpinLock — 1-byte spinlock
- 46 folly::ProducerConsumerQueue — SPSC 큐 분석
- 47 folly::MPMCQueue — multi-producer multi-consumer
- 48 folly::UnboundedQueue — 동적 크기 lock-free
- 49 folly::fibers::Channel — Go-like channel
- 50 folly::dynamic — JSON-like dynamic type 분석
- 51 folly JSON conversion — toJson·parseJson
- 52 folly dynamic ↔ struct — manual marshaling
- 53 folly dynamic Visitor pattern — type별 분기
- 54 folly::Singleton vs Meyers/static — 왜 Folly의 Singleton인가
- 55 folly::SingletonVault 분석 — 등록·소멸·의존성
- 56 folly::Singleton try_get·try_get_fast — TLS-cached 접근
- 57 folly::ExceptionWrapper — type-erased exception holder
- 58 folly::ScopeGuard·SCOPE_EXIT — RAII cleanup
- 59 folly::Optional vs std::optional
- 60 folly::Function vs std::function
- 61 folly::Lazy — 지연 초기화 wrapper
- 62 folly Meta 스타일 code review 패턴
- 63 folly anti-patterns — 잘못 쓰면 std보다 느림
- 64 folly vs std 선택 기준 분석
- 65 folly::coro 개요 — production C++20 코루틴 어댑터
- 66 folly::coro::Task — lazy single-shot 코루틴
- 67 folly::coro::AsyncGenerator — 비동기 스트림
- 68 folly coro blockingWait·collectAll — 동기 경계와 fan-in
- 69 folly::coro::Baton·Mutex — 코루틴-aware 동기화
- 70 folly::Expected — 결과 또는 오류
- 71 folly::Try — Future 결과 wrapper
- 72 folly::Try vs Expected 선택 기준
- 73 folly::Range — 일반 iterator pair
- 74 folly::Uri — URL 파서
- 75 folly Fingerprint64·128 — 분산 hash
- 76 folly SpookyHashV2 — fast non-crypto hash
- 77 folly::Init — main() 부트스트랩
- 78 folly::Indestructible — global lifetime 패턴
- 79 folly::MicroLock — 1-byte 락
- 80 folly::MicroSpinLock — 가장 좁은 spin lock
- 81 folly::format — legacy formatter 분석
- 82 folly::demangle — typeid 디망글링
- 83 folly::DynamicConverter — dynamic ↔ struct
- 84 folly::RecordIO — append-only 로그 파일 포맷
- 85 folly::io::Compression — zstd·lz4·snappy wrapper
- 86 folly::AsyncIO — io_uring·Linux AIO
- 87 folly::CancellationToken — 코루틴·Future 취소 전파
- 88 folly::observer — hot config의 atomic refresh
- 89 fbcode 패턴 모음 — folly 사용의 실전
관련 글
fbcode 패턴 모음 — folly 사용의 실전
Meta fbcode 코드 리뷰에서 반복적으로 등장하는 folly 사용 패턴 — overview + 시리즈 마무리.
같은 시리즈에서 이어 읽기
folly::observer — hot config의 atomic refresh
folly::observer — read mostly 값의 atomic refresh, hot config·feature flag·LB weight 같은 패턴의 표준.
같은 시리즈에서 이어 읽기
folly::CancellationToken — 코루틴·Future 취소 전파
CancellationSource/Token의 전파 모델 — coroutine·Future·callback 트리에서 협력적 취소.
같은 시리즈에서 이어 읽기