Posts WEB - nginx 구조 알아보기
Post
Cancel

WEB - nginx 구조 알아보기

https://aosabook.org/en/v2/nginx.html

  • 고성능, 높은 동시성, 낮은 메모리 사용

  • Apache같은 기존 웹 서버는 요청마다 새로운 스레드나 프로세스를 생성해서 처리했기 때문에, 동시 연결 수가 많아질수록 리소스 소비가 급격히 증가하고 성능이 떨어졌습니다.
  • 따라서, nginx는 C10K(10,000 Clients) 문제 해결을 목표로 이벤트 기반 구조로 설계되었다.
  • 실제로 다양한 운영 체제에서 진행 중인 고급 이벤트 기반 메커니즘의 개발에서 영감을 얻었다.

  • 아키텍처 특징
    • 이벤트 기반
    • 싱글 스레드
    • 비동기
    • 논블로킹

OS 관련

  • nginx는 다양한 작업(요청 수락, 처리, 관리 등)을 수행하면서 epoll, kqueue 등 이벤트 알림 시스템을 사용한다.
    • OS마다 다름
      • Linux: epoll BSD/macOS: kqueue Solaris: event ports
    • 이를 통해 비동기적(Asynchronous)이고 논블로킹(Non-blocking)인 방식으로 I/O 작업을 처리
  • nginx는 OS에 최대한 많은 힌트(정보)를 제공해서, 입출력(I/O)에 대해 비동기 피드백을 빠르게 받도록 한다.
    • 예: 파일이 다 읽혔는지, 클라이언트 소켓에 쓸 수 있는지 등을 OS가 알려줌
    • 이런 방식으로 불필요한 대기 없이 효율적인 I/O 처리가 가능

※ nginx가 OS에 주는 주요 “힌트”의 예시

  1. 읽거나 쓸 준비가 되었는지 알려달라는 요청 (event notification)
    • nginx는 epoll, kqueue 등을 통해 다음을 요청합니다:
      • “이 파일 디스크립터에 읽을 데이터가 생기면 알려줘”
      • “이 소켓이 쓰기 가능한 상태가 되면 알려줘”
    • 이걸 레벨 트리거 / 엣지 트리거 방식으로 감지할 수 있어요.
    • OS는 이 요청을 처리하고, 이벤트가 발생하면 nginx에게 “지금 처리 가능하다”는 신호를 줍니다.
  2. sendfile() 호출
    • 일반적인 파일 전송은: read() → 사용자 공간 → write() → 소켓
    • 하지만 sendfile()은 커널 내에서 직접 파일 → 소켓으로 보내므로:
      • 사용자 공간 복사 생략
      • CPU 사용량 절감
      • 훨씬 빠른 전송 가능
    • nginx는 OS에게 “sendfile을 사용하라”는 힌트를 줍니다.
  • 모듈 기반

    nginx의 모듈 기반 아키텍처 덕분에, 핵심 코드를 수정하지 않고도 기능 확장이 가능하다. 즉, nginx는 매우 확장 가능한 구조를 가졌고, 여러 기능을 모듈화해 놓았기 때문에 유연함 1.9.11 이후부터 동적 모듈 로딩(load_module)이 가능

  • core modules (핵심 기능)
  • event modules (이벤트 처리용)
  • phase handlers (요청 단계별 처리기)
  • protocols (HTTP, FastCGI 등)
  • variable handlers ($변수 처리)
  • filters (본문 변형)
  • upstreams (백엔드 서버 연결)
  • load balancers (로드 밸런싱)

  • "nginx uses multiplexing and event notifications heavily"
    • Multiplexing : 하나의 프로세스/스레드가 여러 연결(I/O)을 동시에 다루는 기술
    • 보통 epoll, kqueue, select, poll 같은 OS 레벨의 이벤트 감시 시스템을 사용
  • "dedicates specific tasks to separate processes"
  • 역할을 나눠서 작업(task)을 여러 프로세스에 분리해서 맡깁니다.
    • Master Process : 설정 파일 로딩, 워커 관리
    • Worker Processes : 클라이언트 요청 처리 (이벤트 루프 돌리는 주체)
    • Cache Loader / Cache Manager : (선택적) 캐시 관리 전용 프로세스

epoll이란 ?

Worker Model

worker : 싱글 스레드 기반의 프로세스

  • nginx가 시작하면 먼저 listen 소켓들을 설정된 포트로 바인딩함.
  • 공유된 listen 소켓에서 새 요청을 받아, 각 워커 내부의 고성능 run-loop를 통해 수천 개의 연결을 처리한다.
    • 하나의 포트를 여러 워커가 공유하고, 이벤트 루프(run-loop)를 통해 연결 수신 → 요청 수신 → 응답 생성 → 응답 전송의 전 과정을 논블로킹 방식으로 처리.
  • 어떤 워커가 요청을 받을지는 OS 커널 수준에서 accept() 처리에 의해 자동 결정됨 (e.g., epoll, SO_REUSEPORT 등).

  • nginx가 블로킹될 수 있는 유일한 경우는 워커 프로세스가 디스크 성능 부족으로 입출력을 기다리는 상황이다.

  • 일반적으로, 코어당 하나의 워커를 두면 멀티코어 구조를 최대한 활용할 수 있고, 스레드 충돌이나 락업(lock-up)을 방지할 수 있다.

이벤트 루프

연결 수신 -> 요청 읽기 -> 응답 생성 -> 응답 보내기 -> 연결 종료

  • 네트워크/디스크 상태를 비동기적으로 감시하고 처리함 ??

  • 이벤트 루프(run-loop)를 통해 논블로킹 방식으로 요청 처리. ?

요청은 이벤트 기반 루프(run-loop) 안에서 비동기적으로 처리 ? 각 워커는 단일 스레드임에도 수천 개의 연결을 논블로킹으로 동시에 처리 가능 ?

  • nginx는 다음을 통해 비동기 처리를 구현:
    • epoll/kqueue: 네트워크 이벤트 감시
    • sendfile: 파일 → 소켓 전송 최적화
    • AIO (Asynchronous I/O): 디스크 I/O 비동기 처리
    • callback 함수 + 타이머: 비동기 실행 순서 제어

파일 디스크립터 + epoll 구조

nginx가 수만 개의 동시 연결을 안정적으로 처리할 수 있는 핵심 이유는 “FD 기반 + epoll 이벤트 루프 + 논블로킹/비동기 처리”라는 구조적 설계

1. 연결마다 독립적인 FD로 관리되기 때문

  • 각 클라이언트 연결은 파일 디스크립터(FD) 하나로 식별됨
  • FD는 정수값이라 관리가 매우 빠름 (O(1) 연산)
  • 커널 내부에서 FD마다 I/O 상태를 추적하므로, nginx는 FD 번호만 가지고 있으면 됨
  • 이 덕분에 수만 개의 연결도 경량화된 구조로 추적 가능
  • 동시에 커널이 직접 연결의 상태, 이벤트 발생 여부를 추적해주기 때문에 안정적

2. epoll이 이벤트가 있는 FD만 알려주기 때문

시스템 콜처리 방식단점
select()FD 집합을 전부 순회FD 수 제한 있음 (보통 1024)
poll()배열 순회1만 개면 1만 개 다 순회 (O(N))
epoll()이벤트 있는 FD만 반환 (O(1))매우 빠름, 스케일 가능

3. 논블로킹 + 비동기 처리로 “멈추지 않고 처리” 가능하기 때문

  • nginx는 모든 소켓을 non-blocking mode로 설정
  • 데이터를 바로 읽거나 쓸 수 없으면 → 그냥 넘기고 다음 작업 수행
    • 즉, 작업이 완료될 때까지 블로킹(wait) 하지 않음 → 한 연결이 느려도 다른 연결에는 영향 없음
  • 디스크나 네트워크가 준비되면 epoll 이벤트로 다시 알림 받음 (비동기, 즉 작업의 완료를 기다리지 않음)

4. 단일 스레드 구조라 락 경합(lock contention)이 없음

  • nginx는 각 워커가 단일 스레드로 동작
  • 수많은 연결을 하나의 이벤트 루프에서 비동기적으로 처리
  • 다중 스레드가 공유 자원 놓고 싸우지 않음 → 스케일이 잘 되고 안정적

5. epoll + FD + 콜백 구조는 “설계적으로 단순”하면서도 강력함

  • 수천 개 연결 중 이벤트 발생한 소수만 핸들러 실행
  • 나머지는 커널이 FD 상태만 추적 중 (리소스 사용 안 함)
1
2
3
4
for (int i = 0; i < nevents; i++) {
    ngx_event_t *ev = events[i].data.ptr;
    ev->handler(ev);  // 이벤트 발생한 연결만 처리
}

워커 수 조정

디스크 사용량과 CPU 부하 패턴에 따라 nginx 워커 수를 조정해야 할 수 있다. 이 규칙은 다소 기본적이며, 시스템 관리자는 워크로드에 맞게 몇 가지 설정을 실험해보는 것이 좋다.

  • 만약 부하가 CPU 중심이라면(예: TCP/IP 처리, SSL, 압축 등), 워커 수는 CPU 코어 수와 같게 설정하는 것이 좋다.
  • 반면 부하가 디스크 I/O 중심이라면(예: 다양한 콘텐츠 제공, 무거운 프록시 처리), 워커 수는 코어 수의 1.5 ~ 2배 정도로 설정할 수 있다.

부록

  • 아파치의 이벤트 기반 구조와 다른점 ? => 라인에서 대규모 트래픽 다룰때 아파치 쓴다한거 같은데 (라인개발실록)

리슨 소켓, 클라이언트 통신 소켓

  • TCP 서버의 소켓 동작 흐름
    • 서버가 먼저 listen() 소켓을 열고 포트에 바인딩
    • 클라이언트가 연결 시도 → accept() 호출로 연결 수락
    • 이때 새로운 소켓이 생성되어, 클라이언트와의 통신에 사용됨
  • 예시 ``` int listen_fd = socket(AF_INET, SOCK_STREAM, 0); // 리슨 전용 소켓 bind(listen_fd, …); listen(listen_fd, SOMAXCONN);

// 클라이언트 연결 수락 → 새로운 FD 생성 int client_fd = accept(listen_fd, NULL, NULL); // 개별 클라이언트와 통신하기 위한 독립적인 소켓

1
1
2
3
4
5
6
7
8
9
        [Master Process]
               │
    ┌──────────┴──────────┐
    │                     │  [Worker #1]           [Worker #2]
    │                     │
    │                     │    [listen socket]    ← 모든 워커가 공유
    │
accept()
    ↓  [client socket]  ← 클라이언트 연결 1개당 생성 ```

리버스 프록시

1
2
3
4
[Client] ─────▶ [nginx] ─────▶ [Upstream Server]
             ▲        ▲
        listen FD   upstream FD
        (클라이언트용)   (백엔드용)
소켓 종류설명
리슨 소켓클라이언트 연결 대기용 (공통)
클라이언트 소켓클라이언트와의 실제 데이터 송수신용 FD
업스트림 소켓백엔드 서버와 통신하기 위한 FD (예: proxy_pass)
This post is licensed under CC BY 4.0 by the author.