Nginx는 고성능, 고가용성을 제공하는 웹 서버이자 리버스 프록시 서버로, 그 효율성과 확장성 때문에 널리 사용됩니다. Nginx는 로드밸런싱, 리버스 프록시, 레이턴시 감소(캐싱, 암호화, 압축) 등 다양한 기능을 제공하는 효율적인 도구이지만 오늘은 Nginx의 핵심인 이벤트 처리 방식에 대해 좀 더 자세히 알아보려고합니다.

Nginx 아키텍처 개요

Nginx의 요청 처리 방식에 대해 자세히 알아보기 이전에 Nginx의 전체 구조를 간단하게 살펴보겠습니다.

img

Nginx는 하나의 마스터 프로세스와 여러 작업자 프로세스가 있습니다. 또한 캐시 로더와 캐시 매니저와 같은 몇 가지 특수 목적 프로세스도 있습니다. nginx 1.x 버전에서는 모든 프로세스가 단일 스레드이며 모든 프로세스는 주로 프로세스 간 통신을 위해 공유 메모리 메커니즘을 사용합니다. 마스터 프로세스는 루트 사용자로 실행되고 캐시 로더, 캐시 관리자 및 워커는 권한 없는 사용자로 실행됩니다.

Master Process

Master Process는 다음과 같은 일을 담당합니다.

  • 구성 읽기 및 유효성 검사
  • 소켓 생성, 바인딩 및 닫기
  • 구성된 워커 프로세스 수 시작, 종료 및 유지 관리
  • 서비스 중단 없이 재구성
  • 논스톱 바이너리 업그레이드 제어(새 바이너리 시작 및 필요한 경우 롤백)
  • 로그 파일 다시 열기
  • 임베디드 Perl 스크립트 컴파일

Worker Process

워커 프로세스는 클라이언트의 연결을 수락, 처리 및 처리하고, 역방향 프록시 및 필터링 기능을 제공하며, nginx가 할 수 있는 거의 모든 작업을 수행합니다. nginx 인스턴스의 동작 모니터링과 관련하여 시스템 관리자는 웹 서버의 실제 일상적인 작업을 반영하는 프로세스라는 점에서 워커를 주시해야 합니다.

Cache Loader Process & Cache Manager

캐시 로더 프로세스는 온디스크 캐시 항목을 확인하고 nginx의 인메모리 데이터베이스를 캐시 메타데이터로 채우는 작업을 담당합니다. 기본적으로 캐시 로더는 특별히 할당된 디렉토리 구조에 이미 디스크에 저장된 파일로 작업할 수 있도록 nginx 인스턴스를 준비합니다. 디렉터리를 탐색하고, 캐시 콘텐츠 메타데이터를 확인하고, 공유 메모리의 관련 항목을 업데이트한 다음 모든 것이 깨끗하고 사용할 준비가 되면 종료합니다.

캐시 관리자는 캐시 만료 및 무효화를 주로 담당합니다. 정상적인 nginx 작동 중에는 메모리에 남아 있다가 장애가 발생하면 마스터 프로세스에 의해 다시 시작됩니다.

Nginx의 요청 처리 방식

img

Nginx는 Event-Driven 방식으로 동작합니다. 한 개 또는 고정된 프로세스만 생성하고, 그 프로세스 내부에서 비동기 방식으로 효율적으로 작업을 처리합니다. Event-Driven 방식으로 Reactor pattern을 사용하는데 Reactor는 이벤트 루프(event loop)로 구현되며, epoll (리눅스에서), kqueue (BSD 계열에서), event ports (Solaris에서)와 같은 현대적인 이벤트 알림 시스템을 사용하여 실행됩니다. Nginx가 요청을 처리하는 방식 좀 더 구체적으로 살펴보기 위해 Linux 2.6+ 이상에서 사용 가능한 epoll을 통한 이벤트 처리방식을 살펴보겠습니다.

  1. Nginx 실행
  2. Master Process 생성됨
  3. Master Process가 설정파일 읽음
  4. Master Process가 Listen Socket 생성
  5. Master Process가 Worker Process들, 캐시 프로세서, 캐시 매니저 등 생성
    • 더 정확히는, 마스터 프로세스가 리슨 소켓을 생성하고, 이후 워커 프로세스들이 생성될 때 해당 소켓을 상속받습니다. SO_REUSEPORT 옵션이 활성화된 경우, 각 워커 프로세스는 자체적으로 리슨 소켓을 열 수도 있습니다. 옵션이 활성화되지 않은 경우(default), 마스터 프로세스에 의해 생성된 단일 리슨 소켓을 모든 워커 프로세스가 공유하게 되며, 운영 체제의 스케줄링 메커니즘에 따라 새로운 연결이 특정 워커에게 할당됩니다.
  6. Worker Process가 epoll_create 시스템 콜으로 epoll instance(table) 생성
    • 이 인스턴스는 이벤트를 모니터링할 파일 디스크립터들의 집합을 관리합니다.
  7. Worker Process가 epoll_ctl 시스템 콜을 사용하여 Listen Socket 파일 디스크립터를 epoll instance에 등록함(SO_REUSEPORT가 활성화되지 않은 경우)
  8. Listen Socket에 새로운 요청이 들어오면 Worker Process에 배정이 되고, Worker Process가 accept 시스템콜로 새로운 연결 소켓 생성
  9. Worker Process가 생성된 연결 소켓의 파일 디스크립터를 epoll instance에 등록
  10. Worker Process는 epoll_wait 호출을 통해 epoll instance에 등록된 파일 디스크립터 중에서 I/O 준비가 완료된 것들을 확인하고 해당 이벤트를 처리
    • 이벤트가 발생하면 즉시 처리를 시작하지만, 처리 과정 자체가 워커 프로세스를 블록하지 않습니다.
    • 처리 과정이 워커 프로세스를 블록하지 않고 실행되는 예시를 살펴봅시다. 워커 프로세스가 epoll_wait을 통해 파일 디스크립터1에 대한 읽기 가능(EPOLLIN) 이벤트를 감지합니다. 워커 프로세스는 해당 파일 디스크립터1로부터 데이터를 읽기 시작합니다. 이 때, 읽기 작업은 비동기적으로 수행됩니다. 예를 들어, Nginx는 넌블로킹 소켓을 사용하며, read()나 recv() 호출이 즉시 반환되고, 실제 데이터가 준비될 때까지 다른 작업을 계속 수행할 수 있습니다. 데이터 읽기가 완료되면, 이에 대응하는 콜백 함수나 이벤트 핸들러가 실행됩니다. 이 핸들러는 읽은 데이터를 처리하고, 필요한 경우 응답을 생성하여 클라이언트에게 전송합니다. 이 과정 역시 비동기적으로 진행됩니다. 데이터 처리가 완료되면, 워커 프로세스는 epoll_wait에 다시 진입하여 더 이상 처리할 이벤트가 있는지 확인합니다. 이러한 비동기 처리 모델을 통해, 워커 프로세스는 I/O 작업이 블로킹되지 않고 여러 I/O 이벤트를 효율적으로 관리할 수 있습니다.

epoll 이란?

epoll의 대규모 동시 연결을 효율적으로 처리할 수 있는 능력 덕분에 Nginx의 확장성이 크게 향상되었습니다. selectpoll 같은 이전의 멀티플렉싱 메커니즘은 매번 모든 파일 디스크립터를 관찰하므로 파일 디스크립터의 수가 증가함에 따라 성능이 선형적으로 저하되는 문제가 있었습니다. epoll은 엣지 트리거 모드는 연결 상태의 변화(예: 읽기 가능, 쓰기 가능)만을 알림으로 받습니다. 이는 Nginx가 불필요한 이벤트 체크를 줄이고, CPU 사용을 최적화하여, 더 많은 연결을 더 빠르게 처리할 수 있게 합니다. 결과적으로, epoll을 사용함으로써 Nginx는 더 높은 동시 연결 수와 더 낮은 지연 시간을 달성할 수 있습니다.

epoll에서 이벤트는 파일 디스크립터의 상태 변화를 나타내는 신호 또는 알림입니다. 이러한 상태 변화에는 데이터가 읽기 가능, 쓰기 가능, 또는 오류 상태 등이 포함될 수 있습니다. epoll 시스템은 이러한 이벤트들을 효율적으로 모니터링하고, 애플리케이션이 적절히 반응할 수 있도록 알림을 제공합니다. epoll 이벤트의 주요 종류에는 다음과 같은 것들이 있습니다:

  • EPOLLIN: 데이터가 읽기를 위해 준비되었음을 나타냅니다 (예: 네트워크 소켓에 새 데이터가 도착함).
  • EPOLLOUT: 데이터를 쓸 수 있는 상태임을 나타냅니다 (예: 네트워크 소켓이 더 많은 데이터를 보낼 준비가 됨).
  • EPOLLPRI: 긴급한 데이터가 읽기를 위해 준비되었음을 나타냅니다 (예: OOB 데이터).
  • EPOLLERR: 파일 디스크립터에서 오류가 발생했음을 나타냅니다.
  • EPOLLHUP: 파일 디스크립터에 hang up이 발생했음을 나타냅니다.
  • EPOLLET: 엣지 트리거 모드를 활성화합니다. 이는 파일 디스크립터의 상태가 변할 때만 이벤트를 알립니다.

epoll 이벤트 저장 방식

epoll에서 이벤트는 내부 데이터 구조에 저장됩니다. epoll은 이벤트를 관리하기 위해 두 가지 주요 데이터 구조를 사용합니다:

1) 이벤트 테이블(Event Table): epoll 인스턴스에 등록된 모든 파일 디스크립터와 관련 이벤트 정보를 저장합니다. 이 테이블은 epoll_create 시스템 호출을 통해 생성되며, epoll_ctl 호출을 통해 파일 디스크립터를 추가, 수정, 삭제할 수 있습니다.

2) 준비 리스트(Ready List): 현재 I/O 작업을 수행할 준비가 완료된 파일 디스크립터의 이벤트를 저장합니다. 즉, epoll_wait 호출 시 반환되는 이벤트들이 이 리스트에 저장됩니다. 이 리스트는 애플리케이션이 처리해야 할 이벤트만 포함하므로, 효율적인 이벤트 처리가 가능합니다.

이벤트가 저장되는 위치

epoll의 데이터 구조는 커널 공간 내에 위치합니다. 애플리케이션은 시스템 호출을 통해 이러한 구조와 상호작용합니다. epoll 시스템 호출을 사용할 때, 애플리케이션은 커널에 이벤트에 대한 정보를 제공하고, 커널은 이 정보를 내부의 이벤트 테이블에 저장합니다. 이벤트 발생 시, 커널은 이를 준비 리스트에 추가하고, epoll_wait 호출을 통해 애플리케이션에 이벤트를 알립니다.

이러한 설계 덕분에 epoll은 고성능과 대규모 동시 연결 처리를 가능하게 합니다. 커널 공간 내에서 이벤트를 관리함으로써, epoll은 사용자 공간과 커널 공간 사이의 컨텍스트 스위칭을 최소화하고, 높은 효율성을 달성합니다.

결론

압도적 1위를 달리던 Apache의 MPM은 각 커넥션마다 프로세스나 쓰레드를 복제(fork)하여 요청을 처리했습니다. 프로세스와 쓰레드의 증가는 필요 메모리 공간의 증가, 컨텍스트 스위칭의 오버헤드를 증가시킵니다. 이에 비해 Nginx는 하나의 Worker Process가 Single Thead로 동작하며, 비동기 방식으로 여러 커넥션을 처리할 수 있으므로 자원을 좀 더 효율적으로 사용할 수 있습니다. 또한 epoll, kqueue, event ports와 같은 현대적인 이벤트 알림 시스템으로 많은 커넥션(많은 파일 디스크립터)을 동시에 관리할 수 있습니다.

References

https://aosabook.org/en/v2/nginx.html
https://cyuu.tistory.com/172