본문 바로가기
System Engineering/Nginx

Nginx가 Non-blocking I/O Multiplexing과 epoll 시스템 콜 이용해서 대용량 처리하는 원리

by 코딩하는 동현 2026. 1. 19.

https://ego2-1.tistory.com/27

 

C10K 문제로 살펴보는 서버 아키텍처와 커널 I/O의 진화

개요 C10K 문제란, 단일 서버가 동시에 10,000명(Concurrent 10K connections) 이상의 클라이언트와 연결을 유지하며 통신할 수 있도록 네트워크 소켓 처리 성능을 최적화하는 문제를 말한다.이 용어는 1999

ego2-1.tistory.com

 

이 글은 멀티프로세싱에서 멀티스레딩, 그리고 멀티플렉싱까지의 진화와 함께, Nginx가 Non-blocking I/O Multiplexing 및 epoll 시스템 콜을 이용해 어떻게 대용량 네트워크 트래픽을 효율적으로 처리하는지 설명한다.

1. 멀티프로세싱 기반

  1. 클라이언트가 멀티프로세싱 기반 서버에 요청을 보낸다.
  2. 부모 프로세스가 리스닝 소켓에서 accept()를 호출해 새로운 연결을 수락한다.
  3. fork()를 통해 자식 프로세스를 생성하고, 새로 생성된 소켓(리스닝 소켓이 아님)의 파일 디스크립터를 자식 프로세스에게 넘긴다.

커넥션마다 프로세스가 할당되어 fork() 및 context switching 비용이 커지는 구조이다.

2. 멀티스레딩 기반

  • 프로세스보다 생성 비용은 적지만, 스레드를 만들 때 각각 별도의 stack 공간(일반적으로 1MB)이 필요하다.
  • 단순히 1만 명이 접속할 경우 1만 개의 스레드 × 1MB = 10GB 메모리가 필요하다.
  • context switching 비용 또한 무시할 수 없다.

3. 멀티플렉싱 기반

멀티플렉싱 시스템 콜을 활용한 다중 소켓 감시

  • select, poll, epoll과 같은 system call을 통해 한 개의 스레드가 여러 소켓(File Descriptor)을 동시에 감시할 수 있다.

기존의 Blocking I/O 모델

  • 클라이언트 요청마다 read()와 같은 함수 호출이 socket 단위로 blocking된다.
  1. 커널에 데이터가 없으면 무한정 block 상태가 된다.
  2. 커널 → user space 복사까지 대기한다.

데이터 이동 경로

  • 하드웨어(NIC) → kernel space : 커널이 패킷을 커널 버퍼에 임시 저장한다.
  • kernel space → user space : Application 버퍼로 데이터 복사 시 CPU가 개입하며, 이후 스레드를 깨우게 된다.

Multiplexing I/O 모델

  • select, poll 등의 system call을 통해 여러 소켓을 동시에 감시한다.
  • 읽을 준비가 된 소켓에 대해서만 I/O 작업을 진행한다.
  • 커널이 관리하는 소켓의 데이터가 없으면 프로세스는 block 상태이며,
  • 데이터가 도착할 때 커널이 이를 감지해서 readable 상태를 알리고,
  • 프로세스는 준비된 소켓만 읽게 된다.

I/O Models

구분 Blocking Non-blocking
Synchronous (동기) (1) Legacy Blocking I/O (Apache, Java IO) (2) Non-blocking I/O (Busy Wait, 비효율)
Asynchronous (비동기) (3) I/O Multiplexing (Nginx, Node.js - epoll) (4) True Async I/O (AIO) (Windows IOCP, Linux AIO)

1) Blocking I/O (구형 Apache)

  • read() 호출 후 데이터가 유저 메모리로 모두 복사될 때까지 프로세스는 sleep 상태가 된다.
  • CPU가 놀게 되고, 그래서 Apache는 스레드를 수천 개 만들어야 했다.

2) Non-blocking I/O (Busy Wait)

  • 소켓을 O_NONBLOCK으로 만들고, read()를 호출했을 때 데이터가 없으면 즉시 EAGAIN을 반환한다.
  • 프로세스는 멈추지 않지만, 계속 무한루프로 read를 호출해 polling을 하므로 CPU가 100% 소모되어 비효율적이다. (실무에서는 단독으로 사용하지 않는다.)

3) Synchronous Non-blocking I/O Multiplexing (Nginx의 핵심)

일반적으로 "Nginx는 Asynchronous하다"라고 하지만, 엄밀히 말하자면 I/O Multiplexing(다중화) 모델이다.


Nginx 내부 동작 (커널 레벨 중심)

  1. Non-blocking 소켓 사용
    • Nginx는 모든 연결 소켓을 Non-blocking으로 만든다. (accept4(..., SOCK_NONBLOCK))
    • 그래서 Nginx 워커는 I/O 때문에 멈추지 않는다.
  2. Blocking 지점(epoll_wait)
    • Nginx도 Blocking을 한다. 단, read를 하며 기다리는 것이 아니라 "이벤트 발생 대기"를 한다.
    • epoll_wait는 Blocking 함수이나, 1만 개 소켓 중 하나라도 신호가 오길 감시한다.
  3. Sync(동기)적 처리
    • epoll이 데이터 준비 알림을 주면, 데이터 읽기(read)는 워커가 직접(Sync) 수행한다.
    • 이 순간에는 CPU 자원을 다시 소비한다.

(번외) 대용량 데이터의 경우 추천되는 I/O

Asynchronous I/O (AIO)

  • 진정한 비동기(Async)는 알림(Readiness)이 아니라 완료(Completion) 통지 방식이다.
    • Sync (epoll): "데이터 준비됐으니 네가 가져가." (Readiness)
    • Async (AIO): "메모리에 복사까지 다 해놨으니 너는 읽기만 해." (Completion)
  • 네트워크 소켓의 Non-blocking은 쉽지만, 디스크(File) I/O는 리눅스에서 오랫동안 Non-blocking이 불가능했다. 파일 읽기는 반드시 블록될 수밖에 없었다.
  • 그래서 "파일 전송"이나 "대용량 파일 쓰기" 등 특수 케이스에서는 리눅스의 AIO(Asynchronous I/O) 기능을 쓴다.
# nginx.conf

http:
  server:
    listen 80
    server_name localhost

    root /var/www/videos;

    location /video/:
      # 핵심: aio 사용
      aio on;

      # 필수: directio 미설정 시 AIO 무용지물
      directio 4m;

      # 선택: AIO 버퍼 크기 조정 (기본값은 작을 수 있음)
      output_buffers 1 128k;

 

동작 과정

  1. Nginx가 커널에 "파일을 읽어줘"라고 요청 후 즉시 리턴한다.
  2. 다른 요청을 동시에 처리한다.
  3. 커널이 디스크에서 데이터를 다 읽으면 시그널을 보낸다.
  4. 그제서야 Nginx가 데이터를 네트워크로 전송한다.

System Calls

  • select/poll: 모든 FD에게 일일이 물어봐야 해서(탐색) 규모가 커질수록 느려지며 O(n)이다.
  • epoll: 이벤트가 발생한 FD만 커널이 별도 리스트로 제공하므로 O(1)로 동작한다.

epoll man 결과

리눅스 쉘에 epoll man을 하면 system call에 대한 도큐먼트를 볼 수 있다.

1) 자료구조: Interest List vs Ready List

Interest List (관심 목록)

  • FD 등록(ADD)/수정(MOD)/삭제(DEL) 시 Red-Black Tree로 관리되어 O(logN) 속도를 보장한다.

Ready List (준비 목록)

  • 실제 I/O가 발생해서 읽을 수 있는 FD들의 목록으로, Doubly Linked List로 구현되어 있다.
  • epoll_wait는 이 리스트의 FD만 유저에게 넘기므로 활성화된 일부 소켓만 O(1)로 처리할 수 있다.

2) Edge Triggered(ET) vs Level Triggered(LT)

epoll의 Level-Triggered(LT)와 Edge-Triggered(ET)는 언제 이벤트를 알릴지를 결정하며, 고성능 서버에선 핵심이 된다.

예시: 파이프에 데이터를 쓸 때

  1. 파이프에 2KB 데이터 쓰기
  2. epoll_wait() → "rfd 준비됨" 신호
  3. 1KB만 읽고 1KB 남김
  4. 다시 epoll_wait() 호출

Level Triggered (LT, 기본 모드)

  • 데이터가 버퍼에 남아 있는 동안 계속 신호를 준다.
  • select/poll과 유사하며, nginx의 기본값이다.

Edge Triggered (ET, EPOLLET)

  • 상태 변화 시점에 한 번만 신호 주고, 이후 데이터가 남아도 추가 신호가 없다.
  • 한 번의 신호를 놓치면 남은 데이터를 읽지 못해 데드락 위험이 있다.

ET 모드의 올바른 사용법 (nginx 방식)

  1. Non-blocking 소켓을 써야 한다.
  2. read()/write()가 EAGAIN을 반환할 때까지 반복해서 읽거나 쓴다.
  3. EAGAIN이 나오면 다시 epoll_wait으로 돌아간다.
알림 방식 데이터 있을 때마다 상태 변화 시 한 번만
안전성 높음 (놓칠 일 없음) 낮음 (놓치면 데드락)
성능 보통 매우 좋음 (thundering herd 방지)
사용법 간단 Non-blocking + 반복루프 필수
nginx 기본 O (안전 우선) 필요시 ET+EPOLLONESHOT

 

반응형

댓글