본문으로 건너뛰기
Folly Code Review · 85/89

folly::AsyncIO — io_uring·Linux AIO

· Hawk · 4분 읽기

한 줄 요약: 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를 제공.

  1. libaio (kernel AIO) — 2003년경 도입. O_DIRECT만 지원, 일부 fs에서 fallback to blocking.
  2. 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 경로는 다음과 같다.

  1. SQE에 op 정보를 채운다.
  2. io_uring_enter syscall을 호출한다 (SQPOLL mode면 kernel이 자동으로 poll).
  3. kernel이 SQE를 처리하고, 완료되면 CQE에 결과를 기록한다.

poll 경로는 다음과 같다.

  1. user가 CQE ring을 poll한다.
  2. 각 CQE의 user_dataOp*를 복원한다.
  3. 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::AsyncIOstd::asyncBoost.Asio
disk asyncN/Aio_uring/libaiothread per call같음 (asio도 io_uring backend 추가)
network asyncN/Afolly::AsyncSocketN/Ayes
throughputN/A매우 높음 (kernel polling)낮음높음
coroutineN/Afolly::coroN/Aasio::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에 libaio
fd = 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. 1 Folly Code Review — Meta의 production-grade C++ 라이브러리 코드 분석
  2. 2 Folly 개요 — Meta가 production에서 검증한 utility 모음 분석
  3. 3 Folly vs Abseil 철학 비교 — performance-first vs std-compatible
  4. 4 Folly 빌드와 fbcode 환경 — monorepo의 그림자
  5. 5 Folly API stability 정책 — 어떤 보장도 없다는 솔직함
  6. 6 Folly production validation 문화 — peta-scale에서 단련된 코드
  7. 7 folly::Future 분석 — std::future의 한계를 넘는 composable async
  8. 8 folly::Promise·makeFuture — Future를 만드는 두 길
  9. 9 folly::SemiFuture vs Future — executor binding의 명시화
  10. 10 folly::Future thenValue·thenError·thenTry — continuation 체인 분석
  11. 11 folly::collect·collectAll·collectAny — fan-in 패턴 분석
  12. 12 folly::Future retry·window·via — 제어 흐름 조합자
  13. 13 folly::fibers 분석 — M:N stackful coroutine
  14. 14 folly::InlineExecutor — 호출자 thread에서 즉시 실행
  15. 15 folly::CPUThreadPoolExecutor — CPU-bound 작업의 표준 thread pool
  16. 16 folly::IOThreadPoolExecutor — libevent 기반 I/O pool
  17. 17 folly::ManualExecutor — 결정적 테스트를 위한 수동 진행
  18. 18 folly::EventBase 분석 — libevent 이벤트 루프의 핵심
  19. 19 folly::IOBuf 분석 — zero-copy buffer chain의 기본 단위
  20. 20 folly::IOBufQueue — chain의 push/pull 추상화
  21. 21 folly::io::Cursor·RWCursor — chain 위의 stream
  22. 22 folly Zero-copy 패턴 — IOBuf로 ScatterGather I/O 표현
  23. 23 folly::IOBuf shared semantics — clone·unshare·takeOwnership
  24. 24 folly::FBString 분석 — SSO + COW 구현
  25. 25 folly의 fmt::format 통합 — 모던 포맷팅 채택
  26. 26 folly::StringPiece — string_view 호환 분석
  27. 27 folly Join·Split utilities — 문자열 분해와 결합
  28. 28 folly::to·tryTo — text↔num 변환 분석
  29. 29 folly Conv Customization — 사용자 타입 지원
  30. 30 folly Conv 성능 비교 — sprintf·stringstream 대비
  31. 31 folly::F14ValueMap vs std::unordered_map
  32. 32 folly::F14NodeMap — stable pointer가 필요할 때
  33. 33 folly::F14VectorMap — cache-friendly iteration
  34. 34 folly::F14FastMap — auto-select 동작
  35. 35 folly F14 internals — SIMD probing 메커니즘
  36. 36 folly::small_vector — inline storage 분석
  37. 37 folly::FixedString — compile-time string
  38. 38 folly::AtomicHashMap — lock-free read 분석
  39. 39 folly::ConcurrentHashMap — sharded 동시 해시 맵
  40. 40 folly::EvictingCacheMap — LRU 구현 분석
  41. 41 folly::Synchronized — lock wrapper 패턴
  42. 42 folly::SharedMutex 분석
  43. 43 folly::Baton — one-shot wait 동기화
  44. 44 folly::RWSpinLock 분석
  45. 45 folly::PicoSpinLock — 1-byte spinlock
  46. 46 folly::ProducerConsumerQueue — SPSC 큐 분석
  47. 47 folly::MPMCQueue — multi-producer multi-consumer
  48. 48 folly::UnboundedQueue — 동적 크기 lock-free
  49. 49 folly::fibers::Channel — Go-like channel
  50. 50 folly::dynamic — JSON-like dynamic type 분석
  51. 51 folly JSON conversion — toJson·parseJson
  52. 52 folly dynamic ↔ struct — manual marshaling
  53. 53 folly dynamic Visitor pattern — type별 분기
  54. 54 folly::Singleton vs Meyers/static — 왜 Folly의 Singleton인가
  55. 55 folly::SingletonVault 분석 — 등록·소멸·의존성
  56. 56 folly::Singleton try_get·try_get_fast — TLS-cached 접근
  57. 57 folly::ExceptionWrapper — type-erased exception holder
  58. 58 folly::ScopeGuard·SCOPE_EXIT — RAII cleanup
  59. 59 folly::Optional vs std::optional
  60. 60 folly::Function vs std::function
  61. 61 folly::Lazy — 지연 초기화 wrapper
  62. 62 folly Meta 스타일 code review 패턴
  63. 63 folly anti-patterns — 잘못 쓰면 std보다 느림
  64. 64 folly vs std 선택 기준 분석
  65. 65 folly::coro 개요 — production C++20 코루틴 어댑터
  66. 66 folly::coro::Task — lazy single-shot 코루틴
  67. 67 folly::coro::AsyncGenerator — 비동기 스트림
  68. 68 folly coro blockingWait·collectAll — 동기 경계와 fan-in
  69. 69 folly::coro::Baton·Mutex — 코루틴-aware 동기화
  70. 70 folly::Expected — 결과 또는 오류
  71. 71 folly::Try — Future 결과 wrapper
  72. 72 folly::Try vs Expected 선택 기준
  73. 73 folly::Range — 일반 iterator pair
  74. 74 folly::Uri — URL 파서
  75. 75 folly Fingerprint64·128 — 분산 hash
  76. 76 folly SpookyHashV2 — fast non-crypto hash
  77. 77 folly::Init — main() 부트스트랩
  78. 78 folly::Indestructible — global lifetime 패턴
  79. 79 folly::MicroLock — 1-byte 락
  80. 80 folly::MicroSpinLock — 가장 좁은 spin lock
  81. 81 folly::format — legacy formatter 분석
  82. 82 folly::demangle — typeid 디망글링
  83. 83 folly::DynamicConverter — dynamic ↔ struct
  84. 84 folly::RecordIO — append-only 로그 파일 포맷
  85. 85 folly::io::Compression — zstd·lz4·snappy wrapper
  86. 86 folly::AsyncIO — io_uring·Linux AIO
  87. 87 folly::CancellationToken — 코루틴·Future 취소 전파
  88. 88 folly::observer — hot config의 atomic refresh
  89. 89 fbcode 패턴 모음 — folly 사용의 실전