V2의 Nginx 인프라 설계 - 100-hours-a-week/KTB4-3rd-CLOUD GitHub Wiki
Nginx는 외부 HTTPS·WSS 진입점으로 TLS를 종료하고 Spring Boot로 요청을 프록시한다. 조회 요청과 WebSocket 신규 연결이 짧은 시간에 집중되면 TLS handshake, client·upstream 소켓, 파일 디스크립터와 프록시 버퍼가 부하를 만든다.
Nginx가 단일 진입점으로 동작하는 과정은 다음과 같다.
- 사용자의 HTTPS 요청을 443 포트에서 받아 TLS를 종료한다. (Route 53 → EC2 EIP 직결, 앞단 로드밸런서 없음)
-
/경로는 Next.js 컨테이너로 전달한다. 해시가 붙은 정적 파일(/_next/static/)에는 장기 캐시 헤더를 붙인다. -
/api경로는 IP당 20 r/s(burst 40) 제한을 적용한 뒤 Spring Boot 컨테이너로 전달한다. -
/api/wss경로는Upgrade·Connection헤더를 전달해 HTTP Upgrade 후 Spring Boot 내부의 STOMP WebSocket 서버로 연결한다.
V1 단계에서 서비스의 평일 피크 RPS는 1.25로 가정하여 빠른 배포와 서비스 규모를 고려한 단일 인스턴스 내에 Nginx 컨테이너의 구성이 적합했다. 하지만 V2로 서비스가 성장하면서 부하 측정을 근거로 한계 지점을 다음과 같이 정의할 수 있다.
모든 외부 연결의 TCP 수립, TLS 핸드셰이크, 프록시 연결 유지를 Nginx 컨테이너 하나가 처리한다.
- 연결 수립 한계: Nginx 0.25 vCPU 실험에서 200 conn/s일 때 연결 수립 p95가 3.57초였다. 현재 배정은 0.15 vCPU로 이보다 작다.
- 장애 증폭: S1 조회 부하 91 RPS에서 백엔드 응답이 지연되자, 응답을 기다리는 연결이 Nginx에 쌓였다. 연결마다 클라이언트 소켓, TLS 상태, upstream 소켓, 프록시 버퍼를 유지하므로 64 MiB 한도에 반복해서 도달했다. worker가 종료될 때마다 연결이 일괄 끊기고, 재접속 TLS 핸드셰이크가 몰려 p95가 2.4초로 늘었다. 이후 전 요청이 응답 없이 실패했다.
- 단일 장애점: Nginx가 멈추면 프론트엔드, API, WebSocket이 동시에 중단된다. 단일 인스턴스 구조라 진입점을 수평 확장할 수 없다.
즉 Nginx의 한계는 연결 수립 처리량이고, 백엔드 지연 시에는 연결이 누적되면서 지연을 전체 장애로 키우는 통로가 된다.
컨테이너 메모리 한도의 합(MySQL 768 + Backend 768 + Frontend 128 + Nginx 64 = 1,728 MiB)에 OS·Docker 사용량(약 450 MiB)을 더하면 t3.small(2 GiB)을 넘는다. 평소에는 각 컨테이너가 한도보다 적게 써서 문제가 드러나지 않는다.
S1 91 RPS 구간에서 장애는 다음 순서로 전파되었다.
- DB 조회 지연으로 HikariCP 10개 포화, 대기 요청 최대 191개
- Tomcat 스레드 고갈로 백엔드 working set이 407 MiB에서 622~662 MiB로 증가
- Nginx에 응답 대기 연결이 누적되어 메모리 한도에 반복 도달
- 컨테이너들이 동시에 한도 근처까지 늘어나 호스트 메모리 고갈
- 호스트 OOM Kill로 MySQL 또는 Backend가 종료되고, 부하와 무관한 서비스까지 중단
컨테이너별 한도는 한 컨테이너의 폭주를 막지만, 한도 합이 호스트를 넘는 상태에서는 여러 컨테이너가 동시에 커지는 상황을 막지 못한다.
| 구간 | 가용성 | 조회 p95 | 상태 |
|---|---|---|---|
| 45.5 RPS | 100% | 301 ms | 정상 |
| 91 RPS | 6.9% | 10초 (타임아웃) | 시작 60초 안에 붕괴 |
V1의 조회 한계는 45.5~91 RPS 사이다. 가장 먼저 포화되는 자원은 DB 조회와 HikariCP이고, 1·2번은 그 포화가 서비스 전체 장애로 번지는 경로가 된다.
위에서 정의했던V2의 가용성과 성장 지표 부하를 견디는 구조의 조건을 다음과 같이 정의할 수 있다.
- Nginx와 애플리케이션 서버는 분리되어 장애 전파가 되면 안된다.
- Nginx가 단일 진입점이기 때문에 단일 장애 지점이 된다.
- Upgrade·TLS Handshake 등 처리하기 위해서는 Nginx 의 스펙이 0.15 vCPU · 64 MiB 보다 커야 한다.
대안별로 V1의 두 한계 지점을 얼마나 해소하는지와 비용·운영 부담을 나눠서 비교한다. (○ 해소, △ 부분 해소, ✕ 해소 안 됨)
| 방법 | 구성 경로 | 1. 연결 비용 집중 | 2. 장애 격리 경계 | 단일 장애점 | WebSocket 영향 |
|---|---|---|---|---|---|
| Nginx 컨테이너 Scale-up | Route 53 → EC2(Nginx 컨테이너) | △ 배정한 vCPU·메모리만큼 늘지만 상한은 호스트 | ✕ 같은 호스트. 늘린 만큼 다른 컨테이너 한도를 줄여야 해서 오버커밋과 충돌 | 남음 | Nginx 재시작 시 전체 연결 끊김 |
| ALB (ACM 인증서) | Route 53 → ALB → EC2 | ○ TLS·연결 수립을 관리형으로 자동 확장 | △ 진입점은 분리되지만 앱 호스트 안의 컨테이너 자원 공유는 남음 | 해소 (다중 AZ·대상 다중화 시) | 유휴 타임아웃(기본 60초) 조정 필요 |
| Nginx 별도 인스턴스 분리 | Route 53 → EC2(Nginx) → EC2(앱) | △ 인스턴스 스펙만큼 증가 | ○ 앱 호스트의 메모리 고갈이 진입점으로 번지지 않음 | 남음 | Nginx 인스턴스 교체 시 전체 연결 끊김 |
| CloudFront + Nginx 별도 인스턴스 | CloudFront → EC2(Nginx) → EC2(앱) | △ 정적 자원·TLS는 엣지에서 처리, 동적 API·WebSocket은 원본으로 옴 | ○ 앱 호스트와 분리 | 남음 (원본 Nginx) | 연결마다 원본까지 유지 |
| CloudFront + ALB(ACM) → ECS 서비스 (Nginx 제거) | CloudFront → ALB → Next.js·REST·WebSocket 대상 그룹 | ○ 엣지 TLS 종료·원본 연결 재사용 + ALB 자동 확장 | ○ 서비스(대상 그룹) 단위로 장애 범위 축소 | 해소 | WebSocket 서비스 태스크 교체 시에만 재접속 |
| 방법 | 추가 비용 | 운영 부담 | 추가로 설계할 것 | V2 적합성 |
|---|---|---|---|---|
| Nginx 컨테이너 Scale-up | 없음 | 낮음 | 컨테이너 한도 재배분 | 임시 조치 |
| ALB (ACM 인증서) | 시간당 고정 요금 + LCU (서울 리전 월 약 22.42 USD) | 낮음 | WebSocket 유휴 타임아웃, 헬스체크, 요청 hop 1단계 증가 | V2 다중 인스턴스 구성의 전제 |
| Nginx 별도 인스턴스 분리 | 인스턴스 1대 | 중간 (OS·Nginx 패치 직접 관리) | 인스턴스 간 네트워크·보안 그룹 | 격리만 해결, 단일 장애점 남음 |
| CloudFront + Nginx 별도 인스턴스 | 인스턴스 1대 + CloudFront 요청·전송량 | 중간 | 캐시 무효화, 헤더 전달 정책 | 정적 부하를 줄이는 보조 수단 |
| CloudFront + ALB(ACM) → ECS 서비스 (Nginx 제거) | ALB + CloudFront (Nginx 비용 제거) | 낮음 (관리형) | IP당 초 단위 rate limit 이전 (WAF 1분 이상 집계 또는 Bucket4j + Redis), 캐시·헤더 정책, 유휴 타임아웃 | 채택 |
V2 목표에서 웹소켓 동시 연결 1,800개와 행사 혼합 부하 226 RPS의 연결 수립 처리량을 요구할 때를 ALB를 고려할 수 있다.
V2의 백엔드 애플리케이션 인프라 설계 문서에서 REST 서버와 WebSocket 서버의 분리는 확정되었고, 각 서버는 서로 다른 기준(REST는 RPS·p95/p99, WebSocket은 동시 연결 수·신규 연결률)으로 scale-out·scale-in 하도록 운영한다.
Nginx를 ECS 태스크로 옮기는 방법도 가능하지만, 진입점을 여러 태스크로 분산하는 순간 로드밸런서가 다시 필요해진다. CloudFront의 VPC 원본은 ALB·NLB·EC2만 지정할 수 있어서 Nginx 태스크 여러 개에 직접 연결할 수 없다. 결과적으로 CloudFront → ALB/NLB → Nginx 태스크 → REST/WebSocket 태스크 구조가 되어, ALB 비용은 그대로 들고 Nginx가 관리 대상 hop으로 하나 더 남는다.
V1에서 Nginx가 맡던 역할은 대부분 관리형 서비스로 대체할 수 있다.
| Nginx 역할 (V1) | 대체 수단 |
|---|---|
| 443 TLS 종료 | ALB + ACM, 또는 CloudFront 엣지 종료 |
/, /api, /api/wss 경로 라우팅 |
ALB 리스너 규칙 → 서비스별 대상 그룹 |
| WebSocket Upgrade | ALB 기본 지원 (유휴 타임아웃 조정 필요) |
/_next/static/ 장기 캐시 |
CloudFront 캐시 정책 |
| IP당 20 r/s(burst 40) 제한 | 정확히 대체되지 않는다. WAF rate-based 규칙(1분 이상 집계) 또는 애플리케이션 단 rate limit(Bucket4j + Redis) |
따라서 V2의 진입점은 Route 53 → CloudFront(정적 부하 분산) → ALB(ACM) → ECS 서비스(Next.js / REST / WebSocket 대상 그룹 분리)로 선정하고, Nginx는 제거하는 것을 기본안으로 한다.