1. 서론
안정적으로 운영되던 서비스에서 갑작스럽게 특정 시간대에 API 응답 속도가 5초 이상 지연되는 현상을 겪어보신 적 있으십니까? 특히 TPS(초당 트랜잭션 수)가 평소와 다름없는 수준임에도 불구하고, 간헐적으로 발생하는 타임아웃과 지연은 개발자와 운영자를 곤혹스럽게 만듭니다. 네트워크 장비나 인프라의 문제가 아니라면, 그 원인은 의외로 우리가 무심코 사용했던 코드 한 줄에 숨어 있을 수 있습니다.
일부 서버에서 발생한 API 지연 장애에 대한 분석을 진행하여, 조치한 내용을 바탕으로, 지연의 핵심 원인이었던 SimpleClientHttpRequestFactory의 구조적 한계와 이를 해결하기 위한 Connection Pool 도입 전략에 대해 심층적으로 알아보겠습니다.
왜 커넥션 풀이 없는 HTTP 요청이 OS의 자원을 고갈시키는지, 그리고 리눅스 커널 레벨에서 어떤 일이 벌어지는지 궁금하셨던 분들에게 명쾌한 해답이 될 것입니다.
2. 본론
장애 현상과 초기 분석 – 왜 네트워크 문제가 아닌가?
특정 시간대에 일부 애플리케이션 서버에서 평소보다 2000ms 이상 지연되는 현상이 발생했습니다. 평소 하루 20건 미만으로 지연이었던 것과 비교하면 명백한 이상 징후였습니다.
초기에는 네트워크나 타겟 서버의 장애를 의심했습니다. 하지만 분석 결과는 달랐습니다.
- 타겟 서버 정상: 타겟 서버 자체에는 장애나 특이사항이 없었습니다.
- TPS 평이: 전체적인 트래픽 양은 평소와 비슷한 수준이었습니다.
- 특정 API 국한: 인증 정보를 다루는 특정 API 호출 시점에만 지연이 발생했고, 다른 API들은 정상적으로 처리
- 네트워크 장비 무관: 지연이 발생한 서버들이 서로 다른 스위치 장비에 물려 있었으므로, 특정 스위치나 랙(Rack) 단위의 하드웨어 장애일 가능성은 희박
결국 외부 요인이 아닌 어플리케이션 내부의 병목으로 분석 범위를 좁혔고,
그 중심에는 HTTP 요청을 처리하는 SimpleClientHttpRequestFactory가 있었습니다.
원인 분석 – SimpleClientHttpRequestFactory와 OS 자원 고갈
이번 장애의 핵심 원인(Root Cause)은 Connection Pool이 없는 SimpleClientHttpRequestFactory를 사용했기 때문입니다. 이 방식이 왜 5초 이상의 치명적인 지연을 유발했는지 기술적으로 상세히 분석해 보겠습니다.
1. 매 요청마다 반복되는 핸드쉐이크 (Handshake Overhead)
SimpleClientHttpRequestFactory는 요청을 보낼 때마다 새로운 연결을 맺고 끊습니다. 이는 친구에게 질문할 때마다 전화를 걸고 끊는 것과 같습니다.
- DNS 조회: 도메인 주소를 찾습니다.
- TCP 3-way Handshake: 서버와 연결을 수립합니다. (SYN -> SYN-ACK -> ACK)
- SSL/TLS Handshake: 가장 무거운 작업입니다. 인증서를 교환하고 암호화 키를 생성하는 과정에서 많은 CPU 연산과 네트워크 왕복(Round Trip)이 발생합니다.TPS 300 상황에서 이 무거운 과정을 초당 300번씩 반복하니, 실제 데이터를 보내기도 전에 준비 운동하다가 지쳐버린 셈입니다.
2. 포트 고갈 (Port Exhaustion)과 TIME_WAIT의 늪
더 큰 문제는 연결을 끊은 직후(Close)에 발생합니다. 리눅스 OS는 연결을 끊은 포트를 즉시 재사용하지 않고, 혹시 늦게 도착할 패킷을 위해 60초 동안 TIME_WAIT 상태로 잠가둡니다(2MSL Rule).
- 가용 포트의 한계: 리눅스가 외부 통신용으로 내어줄 수 있는 임시 포트(Ephemeral Port)는 약 28,000개(32,768 ~ 60,999)로 한정되어 있습니다.
- 고갈 메커니즘: TPS 300일 때, 1분이면 18,000개(300 * 60초)의 포트가
TIME_WAIT상태에 빠집니다. 트래픽이 조금만 튀어도 가용 포트 28,000개가 순식간에 동납니다. - 결과: 사용할 포트가 없어진 OS는 애플리케이션 스레드를 대기(Block) 상태로 만듭니다. 이것이 바로 타임아웃 설정과 무관하게 발생한 5초 지연의 실체입니다.
조치 방법 – Connection Pool 도입과 Tomcat 튜닝
이 문제를 근본적으로 해결하기 위해 Apache HttpClient(HttpComponentsClientHttpRequestFactory)를 도입하여 Connection Pool을 적용했습니다.
1. Connection Pool 적용 전략
- Factory 교체:
SimpleClient...대신HttpComponentsClient...를 사용하여 연결을 끊지 않고 재사용하도록 변경했습니다. 이로써 핸드쉐이크 비용과TIME_WAIT문제를 동시에 해결했습니다. - 타임아웃 설정 (Fail Fast): 연결 요청(ConnectionRequest)에 200ms, 읽기(Read)에 1,000ms 제한을 두어, 지연 발생 시 스레드를 붙잡고 있지 않고 즉시 에러를 뱉어 시스템 전체가 마비되는 것을 방지했습니다.
- 풀 크기(Capacity Planning): TPS 300을 방어하기 위해
MaxConnTotal을 500으로,MaxConnPerRoute를 300으로 넉넉하게 설정하여 특정 타겟 서버로 가는 요청이 대기열에서 밀리지 않도록 했습니다.
2. Tomcat Max Threads 증설
- 기존 300개였던 톰캣 스레드를 500개로 증설했습니다. Blocking I/O 방식에서는 HTTP 클라이언트 연결 수만큼 스레드가 확보되어야 병목이 생기지 않기 때문입니다. 이는 리틀의 법칙(Little’s Law)에 따라 계산된 필요 용량에 안전 마진을 더한 값입니다.
3. 결론
이번 장애 분석을 통해 우리는 “연결을 맺고 끊는 비용“이 대규모 트래픽 환경에서 얼마나 치명적인지 확인할 수 있었습니다. SimpleClientHttpRequestFactory는 구현이 간단하지만, 커넥션 풀 부재로 인해 OS의 포트 자원을 낭비하고 결국 시스템을 멈추게 만들었습니다.
해결책은 명확합니다. 외부 API 호출이 빈번한 서비스에서는 반드시 Connection Pool을 사용하여 TCP 연결을 재사용해야 합니다. 또한, OS 레벨의 TIME_WAIT 특성과 포트 한계를 이해하고, 이에 맞는 적절한 스레드 풀과 커넥션 풀 사이즈를 산정하는 것이 안정적인 서비스 운영의 핵심입니다. 이번 분석 자료가 여러분의 시스템을 더욱 견고하게 만드는 데 도움이 되기를 바랍니다.