트래픽이 갑자기 10배로 폭증하는 상황을 상상해보세요. 평소에는 문제없이 돌아가던 서비스가 순식간에 500 에러와 응답 지연으로 무너집니다. 원인의 상당수는 애플리케이션 코드가 아니라 그 앞에 놓인 Nginx 리버스 프록시의 기본값 설정에 있습니다. Nginx는 기본 설정만으로도 훌륭하지만, 실무 트래픽에 맞게 튜닝하지 않으면 커넥션 큐 포화, 과도한 컨텍스트 스위칭, 불필요한 CPU 낭비로 병목이 생깁니다. 이 글에서는 리버스 프록시로 쓰이는 Nginx를 실제 수치를 근거로 단계적으로 튜닝하는 방법을 설명합니다.
worker_processes와 worker_connections: 동시 처리 한계 설정
Nginx는 멀티프로세스 + 이벤트 기반(비동기 I/O) 아키텍처입니다. 각 워커 프로세스는 단일 스레드로 수천 개의 커넥션을 이벤트 루프로 처리합니다. 따라서 첫 번째 튜닝 지점은 워커 수와 커넥션 수입니다.
# /etc/nginx/nginx.conf — 워커 및 이벤트 블록
# CPU 코어 수와 동일하게 설정하거나 auto 사용
# auto는 Nginx 1.2.5+에서 nproc 값을 자동 감지
worker_processes auto;
# 워커 프로세스가 열 수 있는 파일 디스크립터 수
# ulimit -n 값과 일치시킬 것 (기본 1024는 너무 낮음)
worker_rlimit_nofile 65536;
events {
# 워커 하나가 동시에 처리할 수 있는 커넥션 수
# 최대 동시 접속 수 = worker_processes × worker_connections
# 4코어 서버에서 4 × 4096 = 최대 16,384 커넥션
worker_connections 4096;
# Linux epoll이 기본값이지만 명시적으로 선언하면 안전
use epoll;
# 하나의 accept 호출로 가능한 많은 커넥션을 수락 (thundering herd 방지)
multi_accept on;
}
OS 레벨에서도 파일 디스크립터 한계를 올려야 합니다.
# /etc/security/limits.conf 또는 systemd 서비스 파일에 추가
nginx soft nofile 65536
nginx hard nofile 65536
# systemd 환경이라면 /etc/systemd/system/nginx.service.d/override.conf
[Service]
LimitNOFILE=65536
# 변경 후
systemctl daemon-reload && systemctl restart nginx
4코어 서버에서 worker_processes auto와 worker_connections 4096 조합이면 이론상 16,384개의 동시 커넥션을 처리할 수 있습니다. 단, 메모리도 고려해야 합니다. 커넥션 하나당 약 10~50KB의 메모리를 소비하므로 16K 커넥션이면 최소 160MB~800MB가 커넥션 처리에만 사용됩니다.
keepalive 튜닝: 커넥션 재활용으로 레이턴시 줄이기
HTTP keepalive는 동일 클라이언트의 여러 요청을 하나의 TCP 커넥션으로 처리하는 메커니즘입니다. 리버스 프록시 환경에서는 클라이언트→Nginx와 Nginx→업스트림(백엔드) 두 구간 모두 최적화해야 합니다.
http {
# 클라이언트와의 keepalive 타임아웃
# 너무 길면 유휴 커넥션이 워커 자원을 점유, 너무 짧으면 TCP 핸드셰이크 반복
keepalive_timeout 65s;
# 하나의 keepalive 커넥션에서 처리할 최대 요청 수
keepalive_requests 1000;
upstream backend {
server 10.0.1.10:8080;
server 10.0.1.11:8080;
server 10.0.1.12:8080;
# Nginx → 백엔드 keepalive 커넥션 풀 크기
# 각 워커가 백엔드와 유지할 유휴 커넥션 수
# 너무 높으면 백엔드의 파일 디스크립터를 소진할 수 있음
keepalive 64;
# keepalive 요청당 타임아웃 (백엔드 연결 유지 시간)
keepalive_timeout 60s;
}
server {
listen 80;
location /api/ {
proxy_pass http://backend;
# 업스트림 keepalive를 사용하려면 반드시 HTTP/1.1 지정
proxy_http_version 1.1;
# Connection 헤더를 빈 문자열로 지워야 keepalive가 유지됨
proxy_set_header Connection "";
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_http_version 1.1과 proxy_set_header Connection "" 두 줄은 세트입니다. 둘 중 하나라도 빠지면 Nginx와 백엔드 사이의 keepalive가 동작하지 않아 요청마다 새 TCP 커넥션을 맺는 오버헤드가 발생합니다. 실제로 이 두 줄만 추가해도 백엔드 레이턴시가 10~30% 개선되는 사례가 흔합니다.
gzip과 프록시 버퍼: CPU와 네트워크 비용의 균형
gzip 압축은 전송 데이터를 줄여 네트워크 비용과 클라이언트 로딩 시간을 개선합니다. 단, CPU 비용이 따릅니다. 압축 레벨 1~9 중 1~4가 CPU 대비 압축률 균형이 가장 좋습니다. 레벨 6(기본값)은 레벨 4 대비 압축률이 5~10% 높지만 CPU는 30~50% 더 소비합니다.
http {
# gzip 압축 설정
gzip on;
gzip_comp_level 4; # 1~9 중 4가 CPU/압축률 균형점
gzip_min_length 1024; # 1KB 이하 응답은 압축하지 않음 (오버헤드가 이득보다 큼)
gzip_vary on; # Vary: Accept-Encoding 헤더 추가 (CDN 캐시 분리)
gzip_proxied any; # 프록시된 요청에도 압축 적용
gzip_types
text/plain
text/css
text/javascript
application/javascript
application/json
application/xml
image/svg+xml;
# 이미지(JPEG, PNG, WebP)는 이미 압축되어 있어 gzip 효과 없음 → 제외
# 프록시 버퍼 설정
# 백엔드 응답을 버퍼에 담아 클라이언트에 전송 → 백엔드 커넥션을 빨리 해제
proxy_buffering on;
proxy_buffer_size 16k; # 응답 헤더 버퍼 (헤더가 클 경우 늘릴 것)
proxy_buffers 8 32k; # 응답 바디 버퍼 (8개 × 32KB = 256KB)
proxy_busy_buffers_size 64k; # 클라이언트에 전송 중인 버퍼 최대 크기
# 타임아웃 설정
proxy_connect_timeout 10s; # 백엔드 연결 시도 제한 (기본 60s는 너무 길다)
proxy_read_timeout 60s; # 백엔드에서 데이터 읽기 제한
proxy_send_timeout 60s; # 백엔드로 데이터 전송 제한
}
proxy_buffering on이 기본값이지만 스트리밍 API(SSE, chunked transfer)에서는 버퍼링을 끄거나 X-Accel-Buffering: no 헤더를 사용해야 합니다. 버퍼 크기는 일반 API라면 256KB 수준이 적절하고, 큰 파일 다운로드 서버라면 proxy_max_temp_file_size를 함께 조정해야 합니다.
업스트림 로드밸런싱과 헬스 체크
리버스 프록시의 핵심 역할 중 하나는 트래픽을 여러 백엔드에 분산하는 것입니다. Nginx 오픈소스는 round-robin, ip_hash, least_conn을 지원합니다.
| 알고리즘 | 동작 방식 | 적합한 케이스 | 단점 |
|---|---|---|---|
| round-robin (기본) | 순서대로 돌아가며 분배 | 백엔드 성능이 균일할 때 | 느린 서버에도 동일 요청 전달 |
| least_conn | 활성 커넥션이 가장 적은 서버 우선 | 처리 시간이 불균일한 API | 상태 정보 공유 오버헤드 |
| ip_hash | 클라이언트 IP 기준 고정 서버 | 세션 스티키가 필요할 때 | 부하 불균형 가능성 |
| random (Plus/OSS 1.15.1+) | 무작위 선택 (two choices 변형) | 대규모 업스트림 풀 | 세션 유지 불가 |
upstream backend {
least_conn; # 활성 커넥션 최소 서버 우선
server 10.0.1.10:8080 weight=3; # 트래픽의 3/5 처리
server 10.0.1.11:8080 weight=2; # 트래픽의 2/5 처리
server 10.0.1.12:8080 backup; # 다른 서버 모두 다운 시에만 사용
# 수동 헬스체크 (오픈소스): 실패 시 일정 시간 제외
# max_fails=3: 3번 실패하면 fail_timeout 동안 제외
# fail_timeout=30s: 제외 후 30초 후 재시도
server 10.0.1.10:8080 weight=3 max_fails=3 fail_timeout=30s;
keepalive 64;
}
능동적 헬스 체크(active health check)는 Nginx Plus 유료 기능입니다. 오픈소스에서는 passive 방식(실제 요청 실패 기준)만 가능합니다. 보다 정교한 헬스 체크가 필요하다면 Nginx Plus 또는 nginx_upstream_check_module 서드파티 모듈을 검토하세요.
실전 팁과 벤치마크 관점
튜닝 전후를 측정하지 않으면 효과를 알 수 없습니다. wrk나 ab로 간단한 부하 테스트를 수행하고 Nginx 상태 모듈(stub_status)로 모니터링하세요.
# stub_status 활성화 (내부 IP에서만 접근 허용)
server {
listen 127.0.0.1:8080;
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
# wrk 부하 테스트: 12 스레드, 400 커넥션, 30초간
wrk -t12 -c400 -d30s http://localhost/api/health
체크리스트 형태의 추가 실전 팁입니다.
- sendfile on / tcp_nopush on / tcp_nodelay on 세트: 정적 파일 전송 효율을 높입니다.
sendfile은 커널 공간에서 직접 파일을 전송해 user-space 복사를 생략합니다. - open_file_cache: 자주 열리는 파일의 디스크립터, 크기, 수정 시간을 캐시해
stat()시스템 콜을 줄입니다.open_file_cache max=10000 inactive=30s;가 일반적인 시작값입니다. - access_log 최소화: 고트래픽 환경에서 모든 요청을 로그로 기록하면 I/O 병목이 됩니다.
access_log off;로 끄거나buffer=16k flush=5s버퍼 옵션을 추가하세요. - SSL/TLS 최적화:
ssl_session_cache shared:SSL:10m;로 TLS 세션을 캐시하면 full handshake를 줄여 HTTPS 응답 시간이 개선됩니다. - rate limiting:
limit_req_zone으로 IP당 요청 속도를 제한해 DDoS와 어뷰저 트래픽을 차단하세요. 정상 트래픽에는burst값으로 순간 급증을 허용합니다.
마무리
Nginx 튜닝은 단일 마법 설정이 없습니다. 서비스의 트래픽 패턴(짧은 요청이 많은지, 큰 파일을 다운로드하는지), 백엔드의 응답 시간 분포, 서버 하드웨어 스펙에 따라 최적값이 달라집니다. 이 글에서 다룬 설정값은 범용 출발점이므로 반드시 실제 트래픽과 유사한 조건에서 부하 테스트를 수행하고 조정하세요. 변경 하나하나를 분리해서 측정하는 것이 효과를 정확히 파악하는 유일한 방법입니다.
자주 묻는 질문
Q. worker_processes를 CPU 코어 수보다 많이 설정하면 안 되나요?
A. 대부분의 경우 코어 수를 넘기면 컨텍스트 스위칭 오버헤드만 늘어납니다. Nginx는 이벤트 기반 비동기 I/O이므로 워커 하나가 여러 커넥션을 처리할 수 있습니다. 예외는 CPU 집약적 작업(SSL 처리, 이미지 처리 모듈 등)이 많을 때인데, 이 경우에도 코어 수의 1~2배를 넘지 않는 것이 일반적입니다.
Q. gzip_comp_level을 높일수록 무조건 좋은 것 아닌가요?
A. 그렇지 않습니다. 레벨 6 이상에서는 압축률 향상이 미미하고 CPU 비용이 급격히 늘어납니다. 실제 벤치마크에서 레벨 1과 레벨 9의 압축 파일 크기 차이는 보통 5~15% 이내인 반면, CPU 사용량은 5~10배 차이가 납니다. 전송 비용이 CPU보다 훨씬 비싼 환경이 아니라면 레벨 4 이하를 권장합니다.
Q. 업스트림 keepalive 값은 어떻게 정하나요?
A. worker_processes × keepalive가 총 유휴 커넥션 수입니다. 백엔드 서버가 허용하는 최대 커넥션 수를 초과하지 않아야 합니다. 일반적으로 백엔드 서버당 keepalive / 업스트림 서버 수개의 커넥션이 유지된다고 볼 수 있습니다. 32~128 범위에서 시작해 백엔드 CLOSE_WAIT 상태 커넥션 수를 모니터링하면서 조정하세요.