고유량 상황에서의 Nginx 성능 최적화 및 병목 현상 해결 방안

Nginx 의 잠재력을 극대화하는 운영 전략

Nginx 는 업계 표준처럼 자리 잡은 고성능 웹 서버입니다. 그러나 프로덕션 환경에서 트래픽이 증가하자마자 시스템이 마비되거나 SSH 접속 자체가 불가능해지는 경험을 한 개발자라면 드물지 않습니다. 이는 종종 서버 교체나 Envoy 와 같은 대안 도입을 고려하게 만들지만, 대부분의 경우 문제는 소프트웨어 자체보다 설정값의 부재와 인프라 구성에 있습니다.

1. 기본 설정값의 위험성과 Worker 조정

Nginx 의 초기 배포 시 제공되는 설정은 안전성을 최우선으로 하여 매우 보수적으로 설계되어 있습니다. 예를 들어 worker_processes 는 기본적으로 단일 스레드로 동작하며, 이는 멀티 코어 CPU 를 사용하는 현대적인 서버에서는 자원 낭비를 의미합니다. 또한 각 워커가 처리할 수 있는 동시 연결 수도 낮게 설정되어 있어 급증하는 요청을 즉시 수용하지 못합니다.

하드웨어 스펙을 효율적으로 활용하려면 다음과 같이 이벤트 핸들링 구간을 최적화해야 합니다.

# /etc/nginx/nginx.conf 예시
# CPU 코어 수만큼 자동 할당하여 처리 능력을 최대화
worker_processes auto;

events {
    # 허용 가능한 최대 동시 연결 수를 시스템 한도까지 확장
    worker_connections 102400;
    
    # 한 번의 이벤트 루프에서 가능한 많은 새 연결을 동시에 수용
    multi_accept on;
}

http {
    # Linux 환경에서 효율적인 입출력 처리를 위해 epoll 사용 권장
    use epoll;
}

이러한 조정은 네트워크 인터페이스의 대기열을 비워주고, CPU 자원을 실질적으로 활용하여 병목을 줄이는 첫걸음입니다.

2. 운영체제 수준의 제약 조건 해제

애플리케이션 설정만 변경하고 커널 파라미터는 방치하면 효과가 반감됩니다. Nginx 가 높은 연결 수를 지원하도록 설정해도, Linux 커널이 허용하는 오픈 파일 디스크립터(FD) 제한을 넘으면 오류가 발생합니다.

시스템 범위의 제한을 완화하기 위해서는 인증 권한 설정 파일과 커널 변수를 수정해야 합니다.

# /etc/security/limits.conf 업데이트
# 모든 사용자에게 소프트/하드 레임트를 동일하게 적용
* hard nofile 65536
* soft nofile 65536

# /etc/sysctl.conf 에 추가 또는 수정
# 수신 버퍼队列 크기 증대
net.core.somaxconn = 65536
# TCP SYN 패킷 대기 큐 증가
net.ipv4.tcp_max_syn_backlog = 8192
# 로컬 포트 범위 확장을 통한 소켓 충돌 방지
net.ipv4.ip_local_port_range = 1024 65535

설정 변경 후 sysctl -p 명령어로 활성화하고 재부팅 없이 새로운 세션에서 효과를 확인할 수 있습니다.

3. 업스트림 연결 풀 (Keepalive) 의 적절한 배분

백엔드 서비스와의 연결을 재활용하여 핸드셰이크 오버헤드를 줄이는 Keepalive 는 유용하지만, 무분별하게 큰 값을 주면 문제가 됩니다. 만약 클라이언트 요청 간격이 넓거나 백엔드 응답 시간이 지연되면, 연결된 상태의 소켓이 누적되어 새 요청을 받아들이는 데 지장이 생깁니다.

QPS 패턴을 고려하여 연결 풀 크기를 계산해야 합니다.

http {
    upstream api_cluster {
        server 10.0.0.5:8080;
        # 과도한 메모리 소모를 막기 위해 적정 수준으로 유지
        keepalive 16;
        
        zone backend_sync 64k;
    }

    server {
        location /api/ {
            proxy_pass http://api_cluster;
            
            # HTTP 1.1 을 명시적으로 지정하여 지속 연결 활성화
            proxy_http_version 1.1;
            # 백엔드로 전달될 Connection 헤더 제거
            proxy_set_header Connection "";
            
            # 타임아웃 정책 추가로 죽은 연결 방지
            proxy_connect_timeout 5s;
        }
    }
}

4. 정적 자산 처리 파이프라인 개선

이미지, CSS, 자바스크립트 파일을 매번 애플리케이션 서버가 처리하도록 하면 불필요한 리소스가 소모됩니다. Nginx 의 핵심 강점은 정적 파일 직접 서비스 기능이며, 이를 통해 애플리케이션 층의 하중을 대폭 경감시킬 수 있습니다.

# 특정 확장자를 가진 파일에 대해 별도 로직 적용
location ~* \.(jpg|jpeg|png|ico|svg)$ {
    root /var/www/static/images;
    # 브라우저에 1 년간 캐싱되도록 지시
    expires 1y;
    # 캐싱된 리소스에 대한 로그 기록 생략으로 IO 부담 감소
    access_log off;
    
    # 압축 전송 활성화
    gzip_static on;
}

5. 백엔드 웹 컨테이너의 병목 제거

Nginx 가 충분히 튜닝되어 있더라도 실제 비즈니스 로직을 수행하는 백엔드가 느리면 전체 체인은 느려집니다. 특히 Java 기반 프레임워크인 스프링 부트의 기본 톰캣 설정은 테스트용으로 낮게 잡혀 있을 수 있습니다.

서버의 물리적 메모리와 CPU 용량을 고려하여 스레드 풀을 조정해야 합니다.

server:
  tomcat:
    # 연결 수용 능력 확대
    max-connections: 20000
    accept-count: 250
    
    threads:
      # 최대 활성 스레드 수
      max: 400
      # 대기 중인 최소 스레드 수
      min-spare: 50
      
    # 긴 시간 소요되는 요청 차단
    connection-timeout: 15000ms

이러한 변화는 갑작스러운 트래픽 피크 시에도 서버가 다운되지 않고 일정을 처리할 수 있는 여유를 제공합니다.

6. 이질적인 서버 환경에서의 부하 분산

업스트림 서버들의 성능이 균일하지 않을 때 단순 라운드 로빈(Round Robin) 방식은 약한 서버를 과부하시키고 강력한 서버는 놀게 만듭니다. 이러한 편차를 보정하기 위해 가중치(Weight) 값을 부여하는 것이 효과적입니다.

upstream heavy_backend {
    # 고사양 서버에는 더 많은 트래픽 할당
    server 192.168.10.5 weight=5;
    # 저사양 서버에는 최소한의 트래픽만 분배
    server 192.168.10.6 weight=1 backup;
}

또한 ip_hash 나 least_conn 같은 다른 알고리즘을 상황에 따라 선택하여 세션 연속성이 필요한 서비스에도 대응할 수 있습니다.

7. 애플리케이션 내부 리소스 관리

외부 설정뿐만 아니라 코드 레벨의 리소스 할당도 중요합니다. 데이터베이스 연동 라이브러리에서 사용할 커넥션 풀 크기가 너무 작으면, 고병발 상황에서 모든 스레드가 데이터베이스 연결 획득을 기다리게 되어 타임아웃이 발생하게 됩니다.

HikariCP 를 사용한 초기화 예시는 다음과 같습니다.

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;

public class DatabaseFactory {
    public static DataSource createSource() {
        HikariConfig config = new HikariConfig();
        // 데이터베이스 최대 처리량을 고려하여 풀 크기 결정
        config.setMaximumPoolSize(100);
        config.setMinimumIdle(20);
        // 연결 생성 실패 시 대기 시간 설정
        config.setConnectionTimeout(30_000);
        
        return new HikariDataSource(config);
    }
}

입구(Nginx) 와 출구(DB) 의 통행량을 일치시키지 않으면 중간에 정체 구간이 생길 수밖에 없습니다.

8. 통합 아키텍처 관점에서의 접근

Nginx 하나만으로 수십만 QPS 를 처리하는 것은 현실적으로 어렵습니다. 진정한 규모 확장은 다층 구조를 기반으로 이루어져야 합니다. 가장 앞단에는 CDN 을 배치하여 트래픽을 거르며, 그 뒤에는 Nginx 가 역방향 프록시로 작동하고, 애플리케이션 전방에는 레디스와 같은 인메모리 캐시를 위치시킵니다. 요청 폭주가 예상될 때는 메시징 시스템을 도입하여 순차적 처리를 유도하고, Nginx 와 백엔드의 역할을 명확히 구분하여 전체 링크의 견고함을 확보해야 합니다.

태그: nginx linux kernel-tuning java spring-boot

6월 22일 03:10에 게시됨