웹 서버·WAS 학습 노트 목차

nginx는 어떻게 만 개의 연결을 감당하나 — 프로세스 모델

설정을 외우기 전에 nginx 가 어떤 모양의 프로그램인지를 알아야 한다. 튜닝 값의 의미도, 리로드가 왜 무중단인지도 전부 여기서 나온다.


1. 마스터 하나, 워커 여럿

$ ps -ef | grep nginx
root     1234     1  nginx: master process /usr/sbin/nginx
nginx    1235  1234  nginx: worker process
nginx    1236  1234  nginx: worker process
nginx    1237  1234  nginx: cache manager process
  • 마스터 프로세스root 로 뜬다

    • 설정 파일을 읽고 검증한다
    • 80·443 같은 특권 포트를 bind 한다 (1024 미만은 root 만 가능)
    • 워커를 낳고 관리한다
    • 시그널을 받는다 (리로드·재시작·로그 회전)
    • 요청을 직접 처리하지 않는다
  • 워커 프로세스 — 비특권 사용자로 낮춰서 돈다

    • 실제 연결을 처리하는 것은 전부 여기다
    • 서로 독립적이다. 하나가 죽어도 마스터가 다시 띄운다

왜 나눠 놨나 — 포트 bind 에는 root 가 필요하지만, 외부 입력을 다루는 코드는 root 로 돌면 안 된다. 권한이 필요한 일만 마스터가 먼저 끝내고, 위험한 일은 권한을 낮춘 워커가 맡는다. 권한 분리의 교과서적 배치다.

2. 워커 하나가 수천 연결을 다룬다

핵심은 연결 하나에 스레드 하나를 쓰지 않는다는 점이다.

다이어그램 로딩 중…
  • 전통적 방식 — 연결마다 스레드를 만들고, 그 스레드가 read() 에서 블로킹된다

    • 대기 중인 연결도 스레드·스택을 점유한다
  • nginx — 커널에게 "이 중 준비된 게 생기면 알려 줘" 라고 등록해 두고

    • 준비된 것만 골라 처리한다
    • 대기 중인 연결은 자료구조 몇 KB 만 차지한다

이 "준비된 것만 알려 주는" 커널 기능이 리눅스에서는 epoll, BSD·macOS 에서는 kqueue 다. nginx 는 플랫폼에 맞는 것을 자동으로 고른다.

$ nginx -V 2>&1 | tr ' ' '\n' | grep -i epoll

3. 그래서 튜닝 값이 이런 모양이다

worker_processes  auto;          # 보통 CPU 코어 수
worker_rlimit_nofile 65535;      # 워커당 파일 디스크립터 한도

events {
    worker_connections 4096;     # 워커 하나가 동시에 다룰 연결 수
    multi_accept on;             # 준비된 연결을 한 번에 여러 개 수락
}

이론상 최대 연결 수

worker_processes × worker_connections

주의 — 이 값은 '연결' 수이지 '클라이언트' 수가 아니다

  • 정적 파일만 주는 경우 — 클라이언트 연결 1개 = 연결 1개
  • 리버스 프록시인 경우 — 클라이언트 연결 1개 + 업스트림 연결 1개 = 2개
    • 실질 동시 사용자 ≈ (worker_processes × worker_connections) / 2

이 나눗셈을 빠뜨리는 것이 가장 흔한 계산 실수다

파일 디스크립터가 진짜 한도다

연결 하나 = 소켓 하나 = 파일 디스크립터 하나 로그 파일 · 열어 둔 정적 파일도 디스크립터를 쓴다

worker_connections 를 아무리 키워도 OS 한도(ulimit -n)나 worker_rlimit_nofile 이 낮으면 거기서 막힌다

  • 증상 — error log 에 "Too many open files" (EMFILE)
    • accept 가 실패하며 연결이 거부된다
  • 확인 — cat /proc/<worker pid>/limits | grep 'open files'
    • systemd 로 띄웠다면 LimitNOFILE 도 함께 본다

worker_rlimit_nofile ≥ worker_connections + 여유(로그·업스트림·캐시) 가 기본 규칙이다

worker_processes auto 는 CPU 코어 수로 잡는다. 코어보다 많이 띄우면 컨텍스트 스위치만 늘어난다. CPU 를 쓰는 일(TLS 핸드셰이크·gzip)이 많으면 코어 수가 곧 상한이라는 뜻이기도 하다.

4. 이벤트 모델의 대가 — 블로킹하면 그 워커가 멈춘다

워커 하나가 4,000 연결을 돌보고 있는데 그중 한 처리에서 디스크를 동기로 읽느라 50ms 를 쓰면 나머지 3,999 연결이 그 50ms 동안 아무 진행도 못 한다

nginx 가 조심하는 지점이 여기다.

  • 무거운 계산을 워커에서 하지 않는다 (그래서 애플리케이션은 뒤로 보낸다)

  • 디스크 읽기가 느릴 수 있는 환경에서는 스레드 풀로 뺀다

    • aio threads;

    • sendfile on;

    • 캐시에 없는 큰 파일을 읽을 때 워커가 막히는 것을 막는다

sendfile 은 파일을 유저 공간으로 올리지 않고 커널 안에서 소켓으로 바로 보낸다. 복사 횟수와 모드 전환이 줄어 정적 파일 전송이 크게 빨라진다.

5. 설정 리로드가 왜 무중단인가

$ nginx -s reload (또는 kill -HUP <master pid>)

  • ① 마스터가 새 설정을 읽고 검증한다

    • 문법이 틀리면 여기서 실패하고 아무것도 바꾸지 않는다 ← 중요
  • ② 검증에 성공하면 새 설정으로 새 워커를 띄운다

  • ③ 기존 워커에게 "새 연결은 그만 받아라" 를 알린다

  • ④ 기존 워커는 처리 중인 요청을 끝까지 마치고 스스로 종료한다

  • 떨어지는 연결이 없다

  • 실패하면 아무 일도 일어나지 않는다 (원자적이다)

배포 전에는 항상 문법 검사를 먼저 한다.

$ nginx -t
nginx: configuration file /etc/nginx/nginx.conf test is successful

주의 — -t 는 '문법' 만 본다

  • upstream 이 실제로 살아 있는지, 인증서 파일이 읽히는지는
  • 리로드 시점에 드러난다

종료 시그널의 차이

  • SIGHUP설정 리로드 (위 절차)

  • SIGQUITgraceful shutdown — 처리 중인 요청을 끝내고 종료

  • SIGTERM — fast shutdown — 즉시 종료. 진행 중 요청이 끊긴다

  • SIGUSR1 — 로그 파일 다시 열기 (로그 회전 후 호출)

  • SIGUSR2 — 실행 파일 교체 — 바이너리 업그레이드를 무중단으로

  • 컨테이너에서는 기본 정지 시그널이 SIGTERM 이다

  • 그대로 두면 배포마다 진행 중 요청이 잘린다

  • STOPSIGNAL SIGQUIT 를 지정하거나 종료 훅에서 quit 를 보낸다

이것이 인프라에서 반복되는 패턴이다 — 정지 시그널을 어떻게 받느냐가 무중단 배포의 절반이다. 뒤의 WAS 편에서 같은 주제가 다시 나온다.

6. 로그 회전에서 SIGUSR1 이 필요한 이유

로그 파일을 mv 로 옮겨도 워커는 계속 '옛 파일' 에 쓴다

  • 프로세스가 들고 있는 것은 파일 이름이 아니라 열린 디스크립터다
  • 옮겨진 파일이 계속 커지고, 새 파일은 비어 있다
  • 디스크가 차는데 로그 파일은 작아 보인다 (원인 못 찾는 전형)

절차

  • ① mv access.log access.log.1
  • ② nginx -s reopen (= SIGUSR1)
  • ③ 그제야 워커가 새 access.log 를 연다

logrotate 설정의 postrotate 에 이 명령이 들어가는 이유다

7. 최신 상황

  • 버전 — nginx.org/en/CHANGES 에 날짜와 함께 전부 나열된다
    • 2026-07-15 기준 최신은 1.31.3
  • HTTP/3 — 1.25.0 부터 실험적 지원 → 이후 CUBIC 혼잡 제어 · 0-RTT 등이 추가됐다
    • 빌드에 QUIC 지원이 포함돼야 한다
  • 동적 모듈 1.9.11 부터 지원. 모듈을 다시 컴파일하지 않고 load_module 로 붙인다
  • TLSv1.3 — 1.23.4 부터 기본 활성

인프라에서는 "지금 우리가 쓰는 버전이 몇인가" 가 곧 사실 관계다 문항이든 문서든 버전을 함께 적는다


한눈에 정리

  • 마스터root · 포트 bind · 설정 검증 · 워커 관리. 요청은 안 다룬다
  • 워커 — 비특권 · 이벤트 루프로 수천 연결. 여기가 실제 처리
  • epoll/kqueue — "준비된 것만 알려 줘" — 연결당 스레드를 안 쓰는 근거
  • 최대 연결 — worker_processes × worker_connections
    • 프록시면 연결이 2배로 든다 (클라이언트 + 업스트림)
  • 진짜 한도 — 파일 디스크립터. worker_rlimit_nofile · ulimit -n 을 함께 올린다
  • 블로킹 금지 — 워커가 막히면 그 워커의 모든 연결이 멈춘다 → aio threads
  • 리로드 — 검증 후 새 워커 → 기존 워커는 마무리 후 종료. 실패하면 무변경
  • 시그널 — HUP 리로드 · QUIT 우아한 종료 · TERM 즉시 · USR1 로그 재개방

출처 — nginx docs: Beginner's Guide · "Controlling nginx" · ngx_core_module(worker_processes · worker_connections · worker_rlimit_nofile) · ngx_http_core_module(sendfile · aio) · nginx CHANGES (1.31.3, 2026-07-15)

웹 서버란 무엇인가 — WAS와의 역할 분담설정 읽기 — server·location·upstream