TCP TIME_WAIT
1. TCP 4-Way Handshake — 연결 종료
- 먼저 FIN을 보낸 쪽이 Active closer
- Active closer가 최종적으로 TIME_WAIT 상태에 진입
- 흐름: FIN_WAIT_1 → FIN_WAIT_2 → TIME_WAIT → CLOSED
- Passive closer 흐름: CLOSE_WAIT → LAST_ACK → CLOSED
2. TIME_WAIT 존재 이유
① 마지막 ACK 유실 보호
- Active closer의 마지막 ACK이 유실되면 Passive closer가 FIN을 재전송
- TIME_WAIT 동안 소켓을 살려둬야 재전송 FIN에 ACK 응답 가능
- 소켓이 없으면 커널이 RST로 응답 → 비정상 종료
② 지연 패킷(Stale packet) 차단
- 동일 4-tuple로 새 연결이 즉시 맺어지면 이전 연결의 떠돌던 패킷이 새 연결에 오염
- 2MSL 대기 동안 이전 패킷들이 네트워크에서 소멸
TIME_WAIT는 "종료 흔적"이 아니라,
"아직 완전히 끝나지 않은 종료를 안전하게 완료하기 위한 대기 상태"
3. TIME_WAIT 없을 때 RST 발생
- 커널은 소켓 테이블에 없는 연결로 패킷이 들어오면 RST로 응답
- TIME_WAIT가 없으면 마지막 ACK 유실 시 재전송 FIN → RST로 끊김
- 지연 패킷이 새 연결에 도달해도 맥락 없는 패킷으로 판단 → RST
4. nginx keepalive와 TIME_WAIT
- keepalive_timeout 0 또는 Connection: close 설정 시
- 응답 후 nginx가 먼저 FIN 전송 → nginx가 Active closer
- nginx 쪽에 TIME_WAIT 누적
- keepalive 활성화 시 보통 클라이언트가 먼저 끊음 → TIME_WAIT가 클라이언트에 생김
# TIME_WAIT 소켓 확인
ss -tan state time-wait
# Local Address가 :80/:443 → nginx가 active closer
# Local Address가 높은 포트 → 클라이언트가 active closer
5. tcp_tw_recycle (제거된 옵션)
- Linux 4.12에서 완전히 제거
- 원래 의도: TIME_WAIT 소켓을 즉시 재활용
- 문제: per-host timestamp 캐시를 사용
- 같은 IP에서 온 패킷의 timestamp가 역전되면 조용히 DROP
- NAT 환경에서 치명적 — 뒤에 있는 클라이언트들이 SYN 자체가 DROP됨
NAT 뒤 클라이언트 A timestamp: 1000 → 캐시: { IP → 1000 }
NAT 뒤 클라이언트 B timestamp: 800 → 800 < 1000 → DROP (연결 불가)
옵션 현재 지원 NAT 안전성 적용 대상
| tcp_tw_reuse | O | 비교적 안전 | outgoing 연결만 |
| tcp_tw_recycle | X (4.12 제거) | 위험 | incoming/outgoing 모두 |
6. 현대 아키텍처에서 서버 측 TIME_WAIT 문제
웹서버(nginx)와 앱서버(gunicorn 등)가 같은 호스트에 있을 때:
클라이언트 → nginx (80/443)
↓
nginx → 앱서버 (127.0.0.1:8000)
- nginx → 앱서버 구간에서 nginx가 클라이언트 역할
- upstream keepalive 없으면 요청마다 새 TCP 연결 → ephemeral 포트 고갈 위험
해결: upstream keepalive 설정
upstream app_server {
server 127.0.0.1:8000;
keepalive 32; # nginx → 앱서버 연결 재사용
}
구간 keepalive 없을 때 TIME_WAIT 위치
| 클라이언트 → nginx | nginx (:80/:443) |
| nginx → 앱서버 | nginx (ephemeral port) |