/엔지니어/트러블슈팅/TIME_WAIT 소켓 고갈 진단 — TCP 연결
트러블슈팅고급linuxtcptime-waitsocket

TIME_WAIT 소켓 고갈 진단 — TCP 연결 상태와 커널 튜닝

TCP 상태 머신에서 TIME_WAIT이 존재하는 이유, ss로 진단하는 법, 임시 포트 고갈과의 관계, tcp_tw_reuse vs 제거된 tcp_tw_recycle, SO_REUSEADDR까지 정확하게 정리합니다.

TIME_WAIT 란?

TIME_WAIT은 TCP 연결을 능동적으로 닫은(active close) 쪽이 거치는 마지막 상태입니다. 연결을 종료한 뒤에도 소켓을 일정 시간(보통 2×MSL, 리눅스에서는 약 60초) 동안 유지합니다. 외부에서 보기엔 "이미 끝난 연결이 왜 이렇게 많이 남아 있지?"라고 의아할 수 있지만, 이는 버그가 아니라 TCP 신뢰성을 위한 의도된 동작입니다.

문제는 고부하 서버(특히 짧은 연결을 대량으로 맺는 리버스 프록시, HTTP 클라이언트)에서 TIME_WAIT 소켓이 수만~수십만 개 쌓이면서 임시 포트(ephemeral port) 고갈로 신규 아웃바운드 연결이 실패하는 상황입니다. cannot assign requested address 같은 에러가 대표적입니다. 이 가이드는 그 원인과 정확한 대응을 다룹니다.

TCP 상태 머신과 4-way handshake

연결 종료는 4-way handshake로 진행되며, 먼저 FIN을 보낸 쪽(active closer)이 TIME_WAIT을 거칩니다.

TEXT
Active Close 측              Passive Close 측
   ESTABLISHED                 ESTABLISHED
       | --- FIN ----------------> |
   FIN_WAIT_1                      |
       | <-- ACK ----------------- |
   FIN_WAIT_2                  CLOSE_WAIT
       | <-- FIN ----------------- |
       | --- ACK ----------------> | LAST_ACK
   TIME_WAIT  (약 60초 대기)      CLOSED
       |
   CLOSED
상태의미
FIN_WAIT_1/2active close 측이 FIN 보내고 상대 응답 대기
CLOSE_WAITpassive close 측이 FIN 받고 자기 close() 대기
LAST_ACKpassive close 측이 FIN 보내고 마지막 ACK 대기
TIME_WAITactive close 측의 마지막 대기 상태

CLOSE_WAIT이 쌓이는 건 TIME_WAIT과 전혀 다른 문제입니다. CLOSE_WAIT 누적은 애플리케이션이 socket을 close() 하지 않는 버그(소켓 누수)의 신호입니다. 커널 튜닝이 아니라 코드를 고쳐야 합니다.

TIME_WAIT이 존재하는 이유

왜 굳이 60초를 기다릴까요? 두 가지 이유가 있습니다.

  1. 마지막 ACK 분실 대비: active closer가 보낸 마지막 ACK가 유실되면 상대가 FIN을 재전송합니다. TIME_WAIT 동안 소켓을 살려둬야 이 재전송 FIN에 다시 ACK를 보낼 수 있습니다. 그렇지 않으면 상대는 RST를 받습니다.
  2. 지연된 옛 패킷이 새 연결에 섞이는 것 방지: 같은 4-튜플(출발IP:포트–목적IP:포트)로 곧바로 새 연결을 열면, 네트워크에 떠돌던 이전 연결의 늦은 패킷이 새 연결로 잘못 들어올 수 있습니다. 2×MSL 대기는 그런 패킷이 모두 소멸하도록 보장합니다.

즉 TIME_WAIT은 안전장치입니다. 무작정 없애려 하지 말고 누가 active close를 하는지부터 봐야 합니다.

진단: ss로 상태 분포 확인

Bash
# 소켓 상태 요약 (가장 빠른 전체 그림)
ss -s

# 출력 예
# TCP:   180344 (estab 412, closed 178900, timewait 178500/0, ...)

timewait 수치가 수만을 넘으면 주의 신호입니다. 상세 분포:

Bash
# 상태별 개수 집계
ss -ant | awk 'NR>1 {print $1}' | sort | uniq -c | sort -rn

# TIME-WAIT 소켓만 보기
ss -tan state time-wait | head

# 특정 목적지로 향하는 TIME-WAIT (어느 백엔드인지)
ss -tan state time-wait dst 10.0.0.20

netstat보다 ss가 빠르고 권장됩니다. 어떤 목적지로 TIME_WAIT이 몰리는지 보면 active close를 누가 하는지 추정할 수 있습니다.

임시 포트(ephemeral port) 고갈

아웃바운드 연결은 출발지 포트를 임시 포트 범위에서 하나씩 빌립니다. 같은 목적지로 향하는 TIME_WAIT 소켓이 이 포트를 점유하면 신규 연결이 포트를 못 받습니다.

Bash
# 현재 임시 포트 범위 확인 (기본 32768~60999, 약 28k개)
cat /proc/sys/net/ipv4/ip_local_port_range
sysctl net.ipv4.ip_local_port_range

# 범위 확장 (포트 풀 증가)
sudo sysctl -w net.ipv4.ip_local_port_range="1024 65535"

여기서 핵심은 4-튜플 유일성입니다. 목적지 IP:포트가 다르면 출발 포트가 같아도 새 연결이 가능합니다. 따라서 단일 백엔드로만 대량 연결하는 경우가 가장 취약합니다. 근본 해법은 다음 순서로 검토합니다.

  1. Keep-Alive / 연결 풀: 매 요청마다 연결을 새로 맺지 말고 재사용 (가장 효과적)
  2. 임시 포트 범위 확장
  3. 백엔드를 여러 IP로 분산
  4. 커널 파라미터 튜닝(아래)

tcp_tw_reuse vs tcp_tw_recycle

여기서 흔한 잘못된 조언을 바로잡아야 합니다.

tcp_tw_recycle — 절대 쓰지 마세요 (제거됨)

net.ipv4.tcp_tw_recycle은 과거 TIME_WAIT을 공격적으로 회수했지만, NAT 뒤의 여러 클라이언트가 같은 IP로 보일 때 타임스탬프 기반 판단이 깨져 연결이 무작위로 거부되는 심각한 버그를 일으켰습니다. 리눅스 커널 4.12에서 완전히 제거되었습니다. 최신 시스템엔 이 파라미터가 존재하지도 않습니다. 인터넷의 오래된 가이드에 이 값이 보이면 무시하세요.

tcp_tw_reuse — 조건부로 안전

net.ipv4.tcp_tw_reuse아웃바운드(클라이언트 측) 연결에 한해, TCP 타임스탬프로 안전이 확인된 TIME_WAIT 소켓을 새 연결에 재사용하도록 허용합니다. recycle과 달리 NAT 문제를 일으키지 않습니다.

Bash
# 1: loopback 등에 적용, 2(기본): 비활성. 보통 1로 설정
sudo sysctl -w net.ipv4.tcp_tw_reuse=1

# 타임스탬프가 켜져 있어야 reuse가 동작 (기본 1)
sysctl net.ipv4.tcp_timestamps

tcp_tw_reuse연결을 거는 쪽의 TIME_WAIT에만 효과가 있습니다. 들어오는 연결을 받는 서버(accept 측)의 TIME_WAIT은 줄여주지 않습니다. 서버 쪽이 문제라면 active close 주체를 클라이언트로 옮기는(HTTP라면 클라이언트가 연결을 닫게) 설계가 더 본질적입니다.

tcp_max_tw_buckets — 상한

Bash
# TIME_WAIT 소켓 총 개수 상한. 초과분은 즉시 폐기되고 로그 경고
sysctl net.ipv4.tcp_max_tw_buckets

상한을 무리하게 낮추면 위에서 설명한 TIME_WAIT의 안전 기능을 깨므로 신중해야 합니다.

SO_REUSEADDR / SO_REUSEPORT

소켓 옵션으로도 관련 문제를 다룹니다.

옵션효과
SO_REUSEADDRTIME_WAIT 상태의 로컬 주소에 bind() 허용. 서버 재시작 시 "Address already in use" 회피의 주된 수단
SO_REUSEPORT여러 프로세스가 같은 포트에 bind, 커널이 연결 분산(멀티 워커)
C
int opt = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

대부분의 서버 프레임워크는 SO_REUSEADDR을 기본으로 켜므로 재시작 시 바인드 실패를 막아줍니다.

권장 적용 절차

Bash
# /etc/sysctl.d/99-tcp-tuning.conf
net.ipv4.ip_local_port_range = 1024 65535
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 30
# net.ipv4.tcp_tw_recycle  ← 존재하지 않음/사용 금지

# 적용
sudo sysctl --system

커널 튜닝은 임시 완화책입니다. 근본 원인은 대개 "연결을 재사용하지 않고 매번 새로 맺는 애플리케이션 동작"입니다. HTTP 클라이언트 Keep-Alive와 연결 풀을 먼저 점검하세요.

정리

항목핵심
TIME_WAIT 주체active close(먼저 FIN 보낸 쪽)만 거침
존재 이유마지막 ACK 재전송 대비 + 옛 패킷 혼입 방지
진단ss -s, ss -tan state time-wait
포트 고갈임시 포트 범위 + 4-튜플 유일성
tcp_tw_recycle커널 4.12에서 제거, 사용 금지
tcp_tw_reuse아웃바운드 한정 안전, 타임스탬프 필요
SO_REUSEADDR재시작 시 bind 실패 회피
근본 해법Keep-Alive / 연결 풀로 연결 재사용

TIME_WAIT은 없애야 할 적이 아니라 이해하고 다뤄야 할 안전장치입니다. "누가 active close를 하는가"와 "연결을 재사용하는가"를 먼저 보면 대부분의 고갈 문제가 풀립니다.

#tcp#time-wait#socket#sysctl#kernel-tuning
편집 안내 · Editorial Note

이 가이드는 AI 도구를 활용해 초안을 구성하고 사람이 명령어·문맥을 검토해 발행했습니다. 운영체제와 도구 버전에 따라 결과가 달라질 수 있으므로 적용 전 공식 문서를 함께 확인하세요. 오류를 발견하시면 이메일로 제보해 주세요.

질문 & 답변 (Q&A)

이 가이드에 대해 궁금한 점을 질문해보세요. 확인 후 답변드립니다.