TCP 연결 종료 과정
TCP는 연결을 해제하기 위해 4-way handshake 과정을 거칩니다. 통신 양측 모두 연결 종료를 주도할 수 있으며, 종료 절차가 완료되면 할당된 리소스는 해제됩니다.
- 연결 종료를 원하는 측(Active Close)이
FIN플래그가 설정된 세그먼트를 전송하고FIN_WAIT_1상태로 전환됩니다. - 수신 측(Passive Close)은 이에 대한 확인 응답으로
ACK를 전송하며CLOSE_WAIT상태로 진입합니다. ACK를 수신한 Active Close 측은FIN_WAIT_2상태로 변경됩니다.- Passive Close 측의 애플리케이션이 남은 데이터 처리를 마치고 연결 종료를 결정하면
FIN세그먼트를 전송하며LAST_ACK상태가 됩니다. - Active Close 측은 수신한
FIN에 대해ACK를 응답하고TIME_WAIT상태로 들어갑니다. - Passive Close 측은
ACK를 수신하면CLOSED상태가 되어 연결이 완전히 종료됩니다. - Active Close 측은
2MSL(Maximum Segment Lifetime) 동안 대기한 후CLOSED상태로 전환됩니다.
각 전송 방향마다 FIN과 ACK가 필요하므로 총 4번의 패킷 교환이 발생합니다. TIME_WAIT 상태는 연결 종료를 주도한 측에서만 나타나는 것이 특징입니다.
4-way Handshake가 필요한 이유
Active Close 측이 FIN을 보내는 것은 더 이상 데이터를 전송하지 않겠다는 의미일 뿐, 데이터 수신은 여전히 가능합니다. Passive Close 측은 FIN을 받은 즉시 연결을 끊지 않고, 먼저 ACK를 보낸 뒤 남은 데이터를 모두 전송한 후에야 자신의 FIN을 전송합니다. 데이터 처리 및 전송에 시간이 소요되므로 ACK와 FIN이 분리되어 전송되는 것이 일반적이며, 이로 인해 4번의 패킷 교환이 필요합니다.
패킷 손실 시나리오 및 상태 변화
첫 번째 FIN 손실
Active Close 측이 FIN을 전송하고 FIN_WAIT_1에 머물러 있을 때 ACK를 받지 못하면, tcp_orphan_retries 파라미터에 정의된 횟수만큼 FIN을 재전송합니다. 최대 재전송 횟수를 초과하고 일정 시간(이전 타임아웃의 2배) 후에도 응답이 없으면 연결을 강제로 CLOSED 상태로 종료합니다.
두 번째 ACK 손실
Passive Close 측이 보낸 ACK가 손실되면 Active Close 측의 재전송 타이머가 발동하여 FIN을 다시 보냅니다. Active Close 측이 FIN_WAIT_2 상태에 도달하면 Passive Close 측의 FIN을 기다립니다. 이 대기 시간은 tcp_fin_timeout(기본값 60초)에 의해 제어됩니다. 단, shutdown() 함수로 수신 방향만 열어둔 경우 tcp_fin_timeout의 영향을 받지 않고 무한 대기할 수 있습니다.
세 번째 FIN 손실
Passive Close 측이 FIN을 보낸 후 LAST_ACK 상태에서 클라이언트의 ACK를 받지 못하면, tcp_orphan_retries 횟수만큼 FIN을 재전송합니다. 초과 시 연결을 강제 종료합니다. Active Close 측은 tcp_fin_timeout 내에 FIN을 받지 못하면 자체적으로 연결을 닫습니다.
네 번째 ACK 손실
Passive Close 측이 ACK를 받지 못해 FIN을 재전송하면, TIME_WAIT 상태에 있는 Active Close 측은 다시 ACK를 보내고 2MSL 타이머를 초기화합니다.
TIME_WAIT 상태와 2MSL 대기 시간
MSL은 패킷이 네트워크 상에서 생존할 수 있는 최대 시간을 의미합니다. TTL이 라우터 통과 횟수를 기준으로 하는 반면, MSL은 시간 단위를 사용합니다. 리눅스에서는 MSL을 30초로 정의하며, TIME_WAIT은 2MSL인 60초 동안 유지됩니다. 이 시간 동안 대기하는 주된 이유는 다음과 같습니다.
- 신뢰할 수 있는 연결 종료 보장: 마지막
ACK가 손실될 경우 Passive Close 측이FIN을 재전송할 수 있도록 충분한 시간을 확보합니다.TIME_WAIT가 없다면 Active Close 측은 이미CLOSED상태이므로FIN재전송에 대해RST를 응답하게 되어 비정상적인 종료가 발생합니다. - 이전 연결의 지연 패킷 간섭 방지: 동일한 4-tuple(출발지 IP, 출발지 포트, 목적지 IP, 목적지 포트)로 새로운 연결이 수립될 때, 네트워크에 남아있는 이전 연결의 지연 패킷이 새 연결에 유입되어 데이터 정합성을 해치는 것을 방지합니다.
2MSL은 왕복 지연을 고려한 시간으로, 이전 연결의 모든 패킷이 네트워크 상에서 소멸되도록 보장합니다.
TIME_WAIT 과다 발생의 영향 및 최적화
TIME_WAIT 상태의 소켓이 과도하게 누적되면 로컬 포트 고갈(기본 범위 32768~61000) 및 파일 디스크립터, 메모리, CPU 등의 시스템 리소스 낭비를 초래합니다.
tcp_tw_reuse 및 tcp_timestamps 활성화
새로운 아웃바운드 연결에 TIME_WAIT 상태의 소켓을 재사용합니다. TCP 타임스탬프 옵션이 활성화되어 있어야 이전 연결과 새로운 연결의 패킷을 구분할 수 있습니다.
sudo sysctl -w net.ipv4.tcp_tw_reuse=1
sudo sysctl -w net.ipv4.tcp_timestamps=1
tcp_max_tw_buckets 조정
시스템 전체에서 허용할 수 있는 TIME_WAIT 소켓의 최대 개수를 지정합니다. 이 임계값을 초과하면 초과분은 즉시 파괴됩니다.
sudo sysctl -w net.ipv4.tcp_max_tw_buckets=15000
소켓 옵션 SO_LINGER 활용
애플리케이션 레벨에서 l_onoff=1, l_linger=0으로 설정하면 close() 호출 시 4-way handshake를 생략하고 즉시 RST 패킷을 전송하여 연결을 종료합니다. 이는 TIME_WAIT 상태를 회피하지만, 데이터 손실 및 연결 재설정 오류를 유발할 수 있습니다.
#include <sys/socket.h>
#include <stdio.h>
void force_close_connection(int socket_descriptor) {
struct linger termination_option;
termination_option.l_onoff = 1;
termination_option.l_linger = 0;
if (setsockopt(socket_descriptor, SOL_SOCKET, SO_LINGER, &termination_option, sizeof(termination_option)) < 0) {
perror("Failed to apply SO_LINGER configuration");
}
}
서버에서 대량의 TIME_WAIT가 발생하는 원인
서버 측에서 TIME_WAIT가 다량 발생하는 것은 서버가 주로 능동적으로 연결을 종료하고 있음을 의미합니다.
- HTTP Keep-Alive 미사용: HTTP 헤더에
Connection: close가 포함되어 있으면 매 요청 후 서버가 연결을 종료합니다. 클라이언트와 서버 모두 Keep-Alive를 활성화하여 연결 재사용을 유도해야 합니다. - Keep-Alive 타임아웃: 클라이언트가 지정된 시간 내에 새로운 요청을 보내지 않으면 서버가 유휴 연결을 종료합니다.
- 최대 요청 수 도달: 단일 Keep-Alive 연결에서 처리할 수 있는 최대 요청 수를 초과하면 서버가 연결을 강제로 닫습니다. QPS가 높은 환경에서는 해당 임계값을 상향 조정해야 합니다.
대량의 CLOSE_WAIT 발생 원인 및 분석
CLOSE_WAIT는 Passive Close 측, 즉 상대방의 FIN을 수신한 측에서 발생하는 상태입니다. 이 상태에서 FIN을 전송하지 못해 LAST_ACK로 넘어가지 못하는 이유는 애플리케이션이 close() 시스템 콜을 호출하지 않았기 때문입니다.
- 이벤트 루프 미등록: 서버 소켓이나 수락된 클라이언트 소켓이
epoll등의 이벤트 감지기에 등록되지 않아, 연결 종료 이벤트를 처리할 기회를 얻지 못한 경우입니다. - 소켓 누수:
accept()를 통해 소켓을 생성했으나, 비즈니스 로직 수행 중 예외가 발생하거나 교착 상태에 빠져 최종적으로 리소스 해제 코드가 실행되지 않은 경우입니다. - 읽기 버퍼 미처리: 클라이언트가 연결을 끊었으나 서버 측에서 해당 소켓의 읽기 이벤트를 처리하지 않고 방치한 경우입니다.
이 문제는 네트워크 설정이 아닌 애플리케이션 코드의 결함이므로, 리소스 해제 로직과 예외 처리 흐름을 정밀하게 점검해야 합니다.
연결 유지 및 이상 상태 감지
TCP 연결이 수립된 상태에서 클라이언트 호스트가 갑자기 다운되거나 네트워크가 단절되면, 서버는 이를 인지하지 못하고 ESTABLISHED 상태를 무한정 유지하여 리소스를 고갈시킬 수 있습니다. 이를 방지하기 위해 TCP Keep-Alive 메커니즘을 사용합니다.
net.ipv4.tcp_keepalive_time=7200
net.ipv4.tcp_keepalive_intvl=75
net.ipv4.tcp_keepalive_probes=9
기본 설정에 따를 경우, 2시간 동안 유휴 상태인 연결에 대해 75초 간격으로 9번의 탐지 패킷을 전송합니다. 모든 탐지에 실패하면 연결을 종료합니다. 애플리케이션에서 이 기능을 사용하려면 소켓 옵션에 SO_KEEPALIVE를 설정해야 합니다. 더 빠른 장애 감지가 필요한 경우, 애플리케이션 계층에서 자체적인 하트비트 프로토콜을 구현하거나 웹 서버의 유휴 타임아웃 설정을 활용하는 것이 권장됩니다.
프로세스 크래시 시 연결 해제
TCP 연결의 상태는 운영체제 커널이 관리합니다. 따라서 서버의 애플리케이션 프로세스가 비정상적으로 종료되더라도, 커널은 해당 프로세스가 소유한 모든 소켓 리소스를 회수하는 과정에서 자동으로 클라이언트 측에 FIN 패킷을 전송합니다. 이를 통해 정상적인 4-way handshake가 완료되고 연결이 안전하게 해제됩니다.