nginx
nginx 는 웹 요청을 받아 주는 서버입니다. 자기 디스크에 있는 파일을 내주기도 하고, 뒤에 선 다른 서버에 요청을 넘겨 그 답을 대신 돌려주기도 합니다. 프로세스 하나가 연결을 여러 개 동시에 붙들고 돌아가며 처리합니다.
쉽고 빠른 이해
nginx는 요청을 받아 처리하는 서버입니다. 이미지 같은 정적 파일은 자기 디스크에서 바로 내주고, 나머지 요청은 뒤쪽 서버로 넘겨 그 답을 대신 돌려줍니다.
연결마다 프로세스나 스레드를 하나씩 새로 두면 그 수만큼 메모리와, 만들고 없애는 데 드는 처리 시간이 듭니다. 동시 연결이 수천 개로 늘면 이 비용을 감당하기 어려워집니다. 그 대신 nginx는 미리 띄운 워커 프로세스 몇 개가 연결을 여러 개씩 동시에 맡아 돌아가며 처리합니다.
- 마스터 프로세스 하나가 설정을 읽고 워커 프로세스 여럿을 띄웁니다.
- 워커 프로세스가 실제 요청을 처리합니다. 하나가 동시에 여러 연결을 붙듭니다.
- 설정을 바꾸면 새 워커를 띄우고, 옛 워커는 하던 일을 마저 끝낸 뒤 물러납니다.
대가도 있습니다. 워커 안에서 시간이 걸리는 연산이 그 워커를 붙잡으면, 같은 워커가 맡은 다른 연결도 같이 기다립니다. 정적 파일처럼 금방 끝나는 처리는 이 대가가 잘 안 보이지만, 시간이 걸리는 처리가 잦으면 그 지연이 같은 워커의 다른 연결로 번집니다. 응답도 기본으로 버퍼에 모았다가 내보내므로, 받는 즉시 그대로 흘려보내지는 않습니다.
상세
nginx 는 스스로를 HTTP(HyperText Transfer Protocol) 웹 서버이자 리버스 프록시, 콘텐츠 캐시, 로드밸런서, TCP(Transmission Control Protocol)·UDP(User Datagram Protocol) 프록시 서버, 메일 프록시 서버라고 소개합니다. 이름은 "engine x" 에서 왔습니다. Igor Sysoev 가 처음 썼습니다.
한 물건이 여러 자리를 겸합니다. 앞에 세워 두면 정적 파일을 내주는 웹 서버입니다. 같은 설정 파일 안에서 뒤쪽 애플리케이션 서버로 요청을 넘기면 리버스 프록시입니다. 그 응답을 디스크에 받아 두면 프록시 캐시입니다. 뒤쪽 서버가 여럿이면 로드밸런서입니다.
마스터와 워커
nginx 에는 마스터 프로세스 하나와 워커 프로세스 여럿이 있습니다. 마스터 프로세스의 주된 목적은 설정을 읽고 검사하는 것, 그리고 워커 프로세스들을 유지하는 것입니다. 요청을 실제로 처리하는 쪽은 워커 프로세스입니다.
nginx 는 이벤트 기반 모델과 운영체제마다 다른 기법을 써서 워커 프로세스들 사이에 요청을 배분합니다.
워커 프로세스의 개수는 설정 파일의 worker_processes 지시자로 정합니다. 주어진 설정에 고정해 둘 수도 있고, 가용
CPU(Central Processing Unit) 코어 개수에 맞춰 자동으로 조정되게 할 수도 있습니다. 워커 프로세스
하나가 동시에 열 수 있는 연결의 최대 수는 worker_connections 로 정하고 기본값은 512 입니다.
flowchart TD
M["마스터 프로세스"] --> W1["워커 프로세스 1"]
M --> W2["워커 프로세스 2"]
M --> W3["워커 프로세스 N (worker_processes)"]
연결 처리 방식
워커 프로세스가 동시에 붙든 연결 여러 개 중 무엇이 준비됐는지는 워커가 하나씩 물어보지 않고
운영체제에게 한 번에 물어봅니다. 연결 처리 방식은 이 물음을 운영체제에 넘기는 방법입니다.
nginx 는 여러 가지 연결 처리 방식을 지원합니다. 어떤 방식을 쓸 수 있는지는 플랫폼에 달렸습니다.
여러 방식을 지원하는 플랫폼에서는 nginx 가 대개 가장 효율적인 방식을 자동으로 고릅니다. 필요하면
지시자(설정 파일에 한 줄로 적는 설정 항목) 중 use 로 명시할 수 있지만, 문서는 대개 명시할
필요가 없다고 적습니다.
| 방식 | 문서가 적는 내용 |
|---|---|
| select · poll | 표준 방식. 더 효율적인 방식이 없는 플랫폼에서 지원 모듈이 자동으로 빌드됩니다 |
| kqueue | FreeBSD 4.1+ · OpenBSD 2.9+ · NetBSD 2.0 · macOS |
| epoll | Linux 2.6+. EPOLLRDHUP 과 EPOLLEXCLUSIVE 플래그는 1.11.3 부터 지원합니다 |
| /dev/poll | Solaris 7 11/99+ · HP(Hewlett Packard)/UX 11.22+ · IRIX(운영체제) 6.5.15+ · Tru64 UNIX(운영체제) 5.1A+ |
| eventport | Solaris 10+. 알려진 문제 때문에 /dev/poll 을 대신 쓰기를 권합니다 |
포기한 것
연결마다 하나씩 두는 실행 흐름
nginx 는 연결 하나마다 프로세스나 스레드를 새로 만드는 실행 흐름을 포기합니다. 프로세스나 스레드를 하나 새로 만들 때마다 그 몫의 메모리와, 만들고 없애는 데 드는 CPU 시간이 듭니다. 동시 연결이 수천 개로 늘면 이 비용이 연결 수만큼 쌓여 감당하기 어려워집니다.
대신 워커 프로세스 하나가 동시 연결을 여러 개 엽니다. worker_connections 가 그 상한이고 기본값은
512 입니다.
여기에 대가가 따라옵니다. 워커를 붙잡는 연산이 생길 수 있습니다. 파일을 읽는 일이 그렇습니다. 정적
파일처럼 금방 끝나는 읽기는 이 대가가 잘 안 보이지만, 워커가 붙잡히면 그 워커가 맡은 다른 연결들도
함께 기다리게 됩니다. 비동기 파일 입출력을 켜는 aio 의 기본값은 off 입니다.
리눅스에서는 커널 2.6.22 부터 쓸 수 있습니다. aio 를 켰어도 파일을 운영체제 캐시 없이 디스크에서
바로 읽게 하는 directio 를 같이 켜야 합니다. 그러지 않으면 읽기가 블로킹된다고 문서가 적습니다.
aio 는 on·off 말고 threads[=pool] 값도 받습니다. 이 값을 주면 읽기를 스레드 풀로
넘깁니다. 1.7.11 부터 thread_pool 지시자로 그 풀을 정의합니다. 문서는 이 스레드 풀을 워커
프로세스를 블로킹하지 않고 파일을 읽고 보내기 위한 것이라고 설명합니다. 기본 풀은
thread_pool default threads=32 max_queue=65536; 입니다. 풀의 모든 스레드가 바쁘면 새 작업은
큐에서 기다리고, 큐가 넘치면 그 작업은 오류로 끝납니다.
파일을 읽는 요청 하나가 이 갈림길 중 어디로 가는지는 이렇게 갈립니다.
flowchart TD
A["파일을 읽어야 한다"] --> B{"aio 설정"}
B -- "off(기본값)" --> C["블로킹 읽기 — 워커가 붙잡힌다"]
B -- "on" --> H{"directio 도 켜져 있나"}
H -- "아니오" --> C
H -- "예" --> I["비동기로 읽는다 — 워커가 안 붙잡힌다"]
B -- "threads[=pool]" --> D{"스레드 풀 상태"}
D -- "여유 있음" --> E["스레드가 맡아 바로 처리한다"]
D -- "다 바쁨, 큐에 자리 있음" --> F["큐에서 기다리다 처리된다"]
D -- "다 바쁨, 큐도 참" --> G["오류로 끝난다"]
오픈소스판의 능동 헬스체크
nginx 는 누구나 무료로 받는 오픈소스판과, 값을 내고 쓰는 상업 구독으로 나뉩니다. 이 문서가 다루는 것은 오픈소스판입니다.
nginx 는 지금까지 나온 뒤쪽 서버를 업스트림이라는 이름으로도 부릅니다. proxy_pass 같은
지시자가 가리키는 서버 묶음이 업스트림이고, 뒤에 나오는 「프록시 대상 서버」도 같은 대상을
가리킵니다.
업스트림 모듈 문서는 서버 목록을 재시작 없이 바꿀 수 있고 주기적으로 상태를 확인하는
헬스체크가 붙은 그룹을 상업 구독의 일부로 제공한다고 적습니다. nginx 가 스스로 나서서 서버에
요청을 보내 살아 있는지 미리 찔러보는 방식이라 이 문서는 이를 능동 헬스체크라 부릅니다.
health_check 지시자를 쓰면 이 검사가 켜집니다. 오픈소스판은 이 주기적 능동 헬스체크를
포기합니다.
기본값이 켜짐인 버퍼링
proxy_buffering 의 기본값은 on 입니다. 버퍼링이 켜져 있으면 nginx 는 프록시 대상 서버의 응답을
가능한 한 빨리 받아 proxy_buffer_size 와 proxy_buffers 가 정한 버퍼에 저장합니다. 응답 전체가
메모리에 들어가지 않으면 일부가 디스크의 임시 파일에 저장될 수 있습니다.
요청 쪽도 같습니다. proxy_request_buffering 의 기본값도 on 이고, 1.7.11 부터 있습니다.
버퍼링이 켜져 있으면 요청 본문 전체를 클라이언트에서 다 읽은 뒤에 프록시 대상 서버로 보냅니다.
끄면 받는 대로 즉시 보냅니다. 대신 문서는 그 경우 nginx 가 이미 요청 본문을 보내기 시작했다면
그 요청을 다음 서버로 넘길 수 없다고 적습니다. 버퍼링을 켜 두고 얻는 것이 그 넘길 수 있음입니다.
sequenceDiagram
participant 클라이언트
participant nginx
participant 프록시 대상 서버
클라이언트->>nginx: 요청 본문을 보낸다
alt proxy_request_buffering on(기본값)
Note over nginx: 본문 전체를 다 받을 때까지 기다린다
nginx->>프록시 대상 서버: 본문을 다 받은 뒤에 보낸다
Note over nginx: 실패하면 다른 서버로 다시 보낼 수 있다
else proxy_request_buffering off
nginx->>프록시 대상 서버: 받는 대로 즉시 보낸다
Note over nginx: 이미 보내기 시작했으면 다른 서버로 못 넘긴다
end
예시
리버스 프록시 최소 설정
server {
location / {
proxy_pass http://localhost:8080/;
}
location ~ \.(gif|jpg|png)$ {
root /data/images;
}
}
입문 안내서가 프록시 서버 설정의 결과 형태로 싣는 설정입니다. .gif·.jpg·.png 로 끝나는
요청을 걸러 root 지시자의 인자로 준 /data/images 에 요청의 URI(Uniform Resource Identifier)
를 이어 붙인 경로로 매핑하고, 나머지 요청은 전부 위에서 설정한 프록시 대상 서버로 넘깁니다.
proxy_pass 의 인자에는 프록시 대상 서버의 프로토콜과 이름과 포트를 적습니다.
부하 분산
http {
upstream myapp1 {
server srv1.example.com;
server srv2.example.com;
server srv3.example.com;
}
server {
listen 80;
location / {
proxy_pass http://myapp1;
}
}
}
srv1 부터 srv3 까지 같은 애플리케이션의 인스턴스 3개가 돕니다. 모든 요청이 서버 그룹 myapp1 로
프록시되고 nginx 가 HTTP 부하 분산을 적용합니다. 분배 방식을 따로 설정하지 않으면 round-robin 이
기본값입니다.
| 방식 | 문서가 적는 내용 |
|---|---|
| round-robin | 요청을 애플리케이션 서버들에 차례대로 배분합니다 |
| least-connected | 활성 연결 수가 가장 적은 서버에 다음 요청을 배정합니다 |
| ip-hash | 클라이언트의 [[IP 주소 |
upstream 블록 안의 server 지시자에는 파라미터가 붙습니다. 업스트림 모듈 문서의 예제는
server backend1.example.com weight=5; 로 가중치를, server backup1.example.com:8080 backup;
으로 예비 서버를, server unix:/tmp/backend3; 로 유닉스 소켓을 보여 줍니다.
프록시 캐시
proxy_cache_path /data/nginx/cache levels=1:2 keys_zone=one:10m;
proxy_cache_valid 200 302 10m;
proxy_cache_valid 404 1m;
proxy_cache_path 는 캐시의 경로와 파라미터를 정합니다. 캐시 데이터는 파일로 저장되고, 캐시 안의
파일 이름은 캐시 키에 MD5(Message-Digest Algorithm 5) 함수를 적용한 결과입니다. levels 는
그 파일 이름 앞에 몇 단계의 하위 디렉토리를 두고 각 디렉토리 이름을 몇 글자로 할지 정합니다. 1
부터 3 까지이고 각 수준은 1 이나 2 를 받습니다. 디렉토리 이름은 해시값의 맨 끝에서부터 그
글자 수만큼씩 떼어 만듭니다. 위 설정(levels=1:2)에서 해시값이 ...4809d65029c 로 끝나면 끝
1 글자인 c 가 첫 단계 디렉토리, 그 앞 2 글자인 29 가 둘째 단계 디렉토리가 되어 캐시 안의
파일 이름은 /data/nginx/cache/c/29/b7f54b2df7773722d382f4809d65029c 처럼 됩니다.
proxy_cache_valid 는 응답 코드별 캐시 시간을 정합니다. 위 두 줄은 코드 200 과 302 인 응답을
10분, 코드 404 인 응답을 1분 캐시합니다. 캐시 시간만 적어 proxy_cache_valid 5m; 으로 두면
200·301·302 응답만 캐시됩니다.
사용처
Kubernetes 의 ingress-nginx 컨트롤러. 이 컨트롤러의 목표는 설정 파일 nginx.conf 를 조립하는
것이라고 문서가 밝힙니다. 그래서 설정 파일이 바뀔 때마다 nginx 를 리로드해야 한다는 요구가 따라
붙습니다. 다만 업스트림 설정에만 영향을 주는 변경, 예컨대 앱을 배포해 엔드포인트가 바뀌는 경우에는
리로드하지 않는다고 적습니다. 그 자리에는 lua-nginx-module 을 씁니다. nginx 가 요청을 실제로
나르는 자리에 앉고, 컨트롤러는 그 설정을 써 주는 자리에 앉습니다.
OpenResty. 스스로를 NGINX 와 LuaJIT 을 기반으로 한 동적 웹 플랫폼이라고 적습니다. nginx 를 그대로 두고 그 위에 Lua 실행 환경을 얹은 자리입니다.
Kong Gateway. Kong 문서는 Nginx 에서 Lua 개발이 lua-nginx-module 로 가능해진다고 적습니다. 그 모듈을 넣어 Nginx 를 직접 컴파일하는 대신, Kong Gateway 는 lua-nginx-module 을 포함한 OpenResty 와 함께 배포됩니다. 문서는 OpenResty 가 Nginx 의 포크가 아니라 그 기능을 확장하는 모듈 묶음이라고 못 박습니다. Kong Gateway 자체는 플러그인이라 부르는 Lua 모듈을 적재하고 실행하도록 설계된 Lua 애플리케이션입니다.
운영
워커 수와 연결 수
| 지시자 | 기본값 | 문서가 적는 내용 |
|---|---|---|
worker_processes |
worker_processes 1; |
워커 프로세스의 개수. auto 를 주면 자동 탐지를 시도합니다 |
worker_connections |
worker_connections 512; |
워커 프로세스 하나가 열 수 있는 동시 연결의 최대 수 |
worker_rlimit_nofile |
없음 | 워커 프로세스의 열린 파일 최대 개수 제한(RLIMIT_NOFILE)을 바꿉니다 |
worker_processes 의 최적값은 CPU 코어 개수, 데이터를 저장한 하드디스크 개수, 부하 패턴을 비롯한
여러 요인에 달렸다고 문서가 적습니다. 헷갈릴 때는 가용 CPU 코어 개수로 두는 것을 시작점으로 듭니다.
auto 파라미터는 1.3.8 과 1.2.5 부터 지원합니다.
worker_connections 를 볼 때 염두에 둘 것이 둘이라고 문서가 적습니다. 첫째, 이 숫자에는 모든
연결이 들어갑니다. 클라이언트와의 연결만이 아니라 프록시 대상 서버와의 연결도 포함됩니다. 둘째,
실제 동시 연결 수는 현재의 열린 파일 최대 개수 제한을 넘을 수 없습니다. 그 제한을 만지는 손잡이가
worker_rlimit_nofile 이고, 메인 프로세스를 재시작하지 않고 상한을 올리는 데 씁니다.
설정을 바꾸는 자리
설정 파일을 다시 읽게 하려면 마스터 프로세스에 HUP(Hangup) 시그널을 보냅니다. 마스터 프로세스의 프로세스
ID 는 기본적으로 /usr/local/nginx/logs/nginx.pid 파일에 적힙니다. 이 이름은 설정 시점에 바꾸거나
nginx.conf 의 pid 지시자로 바꿀 수 있습니다.
마스터 프로세스는 먼저 문법이 유효한지 확인하고, 그다음 새 설정을 적용해 봅니다. 로그 파일과 새 listen 소켓을 여는 일입니다. 이것이 실패하면 변경을 되돌리고 옛 설정으로 계속 동작합니다. 성공하면 새 워커 프로세스들을 시작하고, 옛 워커 프로세스들에는 우아하게 종료하라는 메시지를 보냅니다. 옛 워커 프로세스들은 listen 소켓을 닫고 이미 붙어 있던 클라이언트를 계속 서비스합니다. 모든 클라이언트가 처리되고 나면 옛 워커 프로세스들이 종료됩니다.
flowchart TD
A["마스터가 HUP 을 받는다"] --> B{"설정 문법이 맞나"}
B -- 아니오 --> E["옛 설정으로 계속 돈다"]
B -- 예 --> C{"로그 파일과 새 listen 소켓을 열었나"}
C -- 아니오 --> E
C -- 예 --> D["새 워커를 띄우고 옛 워커에 우아한 종료를 요청한다"]
D --> F["옛 워커가 기존 클라이언트를 마저 서비스한 뒤 종료한다"]
그 마지막 단계에 시간 제한을 거는 것이 worker_shutdown_timeout 입니다. 1.11.11 부터 있고
기본값은 없습니다. 시간이 다 되면 nginx 는 종료를 진행하려고 현재 열려 있는 모든 연결을 닫으려
시도합니다.
마스터 프로세스가 받는 시그널은 일곱입니다.
| 시그널 | 하는 일 |
|---|---|
| TERM(Terminate)·INT(Interrupt) | 즉시 종료. 문서 표기는 fast shutdown 입니다 |
| QUIT(Quit) | 우아한 종료. 문서 표기는 graceful shutdown 입니다 |
| HUP | 설정 변경. 새 설정으로 워커를 새로 띄우고 옛 워커를 우아하게 종료합니다 |
| USR1(User-defined signal 1) | 로그 파일 다시 열기 |
| USR2(User-defined signal 2) | 실행 파일 업그레이드 |
| WINCH(Window change) | 워커 프로세스의 우아한 종료 |
요청 본문 상한
client_max_body_size 의 기본값은 1m 입니다. 요청의 크기가 설정값을 넘으면 413(Request Entity
Too Large) 오류가 클라이언트에게 돌아갑니다. 문서는 브라우저가 이 오류를 제대로 표시하지 못한다는
점을 함께 적습니다. 크기를 0 으로 두면 요청 본문 크기 검사를 끕니다.
관련 항목
겸하는 역할
리버스 프록시 · 콘텐츠 캐시 · 로드밸런서 · 메일 프록시 서버
프로세스와 연결 처리
마스터 프로세스 · 워커 프로세스 · 스레드 풀 · 이벤트 기반 모델 · epoll · kqueue · 비동기 파일 입출력 · 블로킹
부하를 나누는 방식
부하 분산 · round-robin · least-connected · ip-hash · 업스트림 · 헬스체크
위에 얹히는 도구
Kubernetes · ingress-nginx · OpenResty · Lua · lua-nginx-module · Kong Gateway
시그널이 부르는 동작
즉시 종료 · 우아한 종료 · 로그
다른 이름: engine x