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
-
- 설정 파일을 읽고 검증한다
- 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 — 설정 리로드 (위 절차)
-
SIGQUIT — graceful 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)