TCP 연결 상태 완벽 가이드 | ESTABLISHED·TIME_WAIT·CLOSE_WAIT 총정리
이 글의 핵심
TCP 11가지 연결 상태(LISTEN, SYN_SENT, ESTABLISHED, FIN_WAIT, TIME_WAIT 등)의 동작 원리와 상태 전이 다이어그램. netstat으로 네트워크 디버깅하는 실전 가이드.
들어가며: TCP 상태를 이해해야 하는 이유
네트워크 애플리케이션을 개발하거나 디버깅할 때 TCP 연결 상태를 이해하는 것은 필수입니다. netstat 명령어로 본 ESTABLISHED, TIME_WAIT, CLOSE_WAIT 같은 상태들이 무엇을 의미하는지, 언제 발생하는지 알아야 성능 문제와 버그를 해결할 수 있습니다.
이 글에서 다룰 내용
- TCP 11가지 연결 상태
- 3-Way Handshake와 4-Way Handshake
- 상태 전이 다이어그램
- netstat/ss로 상태 확인
- 실전 디버깅 시나리오
1. TCP 연결 상태 개요
TCP 상태 머신
TCP는 상태 기반 프로토콜입니다. 각 소켓은 11가지 상태 중 하나에 있으며, 패킷 송수신에 따라 상태가 전이됩니다.
11가지 TCP 상태
| 상태 | 설명 | 발생 시점 |
|---|---|---|
| CLOSED | 연결 없음 | 초기 상태 |
| LISTEN | 연결 대기 중 | 서버가 포트를 열고 대기 |
| SYN_SENT | 연결 요청 전송 | 클라이언트가 SYN 전송 후 |
| SYN_RECEIVED | 연결 요청 수신 | 서버가 SYN 받고 SYN-ACK 전송 |
| ESTABLISHED | 연결 수립 완료 | 3-Way Handshake 완료 |
| FIN_WAIT_1 | 종료 요청 전송 | 능동적 종료 시작 (FIN 전송) |
| FIN_WAIT_2 | 종료 요청 승인 대기 | FIN에 대한 ACK 수신 |
| CLOSE_WAIT | 종료 요청 수신 | 상대방이 FIN 전송 (수동적 종료) |
| CLOSING | 동시 종료 | 양이 동시에 FIN 전송 |
| LAST_ACK | 최종 ACK 대기 | CLOSE_WAIT에서 FIN 전송 후 |
| TIME_WAIT | 연결 종료 대기 | 능동적 종료 완료, 2MSL 대기 |
2. 연결 수립: 3-Way Handshake
왜 3-Way인가? (2-Way가 아니라)
TCP는 양방향 신뢰성 있는 통신을 제공합니다. 3-Way Handshake는 다음을 보장합니다:
- 클라이언트 → 서버 경로 확인: SYN
- 서버 → 클라이언트 경로 확인: SYN-ACK
- 양방향 통신 준비 완료: ACK
2-Way가 불충분한 이유:
2-Way 시나리오 (불안전):
Client → Server: SYN
Server → Client: SYN-ACK
문제:
- 서버는 SYN-ACK를 보냈지만, 클라이언트가 받았는지 알 수 없음
- 만약 SYN-ACK가 손실되면?
→ 서버는 ESTABLISHED 상태로 전환
→ 클라이언트는 여전히 SYN_SENT
→ 서버가 데이터를 보내도 클라이언트가 받지 못함
3-Way 해결책:
Client → Server: SYN
Server → Client: SYN-ACK
Client → Server: ACK ← 이것이 핵심!
- ACK를 받은 서버만 ESTABLISHED로 전환
- 클라이언트도 SYN-ACK를 받았으므로 ESTABLISHED
- 양방향 통신 확실히 가능
3-Way Handshake 과정
sequenceDiagram
participant Client
participant Server
Note over Client: CLOSED
Note over Server: LISTEN
Client->>Server: 1. SYN (seq=100)
Note over Client: SYN_SENT
Note right of Client: 초기 시퀀스 번호(ISN) 전송
커널이 랜덤 생성
Server->>Client: 2. SYN-ACK (seq=200, ack=101)
Note over Server: SYN_RECEIVED
Note left of Server: 서버의 ISN + 클라이언트 ACK
백로그 큐에 추가
Client->>Server: 3. ACK (ack=201)
Note over Client: ESTABLISHED
Note over Server: ESTABLISHED
Note right of Client: accept() 큐로 이동
애플리케이션이 처리 가능
Note over Client,Server: 데이터 전송 가능
상태 전이 및 커널 동작
클라이언트: CLOSED → SYN_SENT → ESTABLISHED
서버: CLOSED → LISTEN → SYN_RECEIVED → ESTABLISHED
클라이언트 측 커널 동작:
// socket() 호출: TCB (Transmission Control Block) 할당
int sock = socket(AF_INET, SOCK_STREAM, 0);
// 상태: CLOSED
// 커널: TCB 생성, 소켓 구조체 초기화
// connect() 호출: SYN 전송
connect(sock, (struct sockaddr*)&addr, sizeof(addr));
// 1. 초기 시퀀스 번호(ISN) 생성: 현재 시간 + 난수
// 보안: 예측 불가능한 ISN으로 SYN Flood 방어
// 2. SYN 패킷 생성:
// TCP 헤더: SYN 플래그 설정, seq = ISN
// 3. IP 계층으로 전달 → 네트워크로 송신
// 4. 상태: CLOSED → SYN_SENT
// 5. 재전송 타이머 시작 (보통 1초, 2초, 4초... 최대 75초)
// 6. SYN-ACK 대기 (connect()는 블로킹)
// SYN-ACK 수신 시:
// 1. ack 값 검증: ack == ISN + 1인지 확인
// 2. ACK 패킷 생성 및 전송
// 3. 상태: SYN_SENT → ESTABLISHED
// 4. connect() 반환 (블로킹 해제)
서버 측 커널 동작:
// socket() + bind() + listen() 호출
int server = socket(AF_INET, SOCK_STREAM, 0);
bind(server, (struct sockaddr*)&addr, sizeof(addr));
listen(server, 128); // 백로그 큐 크기
// 상태: CLOSED → LISTEN
// 커널:
// - SYN 큐 (미완성 연결, 크기: tcp_max_syn_backlog)
// - Accept 큐 (완성된 연결, 크기: listen backlog)
// SYN 수신 시:
// 1. SYN 큐에 공간 있는지 확인
// - 가득 차면: SYN Cookies 사용 또는 드롭
// 2. 반-완성 연결 생성 (Half-Open Connection)
// 3. 서버의 ISN 생성
// 4. SYN-ACK 패킷 생성 및 전송:
// seq = 서버 ISN
// ack = 클라이언트 ISN + 1
// 5. 상태: LISTEN → SYN_RECEIVED
// 6. 재전송 타이머 시작 (SYN-ACK 재전송)
// ACK 수신 시:
// 1. SYN 큐에서 제거
// 2. Accept 큐로 이동
// 3. 상태: SYN_RECEIVED → ESTABLISHED
// 4. accept() 호출 시 Accept 큐에서 꺼냄
// - 큐가 비어있으면 accept()는 블로킹
int client = accept(server, ...);
// accept() 반환: Accept 큐에서 ESTABLISHED 소켓 반환
백로그 큐 관리:
[SYN 큐] [Accept 큐]
↓ ↓
┌──────────────────────┐ ┌─────────────────────┐
│ SYN_RECEIVED 연결들 │→│ ESTABLISHED 연결들 │→ accept()
│ (미완성) │ │ (완성, 대기 중) │
│ tcp_max_syn_backlog │ │ listen backlog │
└──────────────────────┘ └─────────────────────┘
SYN Flood 공격 시:
- SYN 큐가 가득 참 → 새 SYN 드롭
- 정상 클라이언트도 연결 불가 (DoS)
해결책:
1. SYN Cookies 활성화
- SYN 큐 우회, 무상태(Stateless) 처리
- ISN에 연결 정보 인코딩
2. 백로그 증가
sudo sysctl -w net.ipv4.tcp_max_syn_backlog=8192
sudo sysctl -w net.core.somaxconn=4096
시퀀스 번호(Sequence Number)의 중요성:
왜 랜덤 ISN을 사용하나?
고정 ISN (예: 0)의 위험:
1. 이전 연결의 지연된 패킷이 새 연결에 섞일 수 있음
2. 공격자가 ISN을 예측하여 위조 패킷 전송 가능
랜덤 ISN (RFC 6528):
ISN = M + F(src_ip, src_port, dst_ip, dst_port, secret)
M = 4 마이크로초마다 증가하는 카운터
F = 해시 함수 (MD5 또는 SHA-1)
secret = 부팅 시 생성된 랜덤 키
→ 32비트 공간을 약 4.55시간에 순환
→ 예측 불가능 (보안)
→ 이전 연결과 충돌 방지
Python으로 3-Way Handshake 관찰
import socket
import time
def client_connect():
"""클라이언트 연결 과정"""
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print("📍 상태: CLOSED")
# connect() 호출 시 SYN 전송
print("📤 SYN 전송 중...")
print("📍 상태: SYN_SENT")
sock.connect(('example.com', 80))
print("✅ 연결 수립")
print("📍 상태: ESTABLISHED")
return sock
def server_listen():
"""서버 리스닝 과정"""
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(('0.0.0.0', 8080))
print("📍 상태: CLOSED")
server.listen(5)
print("👂 포트 8080에서 대기 중...")
print("📍 상태: LISTEN")
client, addr = server.accept()
print(f"✅ {addr}로부터 연결 수립")
print("📍 상태: ESTABLISHED")
return client
패킷 레벨 분석
# tcpdump로 3-Way Handshake 캡처
sudo tcpdump -i any -nn 'tcp port 80' -c 3
# 출력:
# 1. 192.168.1.100.54321 > 93.184.216.34.80: Flags [S], seq 100
# 2. 93.184.216.34.80 > 192.168.1.100.54321: Flags [S.], seq 200, ack 101
# 3. 192.168.1.100.54321 > 93.184.216.34.80: Flags [.], ack 201
3. 연결 종료: 4-Way Handshake
정상 종료 (4-Way Handshake)
sequenceDiagram
participant Client as Client\n(능동적 종료)
participant Server as Server\n(수동적 종료)
Note over Client,Server: ESTABLISHED
Client->>Server: 1. FIN (seq=300)
Note over Client: FIN_WAIT_1
Server->>Client: 2. ACK (ack=301)
Note over Client: FIN_WAIT_2
Note over Server: CLOSE_WAIT
Note over Server: 애플리케이션이\nclose() 호출할 때까지 대기
Server->>Client: 3. FIN (seq=400)
Note over Server: LAST_ACK
Client->>Server: 4. ACK (ack=401)
Note over Client: TIME_WAIT
Note over Server: CLOSED
Note over Client: 2MSL (60초) 대기
Note over Client: CLOSED
능동적 종료 vs 수동적 종료
# 능동적 종료 (Active Close)
# - close()를 먼저 호출하는 쪽
# - FIN_WAIT_1 → FIN_WAIT_2 → TIME_WAIT → CLOSED
client_socket.close() # 클라이언트가 먼저 종료
# 상태: ESTABLISHED → FIN_WAIT_1 → FIN_WAIT_2 → TIME_WAIT → CLOSED
# 수동적 종료 (Passive Close)
# - 상대방의 FIN을 받는 쪽
# - CLOSE_WAIT → LAST_ACK → CLOSED
# 서버는 클라이언트의 FIN을 받음
# 상태: ESTABLISHED → CLOSE_WAIT
# 애플리케이션이 close() 호출
server_socket.close()
# 상태: CLOSE_WAIT → LAST_ACK → CLOSED
4. 11가지 TCP 상태 상세 설명
1. CLOSED
초기 상태. 연결이 존재하지 않음.
2. LISTEN
서버가 특정 포트에서 연결 요청을 기다리는 상태.
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('0.0.0.0', 8080))
server.listen(5) # 백로그 큐 크기 5
# 이 시점에서 상태: LISTEN
print("Server is LISTEN on port 8080")
# 확인
netstat -an | grep 8080
# tcp4 0 0 *.8080 *.* LISTEN
3. SYN_SENT
클라이언트가 SYN 패킷을 전송하고 SYN-ACK를 기다리는 상태.
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setblocking(False) # 논블로킹 모드
try:
sock.connect(('example.com', 80))
except BlockingIOError:
# 이 시점에서 상태: SYN_SENT
print("Connection in progress (SYN_SENT)")
# select로 연결 완료 대기
import select
_, writable, _ = select.select([], [sock], [], 5)
if writable:
print("Connection ESTABLISHED")
SYN_SENT가 오래 유지되는 경우:
- 방화벽이 SYN 패킷 차단
- 서버가 다운되어 응답 없음
- 네트워크 지연
4. SYN_RECEIVED
서버가 SYN을 받고 SYN-ACK를 전송한 후, 최종 ACK를 기다리는 상태.
SYN Flood 공격 상세 분석:
flowchart LR
Attacker[공격자] -->|대량 SYN| Server[서버]
Server -->|SYN-ACK| Void[응답 없음]
Note1["서버의 SYN_RECEIVED\n큐가 가득 참"]
style Attacker fill:#ff6b6b
style Void fill:#ff6b6b
SYN Flood 공격 내부 메커니즘:
일반 연결:
Client → Server: SYN
Server → Client: SYN-ACK
Client → Server: ACK ← 정상 완료
Server: SYN 큐에서 Accept 큐로 이동
SYN Flood 공격:
Attacker → Server: SYN (위조 IP: 1.1.1.1)
Server → 1.1.1.1: SYN-ACK ← 응답 없음 (위조 IP)
Server: SYN 큐에 SYN_RECEIVED 상태로 대기
Attacker → Server: SYN (위조 IP: 2.2.2.2)
Server → 2.2.2.2: SYN-ACK ← 응답 없음
Server: SYN 큐에 추가
... 수천 개 반복 ...
Server: SYN 큐 가득 참 (1024개)
→ 정상 클라이언트 SYN도 드롭
→ 서비스 거부 (DoS)
SYN 큐 메모리 소비:
각 SYN_RECEIVED 연결:
- TCB (Transmission Control Block): ~280 bytes
- 재전송 타이머
- 소켓 구조체
1024개 × 280 bytes = ~280KB (작지만 리소스 고갈)
재전송 타이머:
1초 → 2초 → 4초 → 8초 → 16초 → 32초
총 63초 동안 SYN 큐 점유
공격 효과:
- 초당 1000개 SYN
- 각각 63초 유지
- 필요 큐 크기: 63,000개
- 서버 마비
SYN Cookies 방어 메커니즘:
SYN Cookies 원리 (RFC 4987):
일반 방식 (상태 유지):
1. SYN 수신 → TCB 할당 → SYN 큐 저장
2. SYN-ACK 전송
3. ACK 수신 → TCB 조회 → Accept 큐 이동
문제: SYN 큐 메모리 소비 (공격에 취약)
SYN Cookies 방식 (무상태):
1. SYN 수신 → TCB 할당 안 함!
2. ISN 계산 (Stateless):
ISN = hash(
src_ip, src_port,
dst_ip, dst_port,
timestamp, ← 시간 정보
secret ← 서버 비밀키
) + MSS_encoding + timestamp_encoding
ISN 구조 (32 bits):
[31:27] timestamp (5 bits, 32초 순환)
[26:24] MSS encoding (3 bits, 8가지 MSS)
[23:0] hash 값 (24 bits)
3. SYN-ACK 전송 (seq=ISN)
→ SYN 큐에 저장 안 함! (메모리 절약)
4. ACK 수신 시:
- ack 값에서 정보 복원:
* ack - 1 = ISN
* ISN에서 timestamp, MSS 추출
* hash 재계산하여 검증
검증:
calculated_hash = hash(...)
if (ack - 1) matches calculated_hash:
→ 정상 연결
→ ESTABLISHED 생성
else:
→ 위조 패킷
→ 드롭
장점:
- SYN 큐 불필요 (메모리 무제한)
- SYN Flood 공격 무효화
단점:
- TCP 옵션 손실 (Window Scale, SACK 등)
- MSS만 8가지로 제한
- 성능 약간 저하
활성화:
Linux:
net.ipv4.tcp_syncookies=1
동작:
- SYN 큐 < 75%: 일반 방식
- SYN 큐 ≥ 75%: SYN Cookies 자동 활성화
확인:
$ netstat -s | grep "SYNs to LISTEN"
123 SYNs to LISTEN sockets dropped
456 times used SYN cookies
백로그 큐 (Backlog Queue) 상세:
listen(fd, backlog) 내부 구조:
Linux 커널 큐 2개:
1. SYN Queue (미완성 연결):
크기: net.ipv4.tcp_max_syn_backlog (기본 1024)
상태: SYN_RECEIVED
[SYN1] [SYN2] [SYN3] ... [SYNn]
↓
각 항목:
- 클라이언트 IP:Port
- ISN
- 재전송 타이머
- TCP 옵션
2. Accept Queue (완성된 연결):
크기: min(backlog, net.core.somaxconn)
상태: ESTABLISHED
[CONN1] [CONN2] [CONN3] ... [CONNn]
↓
accept() 호출 시 꺼냄
흐름:
SYN 수신 → SYN Queue 추가 (SYN_RECEIVED)
ACK 수신 → Accept Queue 이동 (ESTABLISHED)
accept() → Accept Queue에서 제거
큐 오버플로우:
SYN Queue 가득:
- SYN Cookies 활성화 (tcp_syncookies=1)
- 또는 SYN 드롭
Accept Queue 가득:
- 새 ACK 무시
- 클라이언트는 SYN-ACK 재전송 대기
- 클라이언트 타임아웃 발생
튜닝:
# SYN 큐 증가
net.ipv4.tcp_max_syn_backlog=8192
# Accept 큐 증가
net.core.somaxconn=4096
# 애플리케이션
listen(fd, 4096) # 큐 크기 명시
확인:
ss -lnt
State Recv-Q Send-Q Local Address:Port
LISTEN 0 128 0.0.0.0:80
↑ ↑
Accept큐 Accept큐 최대 크기
현재 대기 (listen backlog)
# SYN_RECEIVED 상태 확인
netstat -an | grep SYN_RECV
# SYN Flood 방어 (Linux)
sudo sysctl -w net.ipv4.tcp_syncookies=1
sudo sysctl -w net.ipv4.tcp_max_syn_backlog=8192
# 백로그 확인
ss -lnt
5. ESTABLISHED
연결이 수립되어 데이터를 주고받을 수 있는 상태.
# ESTABLISHED 상태에서 데이터 송수신
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('example.com', 80))
# 상태: ESTABLISHED
sock.send(b'GET / HTTP/1.1\r\nHost: example.com\r\n\r\n')
response = sock.recv(4096)
print(response.decode())
# ESTABLISHED 연결 수 확인
netstat -an | grep ESTABLISHED | wc -l
# 특정 포트의 ESTABLISHED 연결
netstat -an | grep ':80.*ESTABLISHED'
6. FIN_WAIT_1
능동적 종료를 시작하여 FIN을 전송한 상태. ACK를 기다림.
# 클라이언트가 먼저 종료
client_socket.close() # FIN 전송
# 상태: ESTABLISHED → FIN_WAIT_1
# 서버의 ACK 대기
7. FIN_WAIT_2
FIN에 대한 ACK를 받았지만, 상대방의 FIN을 기다리는 상태.
sequenceDiagram
participant C as Client
participant S as Server
Note over C,S: ESTABLISHED
C->>S: FIN
Note over C: FIN_WAIT_1
S->>C: ACK
Note over C: FIN_WAIT_2
Note over S: CLOSE_WAIT
Note over S: 서버가 close() 호출할 때까지\nFIN_WAIT_2 유지
FIN_WAIT_2 타임아웃:
# Linux에서 FIN_WAIT_2 타임아웃 설정 (기본 60초)
sudo sysctl -w net.ipv4.tcp_fin_timeout=30
8. CLOSE_WAIT - 가장 흔한 소켓 리소스 누수의 원인
상대방이 FIN을 보내 연결 종료를 요청한 상태.
애플리케이션이 close()를 호출해야 함.
CLOSE_WAIT vs TIME_WAIT 핵심 차이:
TIME_WAIT:
- 능동적 종료 측 (close()를 먼저 호출)
- 커널이 자동 관리
- 60초 후 자동 정리
- 정상적인 상태 (걱정 불필요)
CLOSE_WAIT:
- 수동적 종료 측 (FIN을 먼저 받음)
- 애플리케이션이 명시적으로 close() 호출 필요
- 애플리케이션이 close()를 안 부르면 영원히 유지
- 버그의 신호! (리소스 누수)
CLOSE_WAIT 발생 메커니즘:
# 서버 코드
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('0.0.0.0', 8080))
server.listen(5)
client_sock, addr = server.accept()
# 상태: ESTABLISHED
# 클라이언트가 연결을 닫음 (FIN 전송)
# 커널이 자동으로 ACK 응답
# 상태: ESTABLISHED → CLOSE_WAIT
# ⚠️ 문제: 애플리케이션이 close()를 호출하지 않음
# 상태: CLOSE_WAIT (영구 유지)
# 소켓 파일 디스크립터 누수
# 메모리 누수
# 포트 고갈 (서버 측에서는 문제 적지만)
# ✅ 해결: 반드시 close() 호출
client_sock.close()
# 상태: CLOSE_WAIT → LAST_ACK → CLOSED
CLOSE_WAIT 문제 (소켓 리소스 누수):
# ❌ 나쁜 예 1: Exception 발생 시 close() 누락
def handle_request(sock):
data = sock.recv(1024)
if not data:
return # ❌ close() 호출 안 함
process(data)
sock.send(b'OK')
sock.close() # ❌ 여기 도달하지 못할 수 있음
# ❌ 나쁜 예 2: 무한 루프에서 close() 누락
def handle_client(sock):
while True:
data = sock.recv(1024)
if not data:
break # ❌ close() 없이 종료
process(data)
# ❌ 나쁜 예 3: 멀티스레드에서 소켓 관리 실패
def worker(sock):
try:
process(sock)
except Exception as e:
log_error(e)
# ❌ Exception 발생 시 close() 누락
# ✅ 좋은 예 1: try-finally
def handle_request(sock):
try:
data = sock.recv(1024)
if not data:
return
process(data)
sock.send(b'OK')
finally:
sock.close() # ✅ 항상 실행됨
# ✅ 좋은 예 2: Context Manager
def handle_request(sock):
with sock: # ✅ __exit__에서 자동 close()
data = sock.recv(1024)
process(data)
sock.send(b'OK')
# ✅ 좋은 예 3: recv(0) 감지
def handle_client(sock):
try:
while True:
data = sock.recv(1024)
if not data: # FIN 수신 (상대방이 닫음)
break
process(data)
finally:
sock.close() # ✅ 반드시 닫기
CLOSE_WAIT 디버깅 실전 가이드:
# 1. CLOSE_WAIT 소켓 수 확인
netstat -an | grep CLOSE_WAIT | wc -l
# 100개 이상이면 문제!
# 2. 프로세스별 소켓 수 확인
lsof -p <PID> | grep TCP | wc -l
# 또는
ls -l /proc/<PID>/fd | grep socket | wc -l
# 3. 어떤 소켓이 CLOSE_WAIT인지 확인
sudo ss -tanp | grep CLOSE_WAIT
# 출력 예시:
# CLOSE-WAIT 0 0 192.168.1.100:8080 192.168.1.200:54321 users:(("python",pid=1234,fd=5))
# ↑ 서버 포트 ↑ 클라이언트 포트 ↑ 프로세스 정보
# 4. 특정 프로세스의 소켓 상태
sudo lsof -p <PID> -a -i TCP
# 출력 예시:
# python 1234 user 5u IPv4 12345 TCP *:8080 (LISTEN)
# python 1234 user 6u IPv4 12346 TCP 192.168.1.100:8080->192.168.1.200:54321 (CLOSE_WAIT)
# ↑ 문제!
# 5. 파일 디스크립터 한계 확인
ulimit -n
# 1024 ← 기본값
# CLOSE_WAIT가 1024개 쌓이면 새 연결 불가!
# 6. 커널 소켓 통계
cat /proc/net/sockstat
# 출력:
# TCP: inuse 150 orphan 0 tw 50 alloc 200 mem 10
# ↑ 사용 중 ↑ 고아 ↑ TIME_WAIT ↑ 할당됨
실제 프로덕션 사례:
# 사례 1: Django 뷰에서 외부 API 호출
import requests
def my_view(request):
response = requests.get('https://api.example.com/data')
# ❌ 문제: requests는 내부적으로 소켓 사용
# Connection: close로 응답 시 서버가 먼저 FIN 전송
# requests가 제대로 정리 안 하면 CLOSE_WAIT 누적
# ✅ 해결책 1: Session 사용 (연결 재사용)
session = requests.Session()
response = session.get('https://api.example.com/data')
session.close() # 명시적 종료
# ✅ 해결책 2: Context Manager
with requests.Session() as session:
response = session.get('https://api.example.com/data')
# 사례 2: 데이터베이스 연결 누수
import psycopg2
def query_database():
conn = psycopg2.connect(
host='localhost',
database='mydb',
user='user',
password='pass'
)
cursor = conn.cursor()
cursor.execute('SELECT * FROM users')
results = cursor.fetchall()
# ❌ conn.close() 누락!
# PostgreSQL 서버에서 CLOSE_WAIT 누적
# ✅ 해결책
try:
# ... 쿼리 실행 ...
finally:
cursor.close()
conn.close()
# 사례 3: WebSocket 연결 관리
import websocket
def handle_websocket():
ws = websocket.create_connection('ws://server.com/socket')
ws.send('Hello')
result = ws.recv()
# ❌ ws.close() 누락!
# ✅ 해결책
try:
ws.send('Hello')
result = ws.recv()
finally:
ws.close()
CLOSE_WAIT 모니터링 자동화:
#!/bin/bash
# close_wait_monitor.sh
THRESHOLD=50
CLOSE_WAIT_COUNT=$(netstat -an | grep CLOSE_WAIT | wc -l)
if [ $CLOSE_WAIT_COUNT -gt $THRESHOLD ]; then
echo "⚠️ ALERT: CLOSE_WAIT count is $CLOSE_WAIT_COUNT (threshold: $THRESHOLD)"
# 프로세스별 통계
echo "Top processes with CLOSE_WAIT:"
sudo ss -tanp | grep CLOSE_WAIT | awk '{print $NF}' | sort | uniq -c | sort -rn | head -5
# 슬랙 알림 전송
curl -X POST -H 'Content-type: application/json' \
--data "{\"text\":\"CLOSE_WAIT alert: $CLOSE_WAIT_COUNT connections\"}" \
https://hooks.slack.com/services/YOUR/WEBHOOK/URL
fi
# Python 모니터링 스크립트
import subprocess
import time
def count_close_wait():
result = subprocess.run(['netstat', '-an'], capture_output=True, text=True)
count = result.stdout.count('CLOSE_WAIT')
return count
def monitor_close_wait(threshold=50, interval=60):
"""CLOSE_WAIT 소켓을 주기적으로 모니터링"""
while True:
count = count_close_wait()
print(f"CLOSE_WAIT count: {count}")
if count > threshold:
# 경고 로그
print(f"⚠️ WARNING: CLOSE_WAIT count exceeded threshold ({count} > {threshold})")
# 상세 정보 수집
result = subprocess.run(['ss', '-tanp'], capture_output=True, text=True)
with open('/var/log/close_wait_debug.log', 'a') as f:
f.write(f"\n=== {time.ctime()} ===\n")
f.write(result.stdout)
time.sleep(interval)
if __name__ == '__main__':
monitor_close_wait(threshold=50, interval=60)
CLOSE_WAIT 근본 원인 분석:
CLOSE_WAIT가 쌓이는 3가지 근본 원인:
1. 코드 버그
- try-finally 누락
- Exception 처리 미흡
- 조기 return에서 close() 누락
2. 아키텍처 문제
- 장시간 실행되는 백그라운드 작업
- 연결을 전역 변수에 저장 후 관리 실패
- 멀티스레딩/비동기에서 소켓 생명주기 관리 실패
3. 라이브러리 버그
- 서드파티 라이브러리의 소켓 정리 버그
- 오래된 버전 사용
해결 원칙:
→ 모든 소켓 open에 대응하는 close 보장
→ Context Manager 활용
→ 리소스 누수 테스트 자동화
9. CLOSING
양이 동시에 FIN을 전송한 경우 (드물게 발생).
sequenceDiagram
participant C as Client
participant S as Server
Note over C,S: ESTABLISHED
C->>S: FIN
Note over C: FIN_WAIT_1
S->>C: FIN (동시 전송)
Note over S: FIN_WAIT_1
Note over C: CLOSING
Note over S: CLOSING
C->>S: ACK
S->>C: ACK
Note over C: TIME_WAIT
Note over S: TIME_WAIT
10. LAST_ACK
수동적 종료에서 FIN을 전송하고 최종 ACK를 기다리는 상태.
# 서버가 수동적으로 종료
# 1. 클라이언트가 FIN 전송 → 서버는 CLOSE_WAIT
# 2. 서버가 close() 호출 → FIN 전송, LAST_ACK 상태
# 3. 클라이언트의 ACK 수신 → CLOSED
11. TIME_WAIT - TCP의 가장 오해받는 상태
연결 종료 후 2MSL(Maximum Segment Lifetime) 동안 대기.
지연된 패킷 처리 및 포트 재사용 방지.
TIME_WAIT가 반드시 필요한 2가지 이유:
1. 마지막 ACK 손실 시나리오
정상적인 4-Way Handshake:
Client Server
| |
| ------ FIN ----> |
| <----- ACK ----- |
| <----- FIN ----- |
| ------ ACK ----> | ← 마지막 ACK
| |
TIME_WAIT CLOSED
(60초 대기)
만약 마지막 ACK가 손실된다면:
Client Server
| |
| ------ ACK ----> | (손실!)
| |
TIME_WAIT LAST_ACK
(재전송 타이머)
| |
| <----- FIN ----- | ← FIN 재전송
| ------ ACK ----> | ← 재응답
| |
TIME_WAIT CLOSED
TIME_WAIT가 없다면?
→ 클라이언트가 이미 CLOSED
→ FIN 재전송에 RST로 응답
→ 서버가 비정상 종료로 인식
→ 애플리케이션 로그에 에러 발생
2. 지연된 패킷 문제
시나리오: 빠른 연결-종료-재연결
연결 A:
Client:12345 ←→ Server:80
- 데이터 전송 중 일부 패킷이 네트워크에서 지연
- 연결 종료
즉시 재연결 (TIME_WAIT 없이):
Client:12345 ←→ Server:80 (동일한 4-튜플!)
문제:
- 이전 연결 A의 지연된 패킷이 도착
- 커널이 새 연결 B의 패킷으로 오인
- 데이터 손상 발생!
TIME_WAIT 해결책:
- 60초 동안 같은 4-튜플 사용 금지
- MSL(보통 30초) * 2 = 60초
- 네트워크의 모든 패킷이 소멸될 때까지 대기
- 60초 후에만 포트 재사용 가능
2MSL (Maximum Segment Lifetime) 계산:
MSL = 네트워크에서 패킷이 살 수 있는 최대 시간
= IP TTL * 홉 시간
= 보통 30초 (RFC 1122 권장)
2MSL = 60초
= 송신 패킷 소멸 시간 (30초)
+ 응답 패킷 소멸 시간 (30초)
Linux 기본값:
net.ipv4.tcp_fin_timeout = 60 (초)
조정 가능하지만 권장하지 않음:
sudo sysctl -w net.ipv4.tcp_fin_timeout=30
# 위험: 지연된 패킷 문제 발생 가능
TIME_WAIT 문제 (포트 고갈)
# 문제 재현 코드
import socket
import time
# 클라이언트가 짧은 연결을 반복 생성
for i in range(65000): # 포트 범위 초과 시도
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('server.com', 80))
sock.send(b'GET / HTTP/1.1\r\n\r\n')
sock.recv(1024)
sock.close() # 능동적 종료 → TIME_WAIT
# 이 소켓은 60초 동안 포트 점유
print(f"Connection {i} closed")
# 에러 발생:
# OSError: [Errno 99] Cannot assign requested address
# 원인: 사용 가능한 포트(32768~60999) 모두 TIME_WAIT 상태
# TIME_WAIT 소켓 수 확인
netstat -an | grep TIME_WAIT | wc -l
# 많은 경우 (수천 개):
# - 클라이언트가 짧은 연결을 반복적으로 생성
# - 로드 밸런서, 프록시에서 흔함
# - HTTP/1.0 (Connection: close)
# 포트 범위 확인
cat /proc/sys/net/ipv4/ip_local_port_range
# 32768 60999
# → 약 28,000개 포트만 사용 가능
TIME_WAIT 최적화 전략:
# 1. SO_REUSEADDR (서버 측)
# - TIME_WAIT 소켓이 있어도 바인딩 허용
# - 서버 재시작 시 "Address already in use" 방지
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(('0.0.0.0', 8080))
# 이전 서버의 TIME_WAIT 소켓이 있어도 바인딩 성공
# 2. SO_LINGER (위험! 신중히 사용)
import socket
import struct
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# linger = (onoff, linger_time)
# onoff=1, linger_time=0: close() 시 즉시 RST 전송 (TIME_WAIT 건너뜀)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack('ii', 1, 0))
sock.connect(('server.com', 80))
sock.send(b'data')
sock.close() # RST 전송, TIME_WAIT 없음
# ⚠️ 위험:
# - 데이터 손실 가능 (송신 버퍼에 남은 데이터 버림)
# - 서버가 "Connection reset by peer" 에러 발생
# - 오직 로컬 테스트용으로만 사용
# 3. net.ipv4.tcp_tw_reuse (클라이언트 측)
# - 새로운 아웃바운드 연결 시 TIME_WAIT 소켓 재사용
sudo sysctl -w net.ipv4.tcp_tw_reuse=1
# 안전한 이유:
# - 타임스탬프 옵션으로 이전 패킷 구분
# - 클라이언트에서만 작동 (서버는 영향 없음)
# 4. net.ipv4.tcp_tw_recycle (금지!)
# - Linux 4.12에서 제거됨
# - NAT 환경에서 심각한 문제 발생
sudo sysctl -w net.ipv4.tcp_tw_recycle=1 # ❌ 사용 금지!
# 문제:
# - 같은 NAT 뒤의 여러 클라이언트가 같은 IP로 보임
# - 타임스탬프 역전 시 패킷 드롭
# - 무작위 연결 실패
TIME_WAIT 근본 해결책: Keep-Alive
# ❌ 나쁜 예: 매번 새 연결 (TIME_WAIT 누적)
for i in range(1000):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('server.com', 80))
sock.send(b'GET / HTTP/1.1\r\nHost: server.com\r\n\r\n')
sock.recv(1024)
sock.close() # TIME_WAIT
# ✅ 좋은 예: 연결 재사용 (HTTP/1.1 Keep-Alive)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('server.com', 80))
for i in range(1000):
sock.send(b'GET / HTTP/1.1\r\nHost: server.com\r\nConnection: keep-alive\r\n\r\n')
sock.recv(1024)
# 한 번만 close() → TIME_WAIT 1개만
sock.close()
# HTTP/2, HTTP/3: 멀티플렉싱
# - 하나의 TCP 연결로 여러 요청 처리
# - TIME_WAIT 문제 거의 없음
언제 TIME_WAIT가 문제가 되나?
문제 되는 경우:
1. 로드 밸런서 / 리버스 프록시
- 백엔드 서버로 짧은 연결 반복 생성
- 포트 고갈 위험
2. 크롤러 / 스크래퍼
- 수천 개 사이트에 연결
- 빠른 속도로 연결 생성/종료
3. 부하 테스트 도구
- 초당 수만 개 요청
- 클라이언트 포트 고갈
문제 안 되는 경우:
1. 일반 웹 서버 (서버는 TIME_WAIT 거의 없음)
- 클라이언트가 능동 종료
- 서버는 CLOSE_WAIT → CLOSED
2. Keep-Alive 사용하는 애플리케이션
- 연결 재사용
- TIME_WAIT 최소화
3. 서버 측 (listen 소켓)
- LISTEN 소켓은 TIME_WAIT 없음
- Accept된 소켓만 영향받음
5. 상태 전이 다이어그램
완전한 TCP 상태 전이
stateDiagram-v2
[*] --> CLOSED
CLOSED --> LISTEN: passive open\n(server)
CLOSED --> SYN_SENT: active open\n(client)
LISTEN --> SYN_RECEIVED: recv SYN\nsend SYN-ACK
SYN_SENT --> ESTABLISHED: recv SYN-ACK\nsend ACK
SYN_SENT --> SYN_RECEIVED: recv SYN\nsend SYN-ACK
SYN_RECEIVED --> ESTABLISHED: recv ACK
ESTABLISHED --> FIN_WAIT_1: close()\nsend FIN
ESTABLISHED --> CLOSE_WAIT: recv FIN\nsend ACK
FIN_WAIT_1 --> FIN_WAIT_2: recv ACK
FIN_WAIT_1 --> CLOSING: recv FIN\nsend ACK
FIN_WAIT_2 --> TIME_WAIT: recv FIN\nsend ACK
CLOSE_WAIT --> LAST_ACK: close()\nsend FIN
CLOSING --> TIME_WAIT: recv ACK
LAST_ACK --> CLOSED: recv ACK
TIME_WAIT --> CLOSED: 2MSL timeout
CLOSED --> [*]
클라이언트 vs 서버 상태 흐름
flowchart TB
subgraph Client["클라이언트 (능동적 종료)"]
C1[CLOSED] --> C2[SYN_SENT]
C2 --> C3[ESTABLISHED]
C3 --> C4[FIN_WAIT_1]
C4 --> C5[FIN_WAIT_2]
C5 --> C6[TIME_WAIT]
C6 --> C7[CLOSED]
end
subgraph Server["서버 (수동적 종료)"]
S1[CLOSED] --> S2[LISTEN]
S2 --> S3[SYN_RECEIVED]
S3 --> S4[ESTABLISHED]
S4 --> S5[CLOSE_WAIT]
S5 --> S6[LAST_ACK]
S6 --> S7[CLOSED]
end
6. netstat/ss로 상태 확인
netstat 명령어
기본 사용법
# 모든 TCP 연결 확인
netstat -an | grep tcp
# 특정 상태만 필터링
netstat -an | grep ESTABLISHED
netstat -an | grep TIME_WAIT
netstat -an | grep CLOSE_WAIT
# 프로세스 정보 포함 (Linux)
sudo netstat -anp | grep :80
# 통계 정보
netstat -s | grep -i tcp
상태별 개수 확인
# Linux/Mac
netstat -an | awk '/tcp/ {print $6}' | sort | uniq -c
# 출력 예시:
# 150 ESTABLISHED
# 50 TIME_WAIT
# 5 CLOSE_WAIT
# 3 LISTEN
ss 명령어 (더 빠름)
# 모든 TCP 소켓
ss -tan
# ESTABLISHED 상태만
ss -tan state established
# TIME_WAIT 상태만
ss -tan state time-wait
# 프로세스 정보 포함
ss -tanp
# 통계
ss -s
ss 필터 예시
# 특정 포트
ss -tan '( dport = :80 or sport = :80 )'
# 특정 IP
ss -tan dst 192.168.1.100
# 여러 상태 조합
ss -tan state established state syn-sent
# 수신 큐와 송신 큐 크기 확인
ss -tan | awk '{print $2, $3}'
Windows PowerShell
# TCP 연결 확인
Get-NetTCPConnection
# ESTABLISHED 상태만
Get-NetTCPConnection -State Established
# 특정 포트
Get-NetTCPConnection -LocalPort 80
# 상태별 개수
Get-NetTCPConnection | Group-Object -Property State | Select-Object Name, Count
7. 실전 디버깅 시나리오
시나리오 1: TIME_WAIT 과다 (포트 고갈)
문제 증상
$ netstat -an | grep TIME_WAIT | wc -l
5000
# 새 연결 시도 시 에러
# "Cannot assign requested address"
원인
# ❌ 잘못된 코드: 짧은 연결 반복 생성
for i in range(10000):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('api.example.com', 80))
sock.send(b'GET / HTTP/1.1\r\n\r\n')
sock.recv(1024)
sock.close() # TIME_WAIT 상태로 전환
# 60초 동안 포트 재사용 불가!
해결 방법
# ✅ 해결 1: 연결 재사용 (Keep-Alive)
import requests
session = requests.Session()
for i in range(10000):
response = session.get('http://api.example.com/')
# 같은 연결 재사용, TIME_WAIT 발생 안 함
# ✅ 해결 2: SO_REUSEADDR 설정
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# ✅ 해결 3: 커널 파라미터 조정 (Linux)
# /etc/sysctl.conf
# net.ipv4.tcp_tw_reuse = 1
# net.ipv4.tcp_fin_timeout = 30
시나리오 2: CLOSE_WAIT 누적 (리소스 누수)
문제 증상
$ netstat -an | grep CLOSE_WAIT | wc -l
1000
# 시간이 지나도 줄어들지 않음
# 결국 "Too many open files" 에러
원인
# ❌ 잘못된 코드: 소켓을 닫지 않음
def handle_client(sock):
data = sock.recv(1024)
process(data)
# sock.close() 누락!
# 클라이언트가 연결을 닫으면 CLOSE_WAIT 상태로 남음
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('0.0.0.0', 8080))
server.listen(5)
while True:
client, addr = server.accept()
handle_client(client) # 소켓 누수!
해결 방법
# ✅ 해결 1: try-finally로 확실히 닫기
def handle_client(sock):
try:
data = sock.recv(1024)
process(data)
finally:
sock.close() # 반드시 실행
# ✅ 해결 2: Context Manager 사용
def handle_client(sock):
with sock:
data = sock.recv(1024)
process(data)
# 자동으로 close() 호출
# ✅ 해결 3: 타임아웃 설정
sock.settimeout(30) # 30초 후 자동 종료
디버깅
# CLOSE_WAIT 소켓을 가진 프로세스 찾기
lsof -i -n | grep CLOSE_WAIT
# 프로세스별 CLOSE_WAIT 개수
lsof -i -n | grep CLOSE_WAIT | awk '{print $2}' | sort | uniq -c
# 특정 프로세스의 소켓 상태
lsof -p <PID> | grep TCP
시나리오 3: SYN_SENT 타임아웃
문제 증상
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(5)
try:
sock.connect(('unreachable-server.com', 80))
except socket.timeout:
print("❌ Connection timeout (SYN_SENT)")
원인 및 해결
# 1. 방화벽 확인
sudo iptables -L -n | grep 80
# 2. 라우팅 확인
traceroute unreachable-server.com
# 3. 서버 상태 확인
ping unreachable-server.com
# 4. 포트 스캔
nmap -p 80 unreachable-server.com
시나리오 4: 대량 ESTABLISHED 연결
문제 증상
$ netstat -an | grep ESTABLISHED | wc -l
10000
# 서버 응답 느려짐
# CPU, 메모리 사용량 증가
원인
# ❌ Keep-Alive로 연결을 무한정 유지
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
# 유휴 연결이 계속 ESTABLISHED 상태로 남음
해결 방법
# ✅ 해결 1: 타임아웃 설정
sock.settimeout(60) # 60초 유휴 시 자동 종료
# ✅ 해결 2: Keep-Alive 파라미터 조정
import socket
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
# Linux
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 60) # 60초 후 첫 probe
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 10) # 10초 간격
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 3) # 3번 시도
# ✅ 해결 3: 연결 풀 크기 제한
from concurrent.futures import ThreadPoolExecutor
max_connections = 1000
executor = ThreadPoolExecutor(max_workers=max_connections)
8. 성능 튜닝
Port Exhaustion (포트 고갈) 문제
클라이언트 포트 고갈 메커니즘:
TCP 연결 4-Tuple:
(Client IP, Client Port, Server IP, Server Port)
서버:
- 고정 포트 (예: 80, 443)
- 여러 클라이언트 IP 수용
클라이언트:
- 동적 포트 (Ephemeral Port)
- 범위: 32768 ~ 60999 (Linux)
- 총 개수: 28232개
예시: 클라이언트가 API 서버에 대량 요청
for i in range(100000):
sock = requests.get('https://api.example.com')
sock.close()
연결 과정:
1. 새 연결 생성:
Client (192.168.1.100:32768) → Server (1.2.3.4:443)
2. 요청 완료, close() 호출:
FIN 전송 → TIME_WAIT (60초)
3. 다음 연결:
Client (192.168.1.100:32769) → Server (1.2.3.4:443)
4. 포트 증가...
32770, 32771, ... 60999
5. 포트 고갈:
28232개 모두 TIME_WAIT 상태
→ 새 연결 불가!
증상:
socket.error: [Errno 99] Cannot assign requested address
또는
socket.error: [Errno 24] Too many open files
TIME_WAIT 소켓 확인:
$ netstat -an | grep TIME_WAIT | wc -l
25000 ← 포트 거의 고갈
4-Tuple 유니크 제약:
(Client IP, Client Port, Server IP, Server Port)
같은 서버 (1.2.3.4:443)에 연결 시:
- Client IP: 고정 (192.168.1.100)
- Client Port: 28232개로 제한
- Server IP:Port: 고정 (1.2.3.4:443)
→ 최대 28232개 동시 연결
해결책:
1. TIME_WAIT 재사용 활성화
2. 포트 범위 확장
3. 연결 풀 사용
4. HTTP Keep-Alive
해결 방법 상세:
1. TIME_WAIT 재사용 (net.ipv4.tcp_tw_reuse):
기본 동작:
- TIME_WAIT 소켓은 60초간 포트 점유
- 새 연결은 사용 불가
tcp_tw_reuse=1:
- 타임스탬프 기반 재사용 허용
- 조건:
* 새 연결의 타임스탬프 > 이전 연결
* 클라이언트 측에서만 안전
* 서버에서는 사용 금지 (들어오는 연결 혼란)
설정:
sudo sysctl -w net.ipv4.tcp_tw_reuse=1
효과:
- TIME_WAIT 소켓을 1초 후 재사용 가능
- 포트 고갈 문제 해결
- 클라이언트 벤치마크/크롤러에 유용
주의:
- NAT 환경에서 타임스탬프 꼬일 수 있음
- 방화벽이 타임스탬프 제거하면 효과 없음
2. 포트 범위 확장:
기본:
net.ipv4.ip_local_port_range = 32768 60999
총 28232개
확장:
sudo sysctl -w net.ipv4.ip_local_port_range="10000 65535"
총 55536개 (거의 2배)
효과:
- 동시 연결 수 증가
- TIME_WAIT 대기 시간 동안 더 많은 포트 사용 가능
3. SO_LINGER 옵션 (주의 필요):
sock.setsockopt(
socket.SOL_SOCKET,
socket.SO_LINGER,
struct.pack('ii', 1, 0)
)
효과:
- close() 시 FIN 대신 RST 전송
- TIME_WAIT 우회 → 즉시 포트 재사용
부작용:
- 비정상 종료로 간주
- 버퍼에 남은 데이터 손실
- 상대방 "Connection reset by peer" 에러
→ 실무에서 비권장
4. 연결 풀 (Connection Pool):
문제:
for i in range(10000):
r = requests.get(url) # 매번 새 연결
TIME_WAIT 대량 발생
해결:
session = requests.Session() # 연결 풀 생성
for i in range(10000):
r = session.get(url) # 연결 재사용
HTTP Keep-Alive:
Connection: keep-alive
→ 같은 소켓으로 여러 요청
→ TIME_WAIT 최소화
예시:
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
session = requests.Session()
adapter = HTTPAdapter(
pool_connections=10, # 호스트당 연결 풀 크기
pool_maxsize=20, # 최대 연결 수
max_retries=Retry(total=3)
)
session.mount('https://', adapter)
for i in range(10000):
r = session.get('https://api.example.com')
결과:
- 연결 재사용
- TIME_WAIT 10~20개로 제한
- 포트 고갈 문제 해결
5. 여러 IP 사용 (Multi-Homing):
4-Tuple 제약:
(Client IP, Client Port, Server IP, Server Port)
Client IP 추가:
eth0: 192.168.1.100 → 28232개 포트
eth0:0: 192.168.1.101 → 28232개 포트
eth0:1: 192.168.1.102 → 28232개 포트
총: 84696개 동시 연결 가능
코드:
sock = socket.socket()
sock.bind(('192.168.1.101', 0)) # 특정 IP 바인딩
sock.connect(('1.2.3.4', 443))
6. 서버 측 대응 (역방향 문제):
클라이언트가 서버를 공격:
- 대량 연결 생성 후 즉시 종료
- 서버에 TIME_WAIT 대량 발생
- 서버 포트는 고정이므로 직접 영향 없음
- 하지만 메모리/소켓 디스크립터 고갈
해결:
- 클라이언트 IP당 연결 수 제한
- Rate Limiting
- DDoS 방어 시스템
포트 고갈 모니터링:
# TIME_WAIT 소켓 수 확인
netstat -an | grep TIME_WAIT | wc -l
# 특정 서버로의 TIME_WAIT
netstat -an | grep '1.2.3.4:443' | grep TIME_WAIT | wc -l
# 포트 범위 확인
cat /proc/sys/net/ipv4/ip_local_port_range
# 현재 사용 중인 포트
ss -tan | awk '{print $4}' | grep -o ':[0-9]*$' | sort | uniq | wc -l
# 실시간 모니터링
watch -n 1 'ss -tan state time-wait | wc -l'
# 포트별 상태 분포
ss -tan | awk '{print $1}' | sort | uniq -c
Linux 커널 파라미터
# /etc/sysctl.conf
# TIME_WAIT 재사용 허용 (클라이언트 측)
net.ipv4.tcp_tw_reuse = 1
# TIME_WAIT 재활용 (주의: NAT 환경에서 문제, 비활성화 권장)
net.ipv4.tcp_tw_recycle = 0
# 포트 범위 확장 (클라이언트)
net.ipv4.ip_local_port_range = 10000 65535
# FIN_WAIT_2 타임아웃 (기본 60초)
net.ipv4.tcp_fin_timeout = 30
# SYN 백로그 큐 크기
net.ipv4.tcp_max_syn_backlog = 8192
# 최대 연결 수 (서버 listen backlog)
net.core.somaxconn = 65535
# Keep-Alive 설정
net.ipv4.tcp_keepalive_time = 60 # 60초 후 첫 probe
net.ipv4.tcp_keepalive_intvl = 10 # 10초 간격
net.ipv4.tcp_keepalive_probes = 3 # 3번 시도
# 최대 파일 디스크립터 수
fs.file-max = 2097152
# 적용
sudo sysctl -p
SO_LINGER 옵션
import socket
import struct
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# SO_LINGER 설정
# l_onoff=1, l_linger=0: close() 시 RST 전송 (TIME_WAIT 회피)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack('ii', 1, 0))
sock.connect(('example.com', 80))
sock.send(b'data')
# close() 호출 시:
# - 정상: FIN 전송 → TIME_WAIT
# - SO_LINGER(0): RST 전송 → 즉시 CLOSED
sock.close()
# ⚠️ 주의: RST는 비정상 종료로 간주됨
# 상대방이 "Connection reset by peer" 에러 발생 가능
연결 풀 패턴
import queue
import socket
import threading
class ConnectionPool:
def __init__(self, host, port, pool_size=10):
self.host = host
self.port = port
self.pool = queue.Queue(maxsize=pool_size)
# 미리 연결 생성
for _ in range(pool_size):
sock = self._create_connection()
self.pool.put(sock)
def _create_connection(self):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((self.host, self.port))
return sock
def get_connection(self, timeout=5):
"""연결 가져오기"""
try:
return self.pool.get(timeout=timeout)
except queue.Empty:
return self._create_connection()
def return_connection(self, sock):
"""연결 반환"""
try:
self.pool.put_nowait(sock)
except queue.Full:
sock.close()
def close_all(self):
"""모든 연결 종료"""
while not self.pool.empty():
try:
sock = self.pool.get_nowait()
sock.close()
except queue.Empty:
break
# 사용
pool = ConnectionPool('api.example.com', 80, pool_size=20)
def make_request():
sock = pool.get_connection()
try:
sock.send(b'GET / HTTP/1.1\r\n\r\n')
response = sock.recv(4096)
return response
finally:
pool.return_connection(sock) # 재사용
# 여러 스레드에서 사용
for _ in range(1000):
threading.Thread(target=make_request).start()
# TIME_WAIT 발생 안 함 (연결 재사용)
고급 주제
TCP Half-Open 연결
# Half-Open: 한쪽은 ESTABLISHED, 다른 쪽은 CLOSED
# 발생 원인: 네트워크 단절, 프로세스 강제 종료
# 감지 방법: Keep-Alive
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
# Keep-Alive probe 전송 → 응답 없으면 연결 종료
TCP Reset (RST)
# RST 패킷이 전송되는 경우:
# 1. 존재하지 않는 포트로 연결 시도
# 2. SO_LINGER(0)로 close() 호출
# 3. 애플리케이션 강제 종료
# 4. 방화벽이 연결 차단
# RST 수신 시:
# - 즉시 CLOSED 상태로 전환
# - "Connection reset by peer" 에러
TCP Simultaneous Open
sequenceDiagram
participant A as Host A
participant B as Host B
Note over A: CLOSED
Note over B: CLOSED
A->>B: SYN
Note over A: SYN_SENT
B->>A: SYN (동시 전송)
Note over B: SYN_SENT
Note over A: SYN_RECEIVED
Note over B: SYN_RECEIVED
A->>B: SYN-ACK
B->>A: SYN-ACK
Note over A: ESTABLISHED
Note over B: ESTABLISHED
실전 모니터링 스크립트
Python 모니터링 도구
#!/usr/bin/env python3
import subprocess
import time
from collections import Counter
def get_tcp_states():
"""TCP 상태별 개수 조회"""
try:
# ss 명령어 사용 (더 빠름)
result = subprocess.run(
['ss', '-tan'],
capture_output=True,
text=True
)
lines = result.stdout.strip().split('\n')[1:] # 헤더 제외
states = []
for line in lines:
parts = line.split()
if len(parts) > 0:
state = parts[0]
states.append(state)
return Counter(states)
except FileNotFoundError:
# ss가 없으면 netstat 사용
result = subprocess.run(
['netstat', '-an'],
capture_output=True,
text=True
)
states = []
for line in result.stdout.split('\n'):
if 'tcp' in line.lower():
parts = line.split()
if len(parts) >= 6:
states.append(parts[5])
return Counter(states)
def monitor_tcp_states(interval=5):
"""TCP 상태 실시간 모니터링"""
print("🔍 TCP 연결 상태 모니터링 시작...\n")
try:
while True:
states = get_tcp_states()
print(f"\n{'='*50}")
print(f"⏰ {time.strftime('%Y-%m-%d %H:%M:%S')}")
print(f"{'='*50}")
for state, count in sorted(states.items()):
bar = '█' * min(count, 50)
print(f"{state:15s} {count:5d} {bar}")
total = sum(states.values())
print(f"{'─'*50}")
print(f"{'TOTAL':15s} {total:5d}")
# 경고
if states.get('CLOSE_WAIT', 0) > 100:
print("\n⚠️ WARNING: Too many CLOSE_WAIT connections!")
print(" → Check if application is closing sockets properly")
if states.get('TIME_WAIT', 0) > 5000:
print("\n⚠️ WARNING: Too many TIME_WAIT connections!")
print(" → Consider using connection pooling")
time.sleep(interval)
except KeyboardInterrupt:
print("\n\n✅ 모니터링 종료")
if __name__ == '__main__':
monitor_tcp_states(interval=5)
Bash 원라이너 모음
# 1. 상태별 개수
netstat -an | awk '/tcp/ {print $6}' | sort | uniq -c | sort -rn
# 2. 특정 포트의 연결 수
netstat -an | grep ':80 ' | wc -l
# 3. IP별 연결 수 (상위 10개)
netstat -an | grep ESTABLISHED | awk '{print $5}' | cut -d: -f1 | sort | uniq -c | sort -rn | head -10
# 4. TIME_WAIT 비율
echo "scale=2; $(netstat -an | grep TIME_WAIT | wc -l) / $(netstat -an | grep tcp | wc -l) * 100" | bc
# 5. 연결 수 실시간 모니터링
watch -n 1 'netstat -an | grep tcp | awk "{print \$6}" | sort | uniq -c'
프로그래밍 언어별 상태 확인
Python
import socket
import psutil
# 현재 프로세스의 연결 상태
connections = psutil.net_connections(kind='tcp')
for conn in connections:
if conn.status == 'ESTABLISHED':
print(f"{conn.laddr.ip}:{conn.laddr.port} -> {conn.raddr.ip}:{conn.raddr.port}")
print(f" Status: {conn.status}")
print(f" PID: {conn.pid}")
# 상태별 개수
from collections import Counter
states = Counter(conn.status for conn in connections)
print(states)
Node.js
const { exec } = require('child_process');
function getTCPStates(callback) {
exec('netstat -an | grep tcp', (error, stdout) => {
if (error) {
callback(error);
return;
}
const lines = stdout.trim().split('\n');
const states = {};
lines.forEach(line => {
const parts = line.split(/\s+/);
const state = parts[5];
states[state] = (states[state] || 0) + 1;
});
callback(null, states);
});
}
// 사용
getTCPStates((err, states) => {
if (!err) {
console.log('TCP States:', states);
}
});
Go
package main
import (
"fmt"
"github.com/shirou/gopsutil/v3/net"
)
func main() {
// TCP 연결 조회
connections, err := net.Connections("tcp")
if err != nil {
panic(err)
}
// 상태별 개수
states := make(map[string]int)
for _, conn := range connections {
states[conn.Status]++
}
// 출력
for state, count := range states {
fmt.Printf("%s: %d\n", state, count)
}
}
실전 팁
1. 로드 밸런서 설정
# Nginx에서 Keep-Alive 설정
upstream backend {
server backend1.example.com:8080;
server backend2.example.com:8080;
# Keep-Alive 연결 유지
keepalive 32;
}
server {
location / {
proxy_pass http://backend;
# Keep-Alive 헤더
proxy_http_version 1.1;
proxy_set_header Connection "";
# 타임아웃
proxy_connect_timeout 5s;
proxy_read_timeout 60s;
}
}
2. 데이터베이스 연결 풀
import psycopg2
from psycopg2 import pool
# 연결 풀 생성
connection_pool = psycopg2.pool.ThreadedConnectionPool(
minconn=5, # 최소 연결 수
maxconn=20, # 최대 연결 수
host='localhost',
database='mydb',
user='user',
password='pass'
)
def query_database(sql):
conn = connection_pool.getconn()
try:
cursor = conn.cursor()
cursor.execute(sql)
result = cursor.fetchall()
return result
finally:
connection_pool.putconn(conn) # 연결 반환
# TIME_WAIT 발생 안 함 (연결 재사용)
3. HTTP 클라이언트 최적화
import requests
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry
# 세션 재사용 + 재시도 전략
session = requests.Session()
retry_strategy = Retry(
total=3,
backoff_factor=1,
status_forcelist=[429, 500, 502, 503, 504]
)
adapter = HTTPAdapter(
max_retries=retry_strategy,
pool_connections=10, # 연결 풀 크기
pool_maxsize=20
)
session.mount("http://", adapter)
session.mount("https://", adapter)
# 사용
for i in range(1000):
response = session.get('http://api.example.com/data')
# 연결 재사용, TIME_WAIT 최소화
문제 해결 체크리스트
TIME_WAIT 과다
- HTTP Keep-Alive 사용 확인
- 연결 풀 구현 확인
-
net.ipv4.tcp_tw_reuse활성화 -
net.ipv4.tcp_fin_timeout감소 (30초) - 클라이언트 포트 범위 확대
# 클라이언트 포트 범위 확대
sudo sysctl -w net.ipv4.ip_local_port_range="10000 65535"
CLOSE_WAIT 누적
- 애플리케이션 코드에서
close()호출 확인 -
try-finally또는with문 사용 - 타임아웃 설정 확인
- 예외 처리 확인
- 프로세스 재시작 (임시 해결)
# CLOSE_WAIT 디버깅
import traceback
def handle_connection(sock):
try:
data = sock.recv(1024)
process(data)
except Exception as e:
print(f"❌ Error: {e}")
traceback.print_exc()
finally:
print("🔒 Closing socket")
sock.close() # 반드시 실행
ESTABLISHED 과다
- 유휴 연결 타임아웃 설정
- Keep-Alive 파라미터 조정
- 최대 연결 수 제한
- 연결 풀 크기 조정
- 로드 밸런서 설정 확인
정리
TCP 상태 요약
flowchart TD
Start[연결 시작] --> Handshake[3-Way Handshake]
Handshake --> Est["ESTABLISHED\n데이터 전송"]
Est --> Close[연결 종료]
Close --> Active{누가 먼저\n종료?}
Active -->|클라이언트| TimeWait["TIME_WAIT\n60초 대기"]
Active -->|서버| CloseWait["CLOSE_WAIT\nclose 호출 필요"]
TimeWait --> End[CLOSED]
CloseWait --> LastAck[LAST_ACK]
LastAck --> End
핵심 포인트
| 상태 | 주의사항 | 해결 방법 |
|---|---|---|
| TIME_WAIT | 포트 고갈 가능 | 연결 재사용, Keep-Alive |
| CLOSE_WAIT | 소켓 누수 | 반드시 close() 호출 |
| SYN_SENT | 연결 타임아웃 | 타임아웃 설정, 재시도 |
| ESTABLISHED | 과다 연결 | 연결 풀, 타임아웃 |
디버깅 명령어 요약
# 상태 확인
netstat -an | grep tcp
ss -tan
# 상태별 개수
netstat -an | awk '/tcp/ {print $6}' | sort | uniq -c
# 프로세스별 연결
lsof -i -n -P | grep TCP
# 실시간 모니터링
watch -n 1 'ss -s'
# 특정 포트
netstat -an | grep ':80'
ss -tan '( sport = :80 or dport = :80 )'
성능 최적화 요약
# ✅ 연결 재사용
session = requests.Session()
# ✅ 연결 풀
pool = ConnectionPool(size=20)
# ✅ 타임아웃 설정
sock.settimeout(30)
# ✅ Keep-Alive
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
# ✅ 리소스 정리
try:
# 작업
pass
finally:
sock.close()
관련 글 (내부 링크)
TCP와 함께 보면 좋은 네트워크 관련 가이드입니다:
- HTTP 프로토콜 완벽 가이드 | HTTP/1.1·HTTP/2·HTTP/3 비교 - TCP 기반 상위 프로토콜
- C++ 소켓 프로그래밍 | TCP·UDP·비동기 I/O - TCP 소켓 구현
- Kubernetes Pod 트러블슈팅 | CrashLoopBackOff 완벽 해결 가이드 - 네트워크 문제 디버깅
- DRM 완벽 가이드 | Widevine·FairPlay·PlayReady - TCP 기반 스트리밍
- FFmpeg 완벽 가이드 | 비디오·오디오 처리·스트리밍 - 네트워크 스트리밍
- C++ 비동기 I/O | epoll·io_uring - TCP 고성능 처리
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. TCP 11가지 연결 상태(LISTEN, SYN_SENT, ESTABLISHED, FIN_WAIT, TIME_WAIT 등)의 동작 원리와 상태 전이 다이어그램. netstat으로 네트워크 디버깅하는 실전 가이드. S… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
참고 자료
- RFC 793 - TCP Specification
- TCP/IP Illustrated, Volume 1
- Linux TCP Tuning
- ss(8) man page 한 줄 요약: TCP 연결 상태를 이해하면 TIME_WAIT 포트 고갈, CLOSE_WAIT 소켓 누수 같은 네트워크 문제를 효과적으로 디버깅하고 해결할 수 있습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ HTTP 클라이언트·서버 완벽 가이드 | Beast·파싱·Keep-Alive·청크 인코딩
- C++ WebSocket 완벽 가이드 | Beast 핸드셰이크·프레임·Ping/Pong [#30-1]
- C++ REST API 클라이언트 완벽 가이드 | CRUD·인증·에러 처리·프로덕션 패턴 [#21-3]
이 글에서 다루는 키워드 (관련 검색어)
TCP, 네트워크, netstat, ESTABLISHED, TIME_WAIT, CLOSE_WAIT, 소켓프로그래밍 등으로 검색하시면 이 글이 도움이 됩니다.