시작하기
얼마 전, 프론트에서 서버를 호출하는 과정에서 평소에는 전혀 문제가 없는데 트래픽이 많아지는 상황에서 502가 발생하는것을 발견했다.
일부 로직에서는 http 요청으로 보상트랜잭션도 처리되고있기 때문에 보상트랜잭션마저 제대로 동작하지 않는 문제를 발견했다.
그리고 그 문제의 원인과 해결과정에 대해 정리해보고자 한다.
문제는 “HTTP 요청”이 아니었다.
운영 중인 API 서버에서 간헐적으로 502 Bad Gateway가 발생했다.
공통적인건 트래픽이 많은 상황에서 자주 발생했다.
결론적으로 트래픽이 몰려 요청 수가 많은건 문제가 아니었다.
짧은 시간에 반복 호출되는 API가 문제였다. HTTP 요청 자체가 아니라 HTTP 연결 Keep-Alive 이었다.
HTTP 요청 전에 반드시 일어나는 일
우리가 흔히 말하는 “API 호출”은 사실 다음 단계를 모두 포함한다.
1. TCP 연결 수립 (3-way handshake)
2. HTTP 요청 전송
3. HTTP 응답 수신
4. 연결 유지 또는 종료즉, HTTP는 TCP 연결 위에서만 존재한다.
TCP 연결은 생각보다 비싸다
TCP 연결 하나를 만들기 위해서는 최소한 다음이 필요하다.
- SYN → SYN/ACK → ACK (3-way handshake)
- RTT(Round Trip Time) 1회 이상
- 커널 소켓, 파일 디스크립터 점유
- NAT / Load Balancer 상태 테이블 사용
👉 요청을 보내기도 전에 이미 비용이 발생한다.
Keep-Alive가 없다면 벌어지는 일
Keep-Alive가 꺼져 있거나, 너무 짧게 설정되어 있다면 요청 흐름은 이렇게 된다
요청 1 → TCP 연결 → 응답 → TCP 종료
요청 2 → TCP 연결 → 응답 → TCP 종료
요청 3 → TCP 연결 → 응답 → TCP 종료이 방식의 문제점은 명확하다.
- 매 요청마다 TCP handshake
- TIME_WAIT 소켓 폭증
- 커널 리소스 소모
- 트래픽이 몰릴수록 연결 자체가 병목
HTTP Keep-Alive란 무엇일까
HTTP Keep-Alive는 TCP 연결을 닫지 않고 재사용하는 방식이다.
TCP 연결 1개
├─ HTTP 요청 1
├─ HTTP 요청 2
├─ HTTP 요청 3- TCP handshake 비용 제거
- 평균 응답 시간 감소
- 서버 및 로드밸런서 부담 감소
Node.js 서버의 기본값이 문제이다
여기서 핵심이 되는 것이 Node.js HTTP 서버의 기본 keep-alive 설정이다.
Node.js 기본값
| 설정 | 기본값 |
|---|---|
| server.keepAliveTimeout | 5초 |
| server.headersTimeout | 60초 |
즉, Node.js 서버는 응답 후 5초가 지나면 TCP 연결을 스스로 종료한다.
ALB 환경에서 이게 왜 문제가 될까?
문제의 구조는 다음과 같았다.
Client → ALB → Node.js(Target)기본 설정 상태
ALB idle timeout = 60초
Node keepAliveTimeout = 5초이 상태에서 실제로 벌어지는 일은 다음과 같다
- ALB는 백엔드 연결이 아직 유효하다고 판단
- Node.js 서버는 5초 후 keep-alive로 연결 종료
- ALB가 기존 연결로 요청 전달 시도
- 이미 종료된 TCP → FIN / RST
- ALB는 502 Bad Gateway 반환
👉 애플리케이션 로그에는 아무것도 남지 않는다 👉 연결 단계에서 이미 실패했기 때문이다
우리가 취한 해결 방법
해결 방법은 의외로 단순하다. 하지만 원리를 모르면 떠올리기 쉽지 않다.
핵심 원칙
Target keepAliveTimeout > ALB idle timeout적용한 설정
server.keepAliveTimeout = 65000; // 65초
server.headersTimeout = 66000;즉, ALB가 먼저 연결을 끊도록 유도하여 Target 서버가 먼저 끊는 상황 을 제거해야 한다.
결과는 어땠을까?
keep-alive timeout 조정 이후 502 응답은 더 이상 발생하지 않았다.
트래픽이 많아져도, 순간 요청량이 많아져도 괜찮았다.
이 문제는 코드 수정이 아니라 네트워크 설정 불일치에서 비롯된 문제였다.
ALB는 커넥션 풀을 가지고 있지 않나?
ALB는 백엔드(Target)와의 연결을 재사용한다. 즉, Keep-Alive를 전제로 동작한다.
따라서 타겟 서버가 ALB보다 먼저 연결을 끊으면, ALB 입장에서는 “살아있다고 생각한 연결”이 갑자기 사라지는 셈이다.
실무에서 꼭 기억해야 할 정리
① Keep-Alive는 옵션이 아니라 구조이다.
- 짧은 요청일수록 효과가 큼
- 마이크로서비스 간 통신에서는 필수
② “기본값”은 안전하지 않다.
- Node.js 기본 keepAliveTimeout = 5초
- ALB 기본 idle timeout = 60초
③ Timeout은 계층 간에 맞춰야 한다.
- Client < ALB < Target
각 계층의 timeout은 의도적으로 정렬되어야 한다.
마무리
HTTP Keep-Alive는 단순한 성능 옵션이 아니다.
잘 맞추면 서버를 살리고, 어긋나면 이유 없는 502를 만들어낸다.
이번 이슈를 통해 다시 한 번 느낀것은 장애의 원인은 항상 코드에 있지 않다.
“연결”은 생각보다 오래 살아 있다 그리고 기본값은 항상 의심하자.