Skip to content
Published on

Nginx 내부 Deep Dive — Master/Worker, Event Loop, Phase Handler, Upstream, Pingora 완전 정복 (2025)

Authors

TL;DR

  • Nginx는 2004년 러시아 Igor Sysoev가 C10k 문제 해결을 위해 시작. 현재 세계 웹 서버의 30%+ 점유율.
  • Master-Worker: Master는 설정/관리, Worker가 실제 요청 처리. Worker 프로세스는 고정 개수 (CPU 코어 수).
  • Event-driven reactor: 각 worker가 epoll/kqueue로 수만 개 연결을 단일 스레드로 처리. 스레드/프로세스-per-request가 아님.
  • Non-blocking I/O: 모든 I/O는 비블록. 한 연결이 대기하는 동안 다른 연결 처리.
  • Phase Handler: 요청 처리를 11개 단계로 분리 (post-read → rewrite → access → content → log). 모듈이 원하는 단계에 핸들러 등록.
  • Upstream: Reverse proxy. Keep-alive 연결 풀, 로드 밸런싱, health check.
  • 모듈 시스템: 대부분 컴파일 시 정적 링크. Dynamic module은 1.9.11+.
  • Graceful Reload: 설정 변경 시 새 worker 시작 → 기존 worker가 현재 요청 완료 후 종료. 무중단.
  • Cloudflare Pingora (2022): Rust + Tokio로 재작성. 극한 스케일에서의 nginx 대안.

1. 배경 — C10k 문제

1.1 2003년의 웹 서버

2003년 Apache HTTP Server는 지배적. 두 가지 MPM (Multi-Processing Module):

Prefork:

  • 각 요청마다 프로세스 하나.
  • 프로세스 pool로 재사용.
  • 단순, 메모리 격리.
  • 프로세스당 2-8 MB RAM.

Worker:

  • 각 요청마다 스레드 하나.
  • 프로세스당 여러 스레드.
  • Prefork보다 경량이지만 여전히 스레드당 MB급 스택.

1.2 C10k — 1만 동시 연결의 벽

1999년 Dan Kegel의 "The C10k problem" 글이 문제를 명명. "왜 우리는 1만 동시 연결을 처리하지 못하는가?"

근본 원인: 스레드/프로세스-per-connection 모델의 수학적 한계.

10,000 connections × 8 MB stack = 80 GB RAM

현실적으로 불가능. 서버당 동시 연결은 ~1,000에 멈춤. 당시 서비스 확장의 병목.

1.3 커널 변화

2002년 Linux 2.6에 epoll 도입 (Davide Libenzi). select/poll의 O(n) 한계 극복:

  • kevent 등록: 관심 있는 fd를 커널에 등록.
  • wait: 준비된 이벤트만 반환. O(1).

BSD는 더 일찍 kqueue (1999). Solaris는 event ports.

이것이 수만 연결을 단일 스레드로 처리할 수 있게 해준 OS 레벨의 돌파.

1.4 Igor Sysoev와 Nginx

Igor Sysoev: Rambler.ru (러시아 검색 엔진)의 시스템 관리자. 대규모 트래픽으로 Apache의 한계에 직면.

2002년 시작, 2004년 nginx 0.1.0 공개. 목표:

  1. C10k 해결: 단일 서버로 수만 연결.
  2. 낮은 메모리: 프로세스 몇 개로 충분.
  3. Reverse proxy 강화: Apache보다 더 나은 프록시.
  4. Graceful reload: 다운타임 없는 설정 변경.

초기엔 러시아어 문서만. 2007년경 서구로 확산. 2011년 Nginx Inc. 설립 (F5가 2019년 인수).

1.5 2025년 시장

Netcraft 통계 (2024):

  • Nginx: ~32% (활성 사이트).
  • Apache: ~25%.
  • Cloudflare: ~20% (자체 프록시).
  • Microsoft IIS: ~5%.

여전히 1위. 모든 대형 서비스(Netflix, Dropbox, Airbnb, CDN, 이커머스)가 사용.


2. 아키텍처 — Master + Workers

2.1 프로세스 구조

ps aux | grep nginx:

nginx: master process /usr/sbin/nginx
nginx: worker process
nginx: worker process
nginx: worker process
nginx: worker process
nginx: cache manager process
nginx: cache loader process

Master: 설정 읽기, worker 시작/중지, 로그 파일 열기, 시그널 처리. Worker: 실제 요청 처리. 각자 독립적, 메모리 공유 없음. Cache Manager/Loader: 캐시 파일 관리 (설정된 경우).

2.2 Master 역할

Master는 요청을 처리하지 않는다. 관리자.

시그널 처리:

  • SIGHUP: 설정 재로드.
  • SIGUSR1: 로그 파일 재열기 (logrotate).
  • SIGUSR2: 실행 파일 업그레이드.
  • SIGTERM / SIGQUIT: 종료.
  • SIGWINCH: worker 종료 (master는 유지).

또한:

  • Worker를 주기적으로 health check.
  • 죽은 worker를 자동 재시작.
  • 설정 재로드 시 새 worker 시작, 기존 worker는 graceful shutdown.

2.3 Worker 개수

worker_processes auto;  # CPU 코어 수 자동
# 또는 명시적
worker_processes 4;

일반적으로 CPU 코어 수. 이벤트 드리븐 모델이라 더 많아도 의미 없음 (컨텍스트 스위칭만 증가).

2.4 Worker 연결 수

events {
    worker_connections 4096;
}

각 worker가 처리할 수 있는 동시 연결. 총 동시 연결 ≈ worker_processes × worker_connections.

예: 4 workers × 4096 = 16,384 동시 연결.

시스템 제한 확인:

ulimit -n        # open files 제한
cat /proc/sys/fs/file-max

Nginx worker의 연결 수는 이 제한을 넘지 못함.

2.5 CPU Affinity

worker_cpu_affinity auto;
# 또는 명시적
worker_cpu_affinity 0001 0010 0100 1000;

각 worker를 특정 CPU에 고정 → 캐시 지역성 향상. 고성능 시스템에서 5-10% 향상.

2.6 왜 프로세스인가 (스레드가 아니라)

Nginx는 멀티 스레드가 아닌 멀티 프로세스를 택했다. 이유:

  • 격리: 한 worker crash가 다른 worker에 영향 없음.
  • 락 없음: 공유 메모리 거의 없음 → lock-free.
  • Copy-on-write: Linux fork()가 페이지를 공유 → 메모리 효율.
  • 단순함: 스레드 동기화 복잡도 없음.

단점: 약간의 메모리 중복. 하지만 Linux COW로 실제로는 작음.


3. Event Loop — Reactor Pattern

3.1 핵심 아이디어

각 worker는 단일 스레드로 수만 연결을 처리한다. 어떻게?

Reactor pattern:

while (true) {
    events = epoll_wait(...);  // 준비된 이벤트 대기
    for (event in events) {
        handler(event);  // 해당 fd의 handler 호출
    }
}

모든 I/O는 non-blocking. 한 연결이 데이터를 기다리는 동안 worker는 다른 연결을 처리.

3.2 Epoll

Linux의 고성능 I/O multiplexer:

int epfd = epoll_create1(0);

struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = client_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &ev);

// 이벤트 루프
struct epoll_event events[MAX_EVENTS];
while (1) {
    int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
    for (int i = 0; i < n; i++) {
        handle_event(events[i].data.fd);
    }
}
  • epoll_create1: 인스턴스 생성.
  • epoll_ctl: fd 추가/수정/삭제.
  • epoll_wait: 준비된 이벤트 대기.

O(1) 복잡도 (관심 fd 수와 무관). select(O(n))보다 훨씬 빠름.

3.3 Edge-Triggered vs Level-Triggered

Level-Triggered (LT): 준비 상태일 동안 계속 이벤트 반환. 기본값.

Edge-Triggered (ET): 상태 변화 순간에만 이벤트. 한 번만 반환.

ev.events = EPOLLIN | EPOLLET;  // edge-triggered

Nginx는 Edge-Triggered 사용. 이유:

  • 더 적은 이벤트 처리.
  • 한 번에 최대한 많이 읽어야 함 (non-blocking read를 EAGAIN이 날 때까지).

3.4 Cross-Platform

Nginx는 여러 event mechanism 지원:

  • epoll: Linux.
  • kqueue: FreeBSD, macOS.
  • event ports: Solaris.
  • /dev/poll: Older Solaris.
  • select/poll: Fallback.

configure 시 자동 선택. 자동 OS 감지.

3.5 Connection Lifecycle

1. accept() 시스템콜
2. 새 fd를 epoll에 추가 (EPOLLIN)
3. 클라이언트 데이터 도착 → epoll 이벤트
4. read() 호출 → 요청 파싱
5. Phase handlers 실행
6. 응답 데이터 준비
7. write() 호출 → 버퍼 full → EPOLLOUT 등록
8. 쓰기 가능해지면 다시 epoll 이벤트 → 계속 write
9. 완료 → fd 닫기 또는 keep-alive

핵심: 한 단계가 blocking되면 다른 연결로 전환. I/O 대기 없이 CPU 활용.

3.6 Worker의 단일 스레드

한 worker가 단일 스레드로 수만 연결을 처리한다. 이 모델의 전제:

  • CPU 작업이 매우 짧아야 함: 각 핸들러가 μs 단위.
  • Blocking syscall 금지: 모든 것이 non-blocking.
  • Long computation 금지: 한 연결의 복잡한 계산이 모든 연결을 막음.

Nginx의 모듈은 이 규칙을 엄격히 따른다. 무거운 작업(DB 쿼리, 디스크 I/O)은 upstream으로 위임하거나 thread pool (1.7.11+)에 오프로드.

3.7 Thread Pool

일부 작업은 불가피하게 blocking:

  • 디스크 read (파일 캐시 miss).
  • DNS resolution.

이 경우 nginx는 thread pool에 오프로드:

thread_pool default threads=32 max_queue=65536;

location / {
    aio threads=default;
}

Thread pool worker들이 blocking 작업 수행. 메인 이벤트 루프는 계속.


4. 요청 생명주기 — Phase Handlers

4.1 11 Phases

Nginx는 요청 처리를 11개 단계로 분리:

1. NGX_HTTP_POST_READ_PHASE       - 요청 읽은 직후
2. NGX_HTTP_SERVER_REWRITE_PHASE  - server-level rewrite
3. NGX_HTTP_FIND_CONFIG_PHASE     - location 찾기
4. NGX_HTTP_REWRITE_PHASE         - location-level rewrite
5. NGX_HTTP_POST_REWRITE_PHASE    - rewrite 후 loop 체크
6. NGX_HTTP_PREACCESS_PHASE       - access 전
7. NGX_HTTP_ACCESS_PHASE          - access 제어
8. NGX_HTTP_POST_ACCESS_PHASE     - access 후
9. NGX_HTTP_PRECONTENT_PHASE      - content 전
10. NGX_HTTP_CONTENT_PHASE        - 실제 응답 생성
11. NGX_HTTP_LOG_PHASE            - 로그 기록

4.2 Phase 별 역할

1. POST_READ: 요청 헤더 읽기 직후. IP 로깅 등.

  • Module: realip (X-Forwarded-For 처리).

2. SERVER_REWRITE: server 블록 레벨 rewrite.

  • Module: rewrite directive.

3. FIND_CONFIG: URI에 매치하는 location 블록 찾기.

  • Built-in. 설정 파싱 결과 사용.

4. REWRITE: location 블록 내 rewrite.

  • Module: rewrite directive (location 안에서).

5. POST_REWRITE: Rewrite loop 방지 (5회 이상 rewrite 시 에러).

6. PREACCESS: Access 체크 전.

  • Module: limit_req, limit_conn (rate limit).

7. ACCESS: 접근 제어.

  • Module: access (allow/deny), auth_basic, auth_request, auth_jwt.

8. POST_ACCESS: Access 후 처리.

  • Internal only.

9. PRECONTENT: Content 생성 전.

  • Module: try_files, mirror.

10. CONTENT: 실제 응답 생성.

  • Module: static, index, proxy_pass, fastcgi_pass, uwsgi_pass, memcached, autoindex, dav.

11. LOG: 응답 후 로그.

  • Module: access_log, status.

4.3 Handler 등록

모듈 코드 예:

static ngx_int_t
ngx_http_my_module_init(ngx_conf_t *cf)
{
    ngx_http_core_main_conf_t  *cmcf;
    ngx_http_handler_pt        *h;

    cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module);

    h = ngx_array_push(&cmcf->phases[NGX_HTTP_ACCESS_PHASE].handlers);
    if (h == NULL) return NGX_ERROR;

    *h = ngx_http_my_module_handler;

    return NGX_OK;
}

모듈이 NGX_HTTP_ACCESS_PHASE의 handlers 배열에 자기 handler 추가. 요청 처리 시 해당 phase에서 호출.

4.4 Phase Engine

요청이 들어오면 phase engine이 순차 실행:

while (phase_idx < 11) {
    handler = phases[phase_idx].handlers[handler_idx];
    rc = handler(request);
    
    switch (rc) {
        case NGX_DECLINED:
            // 다음 handler
            handler_idx++;
            break;
        case NGX_AGAIN:
            // 더 대기 (async)
            return;
        case NGX_OK:
            // 다음 phase
            phase_idx++;
            handler_idx = 0;
            break;
        case NGX_HTTP_FORBIDDEN:
        case NGX_HTTP_UNAUTHORIZED:
            // 에러 처리
            finalize(request, rc);
            return;
    }
}

각 handler가 상태 반환. Async 처리도 가능 (NGX_AGAIN 반환 → 나중에 재진입).

4.5 Pipelined Request

Phase 시스템의 우아함: 모듈이 서로 조합 가능.

location /api/ {
    # POST_READ phase
    set_real_ip_from 10.0.0.0/8;
    
    # PREACCESS phase
    limit_req zone=api burst=20;
    
    # ACCESS phase
    auth_jwt "API" token=$http_authorization;
    auth_jwt_key_file /etc/nginx/jwk.json;
    
    # CONTENT phase
    proxy_pass http://backend;
    
    # LOG phase
    access_log /var/log/api.log;
}

각 directive가 다른 phase에 handler를 등록. Nginx가 자동으로 순서대로 실행. 개발자는 "언제 실행될지" 신경 안 써도 됨.


5. Upstream — Reverse Proxy

5.1 핵심 역할

Nginx의 킬러 피처: reverse proxy.

upstream backend {
    server 10.0.0.1:8080;
    server 10.0.0.2:8080;
    server 10.0.0.3:8080;
}

server {
    location / {
        proxy_pass http://backend;
    }
}
  • 클라이언트는 nginx를 봄.
  • Nginx가 backend로 요청 전달.
  • Backend 응답을 클라이언트로 전달.

5.2 Load Balancing

Round Robin (기본):

upstream backend {
    server s1;
    server s2;
    server s3;
}

순차 분배.

Least Connections:

upstream backend {
    least_conn;
    server s1;
    server s2;
}

현재 active 연결이 가장 적은 서버로.

IP Hash:

upstream backend {
    ip_hash;
    server s1;
    server s2;
}

클라이언트 IP 기반 해시. 같은 IP는 항상 같은 서버 (session stickiness).

Hash (generic):

upstream backend {
    hash $request_uri consistent;
}

임의의 변수 기반 해시. consistent는 consistent hashing (서버 변경 시 재분배 최소).

Random:

upstream backend {
    random two least_conn;
}

두 서버를 랜덤 선택 후 least connection.

Weighted:

upstream backend {
    server s1 weight=5;
    server s2 weight=1;
}

가중치 비율 분배.

5.3 Health Check

Passive (기본, open source):

upstream backend {
    server s1 max_fails=3 fail_timeout=30s;
    server s2;
}

요청 실패가 max_fails 쌓이면 fail_timeout 동안 제외.

Active (상업용 Nginx Plus 또는 ngx_http_upstream_check_module):

upstream backend {
    server s1;
    server s2;
    check interval=3000 rise=2 fall=5 timeout=1000;
}

주기적으로 backend에 "ping" 요청. 응답 확인.

5.4 Keepalive

upstream backend {
    server s1;
    keepalive 32;  # 각 worker당 keep-alive 연결 32개
}

server {
    location / {
        proxy_pass http://backend;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
    }
}

Backend로의 연결을 재사용. 연결 생성 비용 절감 → 레이턴시 감소.

중요: proxy_http_version 1.1proxy_set_header Connection ""가 있어야 HTTP/1.1 keep-alive가 작동.

5.5 Request Buffering

proxy_buffering on;        # 기본
proxy_buffers 8 16k;
proxy_buffer_size 4k;
proxy_busy_buffers_size 32k;

Nginx가 backend 응답을 버퍼에 저장. 클라이언트가 느려도 backend는 빠르게 응답 완료.

단점: 스트리밍 응답에 부적합. 비활성화:

proxy_buffering off;

5.6 Timeouts

proxy_connect_timeout 5s;  # backend 연결 timeout
proxy_send_timeout 60s;    # backend에 데이터 보내기 timeout
proxy_read_timeout 60s;    # backend 응답 읽기 timeout

각 단계별 timeout 설정. 느린 backend가 worker를 점유하지 않게.

5.7 Retry

location / {
    proxy_pass http://backend;
    proxy_next_upstream error timeout http_500 http_502 http_503;
    proxy_next_upstream_tries 3;
    proxy_next_upstream_timeout 10s;
}

한 backend 실패 시 다른 backend로 retry. 조건 설정 가능.


6. 캐싱

6.1 Nginx Cache

Nginx는 HTTP 캐시를 내장. CDN 유사:

proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m
                 max_size=1g inactive=60m use_temp_path=off;

server {
    location / {
        proxy_cache my_cache;
        proxy_cache_key "$scheme$request_method$host$request_uri";
        proxy_cache_valid 200 302 10m;
        proxy_cache_valid 404 1m;
        proxy_pass http://backend;
    }
}

파라미터:

  • levels: 디렉토리 레벨 구조 (1:2 = a/bc/...).
  • keys_zone: 인덱스 공유 메모리 영역.
  • max_size: 최대 캐시 크기.
  • inactive: 접근 없으면 삭제.

6.2 Cache Key

기본:

proxy_cache_key "$scheme$request_method$host$request_uri";

GET /api/users는 "http,GET,example.com,/api/users"로 해시 → 파일 경로.

6.3 Cache Validity

proxy_cache_valid 200 1h;
proxy_cache_valid 404 10m;
proxy_cache_valid any 1m;

응답 코드별 TTL. 또는 Cache-Control 헤더 준수:

proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;

Backend 실패 시 stale 캐시 제공.

6.4 Cache Purge

Open source nginx는 수동 purge 제한적. 파일 직접 삭제:

rm -rf /var/cache/nginx/*

또는 ngx_cache_purge 서드파티 모듈:

location ~ /purge(/.*) {
    proxy_cache_purge my_cache "$scheme$request_method$host$1";
}

Nginx Plus는 proxy_cache_purge 내장.

6.5 Microcaching

짧은 TTL(1-5초)로 순간 트래픽 흡수:

proxy_cache_valid 200 1s;

1초의 캐시가 99% hit rate에 도달할 수 있다 (바이럴 게시물, 뉴스 속보). Backend 부하를 극적으로 감소.

6.6 Cache Lock

Thundering herd 방지:

proxy_cache_lock on;
proxy_cache_lock_timeout 5s;

같은 URL에 동시 요청이 들어왔을 때 한 요청만 backend로 전달. 나머지는 첫 요청의 결과를 기다림.


7. SSL / TLS

7.1 기본

server {
    listen 443 ssl;
    server_name example.com;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers off;
}

7.2 Session Resumption

TLS handshake가 비쌈. 재사용:

Session Cache:

ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1h;

10 MB 공유 메모리에 수만 세션 저장.

Session Tickets:

ssl_session_tickets on;

클라이언트가 ticket을 받아 다음 연결 시 재사용.

7.3 HTTP/2

server {
    listen 443 ssl http2;
    # ...
}

HTTP/2는 TLS + multiplexing + server push. 최근 nginx 버전 기본.

7.4 HTTP/3 (QUIC)

Nginx 1.25+ 실험 지원:

server {
    listen 443 quic reuseport;
    listen 443 ssl;
    # ...
    add_header Alt-Svc 'h3=":443"; ma=86400';
}

Cloudflare는 이미 대규모 HTTP/3 배포.

7.5 OCSP Stapling

ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
resolver 8.8.8.8 8.8.4.4 valid=300s;

Nginx가 OCSP 응답을 서버 측에서 미리 가져와 TLS handshake에 포함. 클라이언트의 OCSP 조회 불필요 → 더 빠름 + 프라이버시.


8. 모듈 시스템

8.1 정적 vs 동적

정적 (원래 방식): 모듈을 컴파일 시 nginx 바이너리에 포함.

./configure --with-http_ssl_module --with-http_realip_module ...
make

동적 (1.9.11+):

./configure --add-dynamic-module=/path/to/module
make
load_module modules/my_module.so;

런타임 로드. 하지만 대부분 배포판 package는 자주 쓰는 모듈을 정적으로 포함.

8.2 Core Modules

Nginx의 기본 모듈 (대부분 static):

  • http_core: 기본 HTTP 기능.
  • http_ssl: HTTPS.
  • http_proxy: Reverse proxy.
  • http_fastcgi, http_uwsgi, http_scgi: 각각의 프로토콜.
  • http_cache: 캐싱.
  • http_upstream: Upstream 관리.
  • http_gzip: Gzip 압축.
  • http_brotli: Brotli (서드파티).
  • http_rewrite: URL rewrite.
  • http_access: 접근 제어.
  • stream: TCP/UDP load balancing.
  • mail: 메일 프록시.

8.3 Third-Party Modules

수백 개의 서드파티 모듈:

  • ngx_pagespeed: Google의 웹 성능 최적화.
  • ngx_lua (OpenResty의 일부): Lua 스크립팅.
  • ngx_http_js_module: njs (JavaScript).
  • headers_more: 더 유연한 헤더 조작.
  • geoip: IP 지역 정보.
  • ngx_http_substitutions_filter_module: 응답 본문 substitution.

8.4 OpenResty

OpenResty: nginx + LuaJIT + 모듈 bundle. 사실상 별도의 프로젝트.

location /hello {
    content_by_lua_block {
        ngx.say("Hello from Lua!")
    }
}

Lua로 phase handler 작성 가능. 동적 요청 처리, API gateway, 웹 앱 직접 작성까지.

Alibaba, Cloudflare (초기), Kong API gateway 등이 OpenResty 기반.


9. Graceful Reload

9.1 무중단 설정 재로드

sudo nginx -s reload
# 또는
sudo systemctl reload nginx
# 또는
sudo kill -HUP $(cat /var/run/nginx.pid)

SIGHUP이 master에 전달. Master가:

  1. 새 설정 파일 검증.
  2. 유효하면 새 worker 시작 (새 설정으로).
  3. 기존 worker에 SIGQUIT (graceful shutdown) 전송.
  4. 기존 worker는 현재 요청 완료 후 종료.
  5. 새 worker가 새 요청 받음.

9.2 성능

기존 worker가 지속적 연결을 처리 중이면 종료 지연. worker_shutdown_timeout로 최대 대기 시간 설정.

9.3 다운타임 0

클라이언트 관점에서:

  • 진행 중 요청: 기존 worker가 완료.
  • 새 요청: 새 worker가 처리.
  • 요청 드롭 없음.

이것이 10년 넘게 nginx를 운영에 매력적으로 만든 이유.

9.4 Binary Upgrade

바이너리 자체 교체도 무중단:

# 새 바이너리 설치
sudo cp nginx-new /usr/sbin/nginx

# Master에 USR2 시그널
sudo kill -USR2 $(cat /var/run/nginx.pid)

# 새 master + worker가 시작
# 기존 master + worker는 여전히 실행

# 새 것이 잘 동작하면 기존 종료
sudo kill -WINCH $(cat /var/run/nginx.pid.oldbin)  # 기존 worker 종료
sudo kill -QUIT $(cat /var/run/nginx.pid.oldbin)   # 기존 master 종료

나쁘게 가면 롤백:

sudo kill -HUP $(cat /var/run/nginx.pid.oldbin)
sudo kill -QUIT $(cat /var/run/nginx.pid)

실전 운영의 필수 기능.


10. 설정 예제

10.1 기본 리버스 프록시

upstream app {
    server app1:8080;
    server app2:8080;
    keepalive 32;
}

server {
    listen 443 ssl http2;
    server_name example.com;

    ssl_certificate /etc/ssl/cert.pem;
    ssl_certificate_key /etc/ssl/key.pem;

    location / {
        proxy_pass http://app;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Connection "";
    }
}

10.2 정적 파일 서빙

server {
    listen 80;
    root /var/www/html;
    
    location / {
        try_files $uri $uri/ /index.html;
    }
    
    location ~ \.(jpg|jpeg|png|gif|ico|css|js)$ {
        expires 30d;
        add_header Cache-Control "public, immutable";
    }
}

10.3 Rate Limiting

http {
    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
    limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
}

server {
    location /api/ {
        limit_req zone=api burst=20 nodelay;
        proxy_pass http://backend;
    }

    location /login {
        limit_req zone=login burst=5;
        proxy_pass http://backend;
    }
}

IP당 분당 600 요청 (api), 60 요청 (login). Burst는 순간 허용량.

10.4 Gzip

gzip on;
gzip_vary on;
gzip_min_length 1000;
gzip_comp_level 6;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

텍스트 응답 압축. 대역폭 절감.

10.5 CORS

location /api/ {
    if ($request_method = OPTIONS) {
        add_header Access-Control-Allow-Origin *;
        add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";
        add_header Access-Control-Allow-Headers "Content-Type, Authorization";
        return 204;
    }

    add_header Access-Control-Allow-Origin * always;
    proxy_pass http://backend;
}

10.6 WebSocket Proxy

location /ws {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_read_timeout 86400;  # 24h
}

UpgradeConnection 헤더가 핵심. WebSocket은 HTTP로 시작해 TCP로 upgrade.


11. 튜닝

11.1 worker_processes와 connections

worker_processes auto;
worker_rlimit_nofile 65535;

events {
    worker_connections 4096;
    multi_accept on;
    use epoll;
}
  • worker_rlimit_nofile: 각 worker의 파일 디스크립터 제한.
  • multi_accept: 한 번의 epoll_wait에서 여러 연결 accept.
  • use epoll: 명시적 (자동이 실패할 경우).

11.2 버퍼 크기

client_body_buffer_size 16K;
client_header_buffer_size 1k;
client_max_body_size 10m;
large_client_header_buffers 4 8k;

큰 body가 필요하면 client_max_body_size 증가 (파일 업로드 등).

11.3 Timeouts

client_body_timeout 12s;
client_header_timeout 12s;
keepalive_timeout 75s;
send_timeout 10s;

공격 방어 (slow client) + 리소스 해제.

11.4 sendfile, tcp_nopush, tcp_nodelay

sendfile on;
tcp_nopush on;
tcp_nodelay on;
  • sendfile: 커널이 파일을 소켓으로 직접 복사 (sendfile syscall). 유저 공간 복사 없음.
  • tcp_nopush: Sendfile과 같이 — 큰 청크를 한 번에 전송.
  • tcp_nodelay: Nagle 알고리즘 비활성 → keep-alive 응답 지연 감소.

11.5 OS 튜닝

# /etc/sysctl.conf
net.ipv4.tcp_fin_timeout = 30
net.ipv4.tcp_tw_reuse = 1
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 16384
net.ipv4.tcp_max_syn_backlog = 8192

Nginx 자체만큼 OS 튜닝도 중요.

11.6 Profile

# 이벤트 시간 측정
ab -n 10000 -c 100 http://localhost/

# 시스템 콜 추적
strace -c -p <worker_pid>

# perf
perf record -g -p <worker_pid>
perf report

12. 관측성

12.1 Access Log

log_format main '$remote_addr - $remote_user [$time_local] '
                '"$request" $status $body_bytes_sent '
                '"$http_referer" "$http_user_agent" '
                '"$http_x_forwarded_for" $request_time';

access_log /var/log/nginx/access.log main buffer=32k flush=5s;

$request_time: 전체 요청 처리 시간. 느린 요청 분석에 중요.

12.2 Error Log

error_log /var/log/nginx/error.log warn;

레벨: debug, info, notice, warn, error, crit, alert, emerg.

12.3 Status Page

location /nginx_status {
    stub_status;
    allow 127.0.0.1;
    deny all;
}
Active connections: 291 
server accepts handled requests
 16630948 16630948 31070465 
Reading: 6 Writing: 179 Waiting: 106

기본 메트릭. Prometheus exporter로 시계열로 수집 가능.

12.4 VTS Module

nginx-module-vts: 더 상세한 통계.

  • Per-server, per-location 통계.
  • 응답 코드 분포.
  • Upstream 통계.
  • JSON API.

12.5 OpenTelemetry

Nginx 1.24+ OpenTelemetry 모듈:

otel_exporter {
    endpoint otel-collector:4317;
}

otel_trace on;
otel_trace_context propagate;

Distributed tracing 지원. Trace ID가 요청 전체에 전파.


13. Cloudflare Pingora

13.1 왜 재작성

Cloudflare는 하루 수 조 요청을 처리한다. Nginx가 10년 이상 주력이었지만 한계에 도달:

  1. 성능: 특정 워크로드에서 CPU 비효율.
  2. 메모리 안전: C 기반 → 버그.
  3. 연결 재사용: 복잡한 upstream 풀링.
  4. Async: Worker 프로세스마다 단일 스레드 이벤트 루프 (thread pool은 보조).

2017년경 Pingora 프로젝트 시작. Rust + Tokio.

13.2 발표 (2022)

Cloudflare 블로그: "How we built Pingora, the proxy that connects Cloudflare to the Internet".

결과:

  • 70% CPU 감소.
  • 67% 메모리 감소.
  • Zero memory safety bugs (Rust 타입 시스템).
  • 더 나은 upstream 연결 재사용.

13.3 차이점

Nginx 모델:

  • 다중 worker 프로세스.
  • 각 worker = 단일 스레드 이벤트 루프.
  • Shared-nothing.

Pingora 모델:

  • 다중 worker 스레드 (thread pool).
  • 각 스레드 = Tokio async runtime.
  • Work stealing — 한 스레드의 작업이 밀리면 다른 스레드가.

13.4 Work Stealing의 이점

Nginx: worker A가 바쁘고 worker B가 한가해도 A의 작업을 B에게 넘길 수 없음. 연결이 worker에 고정.

Pingora: Tokio의 task가 어느 스레드에서든 실행 가능. 부하 분산 자동.

Cloudflare의 워크로드(수십만 connection, 극도로 불균일한 부하)에 이 차이가 30%+ 성능 향상.

13.5 오픈소스화 (2024)

Cloudflare가 Pingora를 오픈소스로 공개 (2024년 초). GitHub에 라이브러리 형태로.

use pingora::prelude::*;

pub struct MyProxy;

#[async_trait]
impl ProxyHttp for MyProxy {
    async fn upstream_peer(&self, session: &mut Session, ctx: &mut ()) -> Result<Box<HttpPeer>> {
        Ok(Box::new(HttpPeer::new(
            ("backend.example.com", 443),
            true,
            "backend.example.com".to_string(),
        )))
    }
}

러스트 파일 수십 줄로 커스텀 프록시 작성 가능.

13.6 Nginx를 대체할까

대부분 사용자: No. Nginx가 여전히 훌륭. 생태계, 문서, 모듈.

극한 스케일: Pingora 또는 유사 도구로 이동 가능. Cloudflare, 대형 CDN.

새 프로젝트: 상황에 따라. API gateway는 Envoy, 단순 reverse proxy는 Nginx, 고성능 custom은 Pingora.


14. 대안과 경쟁자

14.1 Apache HTTP Server

오래된 그러나 여전히 광범위 사용. 장점:

  • 풍부한 모듈: 수천 개.
  • .htaccess: Per-directory 설정 (nginx에 없음).
  • MPM 선택: prefork/worker/event.

Apache event MPM은 nginx와 비슷한 event-driven 모델. 성능 격차 좁혀짐.

14.2 Caddy

자동 HTTPS (Let's Encrypt 자동). Go 기반. 설정이 매우 단순:

example.com {
    reverse_proxy localhost:8080
}

한 줄. 개발자 친화적. 단, 성능이 nginx에 약간 뒤짐. 인기 상승 중.

14.3 Traefik

Kubernetes 친화적. Label 기반 자동 설정. Ingress controller로 인기.

성능은 nginx보다 떨어지지만 편의성이 큼. K8s 환경에서 nginx 대신 많이 선택.

14.4 HAProxy

L4 로드 밸런서의 강자. HTTP도 처리. 매우 빠른 reverse proxy. 단, 정적 파일 서빙은 약함. Nginx와 자주 조합 (HAProxy → nginx → backend).

14.5 Envoy

이 세션 Envoy 포스트 참고. 서비스 메시의 데이터 플레인. xDS 동적 설정, 더 현대적 모델. 복잡하지만 강력.

14.6 Cloudflare Pingora (오픈소스)

위에서 설명. 라이브러리 형태라 "직접 조립"이 필요.


15. 학습 리소스

공식:

:

  • "Nginx HTTP Server" — Clement Nedelcu.
  • "Mastering Nginx" — Dimitri Aivaliotis.
  • "Nginx Cookbook" — Derek DeJonghe.

블로그:

  • Igor Sysoev의 원조 발표 (러시아어 → 영어 번역).
  • Cloudflare 블로그 (성능 튜닝 실전).
  • Sysadmin 블로그들.

소스 코드:

  • src/core/ngx_cycle.c — 메인 이벤트 루프.
  • src/event/ngx_event.c — 이벤트 모듈.
  • src/http/ngx_http_core_module.c — Phase engine.

16. 요약 — 한 장 정리

┌─────────────────────────────────────────────────────┐
Nginx Cheat Sheet├─────────────────────────────────────────────────────┤
│ 역사:2004 Igor Sysoev, RussiaC10k 문제 해결                                      │
2011 Nginx Inc., 2019 F5 인수                      │
│                                                       │
│ 아키텍처:Master process (관리)Worker processes (요청 처리)Cache manager/loader                               │
Event-driven single thread per worker              │
│                                                       │
Event Loop:epoll (Linux), kqueue (BSD)Edge-triggered                                     │
Non-blocking I/OThread pool for blocking ops                       │
│                                                       │
│ 요청 Phases:POST_READREWRITEFIND_CONFIG│   → REWRITE(loc)PREACCESSACCESS│   → PRECONTENTCONTENTLOG   (11개 phases)│                                                       │
Upstream:│   proxy_pass, fastcgi_pass, uwsgi_pass              │
LB: round_robin, least_conn, ip_hash, hash        │
Keepalive 연결 pool                                 │
Retry, health check                                │
│                                                       │
│ 캐싱:│   proxy_cache_path                                    │
│   key, valid, inactive                               │
Microcaching (1s TTL)Cache lock (thundering herd 방지)│                                                       │
SSL:TLS 1.2/1.3, HTTP/2, HTTP/3 (experimental)Session cache + tickets                            │
OCSP stapling                                      │
│                                                       │
│ 모듈:Core: http, proxy, upstream, cache                 │
Third-party: pagespeed, lua, njs                  │
Dynamic modules (1.9.11+)OpenResty = nginx + LuaJIT│                                                       │
Graceful Reload:│   nginx -s reload                                    │
│   새 worker 시작 + 기존 worker graceful shutdown    │
Zero downtime                                      │
│                                                       │
│ 튜닝:│   worker_processes auto                              │
│   worker_connections 4096+│   sendfile on, tcp_nopush on                         │
│   keepalive_timeout                                  │
│                                                       │
│ 관측성:│   access_log with $request_time                      │
│   stub_status                                        │
VTS module                                         │
OpenTelemetry (1.24+)│                                                       │
│ 대안:Apache (older, .htaccess)Caddy (auto HTTPS, Go)Traefik (K8s native)HAProxy (L4 LB king)Envoy (service mesh)Pingora (Cloudflare Rust, 2024 OSS)└─────────────────────────────────────────────────────┘

17. 퀴즈

Q1. Nginx가 C10k 문제를 어떻게 해결했는가?

A. 프로세스/스레드-per-request 모델을 버리고 event-driven reactor를 채택. Apache prefork MPM은 요청마다 프로세스 하나를 사용해서, 각 프로세스가 2-8 MB 스택을 필요로 했다. 10,000 동시 연결 = 20-80 GB RAM — 물리적으로 불가능. Nginx의 해결: 고정된 수의 worker 프로세스(보통 CPU 코어 수)가 각자 단일 스레드로 epoll/kqueue 이벤트 루프를 돌린다. 한 worker가 수만 연결을 동시에 처리 — 모든 I/O가 non-blocking이므로 한 연결이 대기하는 동안 다른 연결을 처리. 메모리 사용: 4 workers × 수십 MB = 수백 MB로 10,000+ 연결. 이것이 가능한 것은 2002년 Linux 2.6의 epoll이 O(1) 이벤트 통지를 제공했기 때문. Igor Sysoev의 2004년 nginx는 epoll을 본격 활용한 초기 구현 중 하나였다. 이 아키텍처 결정 하나가 "C10k 문제"를 "과거의 문제"로 만들었다.

Q2. Nginx의 "phase handler" 시스템이 왜 우아한가?

A. 요청 처리를 선언적으로 조립 가능하게 만든다. 각 HTTP 요청은 11개 단계(POST_READ → REWRITE → FIND_CONFIG → ACCESS → CONTENT → LOG 등)를 순차로 거친다. 모듈은 자신이 관심 있는 phase에 handler를 등록한다 — rewrite 모듈은 REWRITE phase에, auth_basic은 ACCESS phase에, proxy_pass는 CONTENT phase에. 개발자가 location 블록에 여러 directive를 쓰면 nginx가 자동으로 올바른 순서로 실행한다. 개발자는 "언제" 신경 안 써도 된다. 이 설계의 결과: (1) Composability — 임의의 모듈을 조합해도 순서가 명확, (2) 확장성 — 새 모듈이 기존 것을 건드리지 않고 특정 phase에 끼어들 수 있음, (3) 디버깅 — 어느 phase에서 실패했는지 명확. Envoy의 filter chain (2017)과 Caddy의 middleware가 이 패턴을 복제한 것은 우연이 아니다. "파이프라인 아키텍처"라는 현대 네트워크 소프트웨어의 표준 패턴을 nginx가 2004년에 정립했다.

Q3. Nginx worker가 "단일 스레드"인데 어떻게 수만 연결을 처리하는가?

A. 모든 I/O가 non-blocking이고 epoll이 준비된 fd만 돌려준다. 전통 모델: 스레드가 recv()를 호출하면 데이터가 올 때까지 블록. 수만 연결 = 수만 스레드 필요. Nginx 모델: 소켓을 O_NONBLOCK으로 만들고 epoll에 등록. 이벤트 루프는 epoll_wait로 대기하다가 "fd 42가 읽을 수 있고, fd 153도 읽을 수 있고, fd 1024는 쓸 수 있다"는 목록을 받는다. 각 fd에 해당하는 handler를 순차 호출하여 가능한 만큼 read/write, 데이터가 부족하면 EAGAIN 반환 → 다음 fd로 이동. 한 연결이 대기 중인 동안에도 worker는 다른 연결을 즉시 처리. 결과: CPU가 거의 idle 없이 계속 일함. 핵심 제약: blocking syscall 절대 금지. 디스크 read도 blocking일 수 있어(페이지 캐시 miss) nginx 1.7+는 thread pool에 오프로드. 같은 원리로 Node.js, Redis, Haproxy도 작동한다. "비동기 I/O + 단일 스레드"가 현대 고성능 서버의 보편 공식.

Q4. Nginx의 "graceful reload"가 기술적으로 어떻게 작동하는가?

A. Master가 SIGHUP을 받으면 새 worker를 시작하고 기존 worker를 부드럽게 종료. 순서: (1) nginx -s reload가 master 프로세스에 SIGHUP을 보냄. (2) Master가 새 설정 파일을 파싱하고 검증 — 오류가 있으면 reload 취소, 기존 상태 유지. (3) 유효하면 master가 새 listening socket을 열고(이미 열린 것은 재사용), 새 worker 프로세스들을 fork. (4) 새 worker는 새 설정으로 accept 시작. (5) Master가 기존 worker들에 SIGQUIT 전송 — "현재 연결은 마저 처리하고 새 연결은 받지 마라". (6) 기존 worker는 listening socket을 닫아 새 연결 거부, 진행 중 연결만 마무리. (7) 모든 요청이 끝나면 기존 worker가 조용히 종료. 사용자 관점: 진행 중 요청은 성공, 새 요청은 새 설정으로 처리 — 요청 드롭 0. 이 패턴이 10년 이상 nginx를 프로덕션 친화적으로 만든 이유. Envoy의 hot restart(SCM_RIGHTS로 socket 전달)는 유사한 목적의 다른 구현 — 같은 문제, 다른 기법. "장시간 운영되는 프로덕션 서버의 설정 변경"이라는 고전적 문제를 nginx가 해결한 표준 방식.

Q5. Nginx의 캐시 기능에서 "microcaching"이 효과적인 이유는?

A. 순간 동시 요청의 thundering herd 흡수. 바이럴 게시물, 뉴스 속보, 화제의 제품 페이지는 수 초 동안 수천/수만 동시 요청을 받는다. 각 요청이 backend를 때리면 DB/API가 쓰러진다. Microcaching: TTL을 1-5초로 아주 짧게 설정 — 사용자 관점에서 캐시가 없어 보이지만 서버 관점에서 엄청난 보호. 예: 초당 10,000 RPS → 1초 캐시 → backend는 초당 1 request만 받음 (99.99% hit rate). 같은 페이지의 다음 10,000 요청은 1초 이내 nginx 메모리에서 즉시 응답. **proxy_cache_lock**과 결합하면 더 강력 — 여러 요청이 동시에 cache miss여도 한 요청만 backend로 나머지는 첫 요청의 결과를 기다린다. 1초 TTL로도 backend 부하가 100x 감소할 수 있다. 단점: 1초간 stale 데이터 — 하지만 대부분 콘텐츠에서 허용 가능. "aggressive caching with tiny TTL"이 CDN/reverse proxy의 숨은 무기. Cloudflare, Fastly 같은 CDN도 같은 원리. Microcaching의 발명자는 사실 2000년대 말 Kazakhstan의 nginx 운영자로 알려져 있다.

Q6. Cloudflare가 Pingora를 만든 이유는?

A. 극한 스케일에서의 nginx 한계. Cloudflare는 하루 수 조 요청을 처리 — nginx가 훌륭하지만 몇 가지 근본 제약에 도달했다. (1) Shared-nothing worker 모델의 한계: 한 worker가 바쁘고 다른 worker가 한가해도 작업 이동 불가 — 연결이 worker에 고정. Cloudflare의 극도로 불균일한 부하에서 이것이 비효율. (2) C 기반 메모리 안전성: 수십 년의 nginx 코드베이스에도 여전히 메모리 안전 bug. (3) Upstream 연결 재사용: nginx의 keepalive가 연결을 연결-당-worker에만 제한. (4) Async 모델: 단일 스레드 이벤트 루프 + thread pool 보조는 특정 패턴에 비효율. Pingora의 답: Rust + Tokio. (1) Work-stealing 스케줄러 — Tokio task가 어느 스레드에서든 실행 가능, 한가한 스레드가 바쁜 스레드의 작업 훔쳐옴. (2) 메모리 안전 — Rust의 타입 시스템으로 buffer overflow/use-after-free 불가. (3) 전역 connection pool — 여러 스레드/요청이 같은 backend 연결 공유. (4) Async/await — 코드가 읽기 쉽고 확장 가능. 결과 (2022 Cloudflare 블로그): CPU 70% 감소, 메모리 67% 감소, 메모리 안전 bug 0. 2024년 오픈소스화. 대부분 사용자에게 nginx는 여전히 정답이지만 극한 스케일에서는 새 세대의 도구가 나왔다. 이것이 "성숙한 기술도 언젠가는 재작성된다"는 소프트웨어 진화의 자연 법칙.

Q7. OpenResty가 nginx와 다른 점은?

A. Nginx + LuaJIT + 모듈 번들 = 프로그래밍 가능한 고성능 플랫폼. 일반 nginx 설정은 정적 directive로 제한된다 — 복잡한 로직(조건부 라우팅, 외부 API 호출, DB 조회)은 fastcgi/proxy_pass로 외부 프로세스에 위임해야 한다. OpenResty (Agentzh/Yichun Zhang): nginx에 LuaJIT를 내장해서 phase handler를 Lua 코드로 작성 가능. 예: content_by_lua_block { ngx.say("Hello") } 로 즉시 HTTP 엔드포인트 생성. 더 강력한 예: Redis에 연결, DB 쿼리, JWT 검증, rate limiting 커스텀 로직 — 모두 Lua로 nginx 안에서. 장점: (1) 성능 — LuaJIT은 Lua를 native code로 컴파일, nginx의 단일 스레드 이벤트 루프에서 실행되므로 수천 req/s 처리 가능, (2) Upstream 없이 복잡한 로직, (3) 이벤트 루프 통합 — Lua 코드도 async, 다른 연결을 막지 않음. 사용 사례: Kong API Gateway (OpenResty 기반), Alibaba의 tengine, Cloudflare의 초기 WAF, Rokt의 HTTP 라우팅. 최신 대안 njs (NGINX JavaScript)는 JavaScript로 같은 일을 하지만 LuaJIT만큼 성숙하지 않다. OpenResty의 창시자 Agentzh는 중국에서 시작해 미국으로 이주, 전용 회사 OpenResty Inc. 운영. "nginx를 애플리케이션 서버로" 변환한 프로젝트.


이 글이 도움이 됐다면 다음 포스트도 확인해 보세요:

  • "Envoy Proxy Internals Deep Dive" — 현대 서비스 메시의 프록시.
  • "Linux Network Stack Deep Dive" — nginx가 활용하는 epoll과 커널 I/O.
  • "CDN & Edge Caching Strategies" — nginx 캐싱의 확장.
  • "HTTP/3 & QUIC Deep Dive" — nginx 1.25의 실험적 지원.