문제: 요청마다 새 TCP 연결이 열린다

NGINX를 리버스 프록시로 쓰면서 upstream 블록만 정의하고 끝내는 경우가 많다. 이 상태에서는 NGINX가 백엔드로 요청을 보낼 때마다 새 TCP 연결을 맺고 응답 후 닫는다. 트래픽이 몰리면 이 비용이 그대로 지연과 자원 소모로 돌아온다.

구체적으로 세 가지가 문제다. 첫째, 매 요청마다 3-way handshake 왕복이 붙어 응답 지연이 늘어난다. HTTPS 업스트림이면 TLS 핸드셰이크까지 더해진다. 둘째, 닫힌 연결이 TIME_WAIT 상태로 쌓인다. 셋째, 로컬 포트(ephemeral port)가 빠르게 소진되어 특정 순간부터 connect() failed (99: Cannot assign requested address) 에러가 터진다.

현재 상태 확인

튜닝 전에 실제로 연결이 재사용되지 않고 있는지 확인한다. 프록시 서버에서 업스트림 방향으로 열린 연결 상태를 세어 본다.

# 업스트림(예: 10.0.0.20:8080)으로 향하는 연결 상태 분포
ss -tan 'dst 10.0.0.20:8080' | awk 'NR>1 {print $1}' | sort | uniq -c

# TIME_WAIT가 수만 개 쌓여 있다면 재사용이 안 되고 있다는 신호
#   32451 TIME_WAIT
#      18 ESTAB

TIME_WAIT가 ESTAB보다 압도적으로 많다면 연결이 계속 새로 생기고 버려지는 중이다.

해결: upstream keepalive 활성화

핵심은 세 가지를 함께 설정하는 것이다. upstream 블록에 keepalive로 유휴 연결 풀 크기를 지정하고, location에서 HTTP/1.1을 쓰도록 하며, 기본으로 붙는 Connection: close 헤더를 비워 준다. 이 세 가지 중 하나라도 빠지면 재사용이 되지 않는다.

upstream app_backend {
    server 10.0.0.20:8080;
    server 10.0.0.21:8080;

    # 워커 프로세스당 유지할 유휴 연결 수
    keepalive 64;
    # 한 연결로 처리할 최대 요청 수 (초과 시 연결 재생성)
    keepalive_requests 1000;
    # 유휴 연결을 닫기까지 대기 시간
    keepalive_timeout 60s;
}

server {
    location / {
        proxy_pass http://app_backend;

        # keepalive 재사용의 필수 조건
        proxy_http_version 1.1;
        proxy_set_header Connection "";
    }
}

proxy_set_header Connection ""를 빠뜨리면 NGINX가 매 요청마다 연결 종료를 요청하므로 keepalive 값이 무의미해진다. 가장 흔히 놓치는 부분이다.

keepalive 값은 어떻게 정하나

keepalive는 전체 최대 연결 수가 아니라 워커당 유지하는 유휴 연결 수다. 활성 연결은 이 값과 별개로 필요한 만큼 열린다. 너무 크게 잡으면 백엔드가 유휴 연결을 과도하게 물고 있게 되고, 너무 작으면 피크 때 재사용 효과가 떨어진다.

실무에서는 백엔드의 동시 처리 능력과 worker_processes를 함께 본다. 백엔드가 처리하는 평균 동시 요청 수를 워커 수로 나눈 값 언저리에서 시작해 관찰하며 조정하는 편이 안전하다.

keepalive_requests / timeout 정렬

NGINX의 keepalive_requests와 keepalive_timeout은 반드시 백엔드 애플리케이션 서버 설정과 맞춰야 한다. 백엔드가 먼저 연결을 닫는데 NGINX가 그 연결을 재사용하려 하면 502가 간헐적으로 발생한다.

항목NGINX 측백엔드 측 정렬 대상
요청 수 한계keepalive_requests서버의 max keep-alive requests
유휴 타임아웃keepalive_timeout서버의 keep-alive timeout(더 길게)

원칙은 백엔드의 유휴 타임아웃을 NGINX보다 길게 두는 것이다. 그래야 항상 NGINX가 먼저 연결을 정리해 경합을 피한다.

주의점

첫째, keepalive 지시어는 upstream 블록의 마지막에 두는 관행이 있지만 순서 자체가 동작에 필수는 아니다. 다만 hash, ip_hash 같은 로드밸런싱 메서드와 함께 쓸 때는 버전별 호환을 확인해야 한다.

둘째, keepalive를 켰다고 무조건 좋아지는 것은 아니다. 요청 간격이 keepalive_timeout보다 길어 유휴 연결이 계속 만료되는 저트래픽 환경에서는 효과가 미미하다.

셋째, 설정 후 반드시 실측한다. 적용 전후로 ss의 TIME_WAIT 개수와 업스트림 응답 지연($upstream_response_time)을 비교해 실제 재사용이 일어나는지 확인해야 한다.

정리

keepalive + proxy_http_version 1.1 + Connection "" 세 가지가 한 세트다. 값은 백엔드 용량과 워커 수를 기준으로 잡고, 타임아웃/요청 수는 백엔드보다 NGINX가 먼저 닫도록 정렬한다. 마지막으로 ss와 로그로 재사용 여부를 검증하면 handshake 비용과 포트 소진 문제를 함께 해소할 수 있다.