1. 라이브러리
  2. TCP와 UDP
  3. TCP 심층 분석

업데이트됨 1개월 전

TCP 연결을 닫습니다. netstat을 확인합니다. 연결이 여전히 그대로 남아 있고, TIME_WAIT로 표시되어 있습니다. 기다려도 사라지지 않습니다. 때로는 수천 개씩—진작에 끝났어야 할 소켓들이 연결 테이블에 유령처럼 맴돌고 있습니다.

버그가 아닙니다. TIME_WAIT는 TCP가 잊으려 하지 않는 것입니다. 연결을 닫은 후 60초 동안, TCP는 그 연결이 있었다는 사실을 기억합니다—네트워크 어딘가에 아직 전달되지 않은 패킷이 남아 있을 수 있기 때문입니다.

TIME_WAIT가 정확히 무엇인가

TCP 연결이 닫힐 때, 종료를 먼저 시작한 쪽은 4-way 핸드셰이크를 완료한 후 TIME_WAIT 상태로 진입합니다. 소켓은 커널의 연결 테이블에 그대로 남아, 로컬 주소, 로컬 포트, 원격 주소, 원격 포트의 정확한 조합이 즉시 재사용되지 않도록 막습니다.

지속 시간은 2MSL—최대 세그먼트 수명(Maximum Segment Lifetime)의 두 배입니다. MSL은 TCP 세그먼트가 네트워크에서 폐기되기 전까지 살아남을 수 있는 이론적 최대 시간으로, 보통 30초입니다. 그래서 대부분의 시스템에서 TIME_WAIT는 60초 동안 유지되지만, 30초나 120초를 쓰는 시스템도 있습니다.

TIME_WAIT 동안 소켓은 거의 아무것도 소비하지 않습니다—버퍼도 없고, 애플리케이션 상태도 없습니다. 커널 연결 테이블에 항목 하나를 차지하며 그 특정 4-튜플의 재사용을 막을 뿐입니다.

TCP가 잊지 않는 이유

TIME_WAIT는 두 가지 이유로 존재하며, 둘 다 혼란을 막기 위한 것입니다.

손실된 ACK 문제

TCP 연결 종료는 ACK로 마무리됩니다. 그 마지막 ACK가 유실되면, 상대방은 FIN을 재전송합니다. TIME_WAIT가 없다면 이미 연결 상태를 삭제한 시스템은 재전송된 FIN에 RST로 응답하게 됩니다. 그러면 상대방은 연결이 실제로 닫혔는지 알 수 없습니다.

TIME_WAIT 상태를 유지함으로써, 시스템은 재전송된 FIN에 제대로 응답할 수 있습니다. 양쪽 모두 연결이 끝났다는 것에 동의합니다. 모호함이 없습니다.

떠도는 패킷 문제

패킷은 네트워크를 통해 예상치 못한 경로를 취합니다. 지연되고, 우회되고, 복제됩니다. 이전 연결에서 출발한 패킷이 같은 목적지로 같은 포트를 쓰는 새 연결이 만들어진 뒤에도 인터넷 어딘가를 떠돌고 있을 수 있습니다.

그 낡은 패킷이 도착하면, 새 연결의 유효한 데이터로 받아들여질 수 있습니다. 데이터 손상. 조용하고, 보이지 않고, 치명적입니다.

TIME_WAIT는 이런 떠도는 패킷들이 모두 만료될 때까지 해당 주소/포트 조합을 재사용하지 않음으로써 이를 방지합니다. 2MSL이면 이전 연결의 모든 패킷이 사라졌음이 보장됩니다.

TIME_WAIT가 문제가 될 때

TIME_WAIT는 정상적인 현상입니다. netstat에서 수천 개의 TIME_WAIT 소켓을 보더라도, 그건 그저 시스템이 연결을 활발히 닫고 있다는 뜻입니다. 아무 문제 없습니다.

문제는 포트가 부족해질 때 생깁니다.

TIME_WAIT 소켓 하나는 임시 포트 하나를 차지합니다. 대부분의 시스템은 약 28,000개의 임시 포트를 제공합니다(보통 32,768~60,999). 60초 안에 같은 목적지로 28,000개의 연결을 만들면 포트 공간이 바닥납니다. 새 연결은 "요청한 주소를 할당할 수 없습니다" 오류와 함께 실패합니다.

이런 상황이 발생하기 쉬운 경우:

  • 단일 API 엔드포인트에 요청을 쏟아내는 HTTP 클라이언트
  • 대용량 트래픽을 전달하는 로드 밸런서
  • 프록시 서버
  • 연결 풀링이 제대로 구현되지 않은 애플리케이션

핵심은 이것입니다: TIME_WAIT는 정확한 4-튜플의 재사용만 막습니다. 다른 목적지로의 연결은 충돌하지 않습니다. 수백 개의 서로 다른 서버와 통신하는 클라이언트라면 포트가 바닥날 일이 거의 없습니다. 하지만 하나의 서버에 초당 수천 건의 요청을 보내는 클라이언트라면 얘기가 다릅니다.

TIME_WAIT 다루기

tcp_tw_reuse (Linux)

net.ipv4.tcp_tw_reuse 커널 파라미터를 설정하면, TCP 타임스탬프가 새 연결임을 확인해줄 때 TIME_WAIT 소켓을 새 아웃바운드 연결에 재사용할 수 있습니다. 안전한 방법입니다—타임스탬프 덕분에 오래된 패킷이 수락되지 않습니다—그리고 클라이언트의 포트 소진 문제를 사실상 해결합니다.

SO_REUSEADDR

이 소켓 옵션을 사용하면 서버가 이전 인스턴스에서 TIME_WAIT 상태인 소켓이 있는 포트에 바인딩할 수 있습니다. 60초를 기다리지 않고 서버를 재시작할 때 유용합니다. 아웃바운드 포트 소진 문제에는 도움이 되지 않습니다.

연결 풀링

근본적인 해결책은 대개 아키텍처 수준에 있습니다: 연결을 계속 생성하고 닫지 마세요. HTTP keep-alive, HTTP/2 멀티플렉싱, 데이터베이스 연결 풀—이 모두가 연결을 닫는 대신 재사용함으로써 TIME_WAIT 자체를 줄입니다.

하나의 지속적인 연결이 수천 개의 단명 연결보다 낫습니다.

언제 걱정해야 할까?

TIME_WAIT 소켓이 존재한다: 정상.

TIME_WAIT 소켓이 새 연결을 막는다: 문제.

인바운드 연결을 받는 서버라면 TIME_WAIT가 문제될 일이 거의 없습니다. 클라이언트마다 다른 소스 포트를 쓰기 때문에 충돌이 생기지 않습니다. 서버의 리스닝 포트는 TIME_WAIT에 진입하지 않습니다.

같은 목적지로 빠르게 아웃바운드 연결을 만드는 클라이언트나 프록시라면, TIME_WAIT가 포트를 소진시킬 수 있습니다. tcp_tw_reuse를 활성화하거나, 연결 풀링을 도입하거나, 또는 둘 다 적용하세요.

netstat 출력에 맴도는 소켓들은 시스템을 괴롭히는 유령이 아닙니다. TCP가 신중하게 행동하는 것입니다—네트워크가 진정으로 다음 단계로 넘어갔음을 확인할 수 있을 만큼, 딱 그만큼만 연결을 기억하는 것입니다.

TIME_WAIT 자주 묻는 질문

왜 TIME_WAIT는 60초이고 더 짧게 할 수 없나요?

2MSL—최대 세그먼트 수명의 두 배이기 때문입니다. MSL은 패킷이 이론상 네트워크에서 살아남을 수 있는 최대 시간입니다. 그 두 배를 기다리면, 포트를 재사용하기 전에 이전 연결의 모든 패킷이 소멸했음이 보장됩니다. 더 짧게 하면 새 연결에 오래된 패킷이 끼어들 위험이 생깁니다.

TIME_WAIT를 그냥 꺼버릴 수 있나요?

아닙니다. TIME_WAIT는 TCP의 신뢰성 보장에 근본적인 요소이기 때문에 0으로 설정할 수 없습니다. tcp_tw_reuse나 연결 풀링으로 영향을 줄일 수는 있지만, 완전히 없애버리면 TCP가 지연 패킷으로 인한 데이터 손상을 막는 능력이 사라집니다.

서버와 클라이언트 중 어느 쪽이 TIME_WAIT에 진입하나요?

종료를 먼저 시작하는 쪽—첫 번째 FIN을 보내는 쪽—이 TIME_WAIT에 진입합니다. keep-alive 없는 HTTP/1.0에서는 보통 서버가 먼저 닫으므로 서버에 TIME_WAIT가 쌓입니다. 현대적인 keep-alive HTTP에서는 클라이언트가 유휴 연결을 먼저 닫는 경우가 많아 클라이언트에 TIME_WAIT가 쌓입니다.

연결 풀링을 쓰는데도 왜 TIME_WAIT 소켓이 보이나요?

연결 풀도 결국 유휴 연결을 닫게 되고, 그때마다 TIME_WAIT 소켓이 생깁니다. 이건 정상입니다. 풀링의 목표는 TIME_WAIT를 완전히 없애는 것이 아닙니다—TIME_WAIT가 포트를 소진하지 않을 만큼 연결 생성·종료 빈도를 낮추는 것입니다.

이 페이지가 도움이 되었나요?

😔
🤨
😃
TIME_WAIT 상태 해설 • 라이브러리 • Connected