Linux 비동기 I/O: Epoll vs. io_uring

io_uring은 Linux에서 비동기 I/O를 위한 최신 표준으로, 완료 기반 모델을 제공하여 대량 네트워크 트래픽을 처리하는 데 필요한 시스템 콜 수를 크게 줄여줍니다. 수십 년 동안 업계 표준이었던 epoll은 준비 알림과 관련된 중복 시스템 콜을 발생시키지만, io_uring은 이러한 중복을 없애고 작업을 배치하여 더 높은 처리량을 달성할 수 있게 합니다.

준비 모델: epoll

Epoll은 준비 기반 시스템입니다. 파일 디스크립터가 I/O 작업을 준비(ready)했을 때 애플리케이션에 알리지만, 실제 작업을 수행하지는 않습니다. 이로 인해 이벤트당 필요한 시스템 콜 수가 많아져 성능 병목이 발생합니다:

  • 등록: 소켓을 등록하기 위한 epoll_ctl 한 번 호출.
  • 알림: 어떤 소켓이 준비되었는지 확인하기 위한 epoll_wait 호출.
  • 실행: 실제 데이터 이동을 위한 read() 또는 write() 별도 호출.

각 시스템 콜이 사용자 모드와 커널 모드 사이의 컨텍스트 스위치를 트리거하기 때문에, 수천 개의 동시 연결을 처리할 때 오버헤드가 크게 증가합니다. 이는 단순 epoll 기반 프록시가 Nginx나 HAProxy와 같은 고도로 최적화된 도구보다 성능이 뒤처지는 근본적인 구조적 한계입니다.

완료 모델: io_uring

Linux 커널 v5.1(2019)에서 도입된 io_uring은 구조를 준비 기반에서 완료 기반으로 전환합니다. 애플리케이션에게 읽을 수 있다는 것을 알려주는 대신, 읽기가 완료되었을 때 알려줍니다.

작동 방식

io_uring은 애플리케이션과 커널 사이에 두 개의 링 버퍼를 공유합니다:

  1. Submission Queue (SQ): 애플리케이션이 I/O 요청을 여기에 넣습니다.
  2. Completion Queue (CQ): 커널이 완료된 작업의 결과를 여기에 넣습니다.

이 공유 메모리 구조를 통해 애플리케이션은 여러 I/O 작업을 한 번에 제출하고, 단일 io_uring_enter() 시스템 콜로 여러 완료를 수집할 수 있습니다. 경우에 따라 오버헤드를 거의 0에 가깝게 줄일 수 있습니다.

고급 성능 기능

  • SQPOLL (Submission Queue Polling): IORING_SETUP_SQPOLL을 사용하면 전용 커널 스레드가 제출 큐를 폴링하도록 생성됩니다. 이는 정상 상태에서 애플리케이션이 io_uring_enter()를 호출할 필요를 없애지만, 큐가 비어 있어도 스레드가 계속 회전하기 때문에 CPU 사용량이 증가합니다.
  • Zero-Copy I/O: io_uring_register_buffers()를 사용하면 매 작업마다 커널이 메모리를 다시 매핑하는 것을 방지할 수 있습니다. 네트워크 전송의 경우, 커널 6.0+에서 제공되는 IORING_OP_SEND_ZC를 사용하면 버퍼를 커널 공간으로 복사하는 과정을 완전히 생략할 수 있습니다.

비교 요약

Feature epoll io_uring
Model 준비 기반 (가능할 때 알림) 완료 기반 (완료될 때 알림)
Syscall Overhead 높음 (이벤트당 2+ 시스템 콜) 낮음 (배치당 1 시스템 콜 또는 SQPOLL 사용 시 0)
Kernel Version 레거시 (2002년부터) 최신 (v5.1+, 2019)
Complexity 비교적 단순 높음 (링 버퍼 관리 필요)

구현 시 고려 사항 및 트레이드오프

io_uring은 뛰어난 성능을 제공하지만, 특정 엔지니어링 과제와 보안 이슈를 동반합니다:

보안 및 안정성

일부 환경에서는 보안 위험 때문에 io_uring을 기본적으로 비활성화합니다. 커널과 사용자 영역 사이에 직접 메모리를 공유하기 때문에 여러 익스플로잇의 표적이 되어 왔습니다. 이러한 이유로 Go와 같은 고성능 런타임은 기본적으로 io_uring을 사용하지 않습니다.

오류 처리

동기식 시스템 콜은 즉시 오류를 반환하지만, io_uring 오류는 비동기적으로 반환됩니다. 오류는 Completion Queue Entry (CQE)의 res 필드에 포함되어 있어, 오류 관리에 다른 접근 방식이 필요합니다.

추가 성능 최적화

io_uring의 한계를 넘어 성능을 끌어올리려면 다음을 고려할 수 있습니다:

  • CPU Pinning: 스레드와 리슨 소켓(SO_INCOMING_CPU)을 특정 코어에 고정하여 CPU 간 통신을 최소화합니다.
  • Memory Alignment: mimalloc이나 concurrencykit 같은 특수 할당자를 사용해 메모리 정렬된 버퍼를 확보합니다.
  • Kernel Bypass: 극한 상황에서는 DPDK(Data Plane Development Kit)를 사용해 커널을 완전히 우회할 수 있지만, 구현 복잡도가 크게 증가합니다.

SUMMARY

io_uring은 완료 기반 비동기 I/O 모델을 제공하여 준비 기반 epoll 시스템에 비해 시스템 콜 오버헤드를 크게 줄이며, 커널 5.1+ 환경에서 고성능 Linux 네트워킹에 선호되는 선택이 됩니다.

TITLE

Linux 비동기 I/O: Epoll vs. io_uring

Sources