이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- TCP 3-way handshake, FIN, RST가 연결 성공/실패 증상을 어떻게 만드는가?
SYN-SENT,SYN-RECV,ESTABLISHED,TIME_WAIT,CLOSE_WAIT상태를 장애 단서로 어떻게 읽는가?- backlog, connection pool, idle timeout, keep-alive가 TCP 상태와 어떻게 연결되는가?
개요
TCP는 HTTP 요청을 보내기 전에 연결을 만들고, 순서 보장과 재전송을 제공한다. 이 편리함에는 handshake 지연, kernel socket 상태, backlog, TIME_WAIT, connection pool 같은 비용이 따른다.
- TCP는 SYN, SYN-ACK, ACK로 연결을 만들고 FIN/RST로 종료한다.
- 연결 상태는 ESTABLISHED, TIME_WAIT, CLOSE_WAIT처럼 OS에 남는다.
- 상태를 보면 애플리케이션이 연결을 닫지 않는지, 클라이언트가 과도하게 연결하는지 추정할 수 있다.
왜 백엔드 개발자가 알아야 하는가
HTTP client timeout, LB 502, DB 연결 고갈, outbound API 지연은 TCP 상태를 보면 원인 후보가 빠르게 줄어든다. 특히 앱 로그에 예외만 남고 네트워크 상태를 보지 않으면 재시작으로 잠깐 가려지는 문제를 반복하게 된다.
- connect timeout은 SYN/SYN-ACK 단계에서 막힌 것일 수 있다.
- read timeout은 연결은 되었지만 응답 byte가 오지 않는 상황일 수 있다.
- CLOSE_WAIT 누적은 상대가 닫았는데 우리 앱이 socket을 닫지 못한다는 신호다.
- TIME_WAIT 증가는 항상 나쁜 것이 아니라 짧은 연결을 많이 만들 때 자연스럽게 늘 수 있다.
- RST는 proxy idle timeout, app crash, 잘못된 port, 중간 장비 정책 종료의 단서가 된다.
요청 흐름에서의 위치
TCP는 HTTP 요청 본문이 오가기 전에 연결을 만드는 구간이다.
client SYN
→ server SYN-ACK
→ client ACK
→ ESTABLISHED
→ HTTP request/response
→ FIN or RST
→ TIME_WAIT 등 종료 상태handshake가 실패하면 HTTP request는 서버 앱까지 오지 않는다. 연결 후 실패하면 HTTP log, proxy log, TCP state를 함께 보아야 한다.
원리
주요 TCP 상태는 다음처럼 읽는다.
| 상태 | 의미 | 실무 해석 |
|---|---|---|
| LISTEN | 서버가 연결을 기다림 | listener 확인 |
| SYN-SENT | client가 SYN을 보냄 | connect 시도 중, 응답 없으면 timeout |
| SYN-RECV | server가 SYN을 받고 SYN-ACK 보냄 | handshake 미완료, backlog/SYN flood 의심 |
| ESTABLISHED | 연결 성립 | active connection 수, pool 사용량 |
| FIN-WAIT | 종료 절차 진행 | 정상 종료 과정 |
| CLOSE-WAIT | 상대가 닫았고 local app이 아직 닫지 않음 | 앱/라이브러리 close 누락 의심 |
| TIME-WAIT | local이 마지막 ACK 후 재사용 지연 | 짧은 연결이 많으면 증가 |
RST는 정상 종료가 아니라 즉시 연결을 끊는 신호다. connection refused도 TCP RST로 보일 수 있고, 이미 맺어진 연결의 RST는 proxy idle timeout, app 재시작, upstream reset처럼 보일 수 있다.
핵심 판단 기준
- connect timeout, TLS timeout, read timeout을 분리해 기록한다.
- server
LISTEN과 clientSYN-SENT를 같이 보면 경로 차단인지 listener 문제인지 좁힐 수 있다. - CLOSE_WAIT 누적은 네트워크보다 앱이나 client library의 connection close 문제를 먼저 의심한다.
- TIME_WAIT는 수치만으로 판단하지 말고 traffic 패턴, connection reuse, ephemeral port 사용률과 함께 본다.
- backlog overflow는 앱 thread 문제처럼 보일 수 있지만 kernel accept queue 한계일 수 있다.
실무에서 자주 만나는 문제
- CLOSE_WAIT 누적을 네트워크 장애로만 본다.
- TIME_WAIT 증가를 무조건 비정상으로 본다.
- RST를 애플리케이션 예외와 연결하지 못한다.
- 새 배포 중 graceful shutdown 없이 연결을 끊어 client가 reset을 본다.
- upstream idle timeout보다 client pool idle 시간이 길어 stale connection을 재사용한다.
- accept queue가 가득 차서 SYN은 오는데 앱이 받아들이지 못한다.
- outbound HTTP client가 connection reuse를 못 해 handshake 비용과 TIME_WAIT를 폭증시킨다.
- packet capture 없이 “방화벽 문제”와 “앱이 reset”을 구분하지 못한다.
TCP 상태는 snapshot이므로 시점을 맞춰 보아야 한다. 장애가 끝난 뒤 ss를 보면 원인을 놓칠 수 있다. metric, packet capture, app log, proxy log를 같은 시간대로 맞추는 것이 중요하다.
디버깅 방법
ss -tan으로 TCP state 분포를 본다.- 패킷이 필요하면
tcpdump로 SYN/RST 흐름을 본다. - connection pool metric과 OS socket 상태를 함께 본다.
nstat으로 retransmit, listen overflow 같은 kernel counter를 본다.- proxy log에서 upstream reset, connect timeout, read timeout을 구분한다.
- 배포 시점과 RST 증가가 겹치는지 본다.
ss -tan | awk 'NR>1 {count[$1]++} END {for (s in count) print s, count[s]}'
ss -tan state close-wait
ss -tan state time-wait | wc -l
nstat -az | egrep 'TcpRetransSegs|TcpExtListenOverflows|TcpExtListenDrops'패킷이 필요할 때는 범위를 좁혀 캡처한다.
sudo tcpdump -nn -i any 'host 203.0.113.10 and tcp port 443 and (tcp[tcpflags] & (tcp-syn|tcp-rst|tcp-fin) != 0)'SYN만 나가고 SYN-ACK가 없으면 경로/drop을, SYN-ACK 후 ACK가 없으면 client 쪽 경로를, RST가 보이면 누가 RST를 보냈는지를 본다.
코드로 확인하기
짧은 연결을 계속 만들면 handshake 비용과 TIME_WAIT가 늘어난다. HTTP client pool을 쓰는 이유가 여기에 있다.
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(2))
.build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/health"))
.timeout(Duration.ofSeconds(3))
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode());위 예시는 connect timeout과 request timeout을 분리한다. 운영에서는 dependency별 timeout, connection pool, idle max age를 함께 맞춘다.
watch -n 1 "ss -tan | awk 'NR>1 {count[\\$1]++} END {for (s in count) print s, count[s]}'"부하 테스트 중 이 명령으로 TIME_WAIT, CLOSE_WAIT, ESTABLISHED의 변화 방향을 같이 보면 pool 재사용이 되는지 감을 잡을 수 있다.
주니어가 자주 하는 오해
- TIME_WAIT가 보이면 무조건 장애라고 생각한다.
- TIME_WAIT는 정상 종료 과정에서도 생긴다. 양과 증가 속도, port 사용률을 함께 봐야 한다.
- CLOSE_WAIT와 TIME_WAIT를 비슷하게 본다.
- CLOSE_WAIT는 local app이 close를 마무리하지 못한 신호일 수 있어 더 위험하다.
- HTTP 502는 항상 app exception이라고 생각한다.
- upstream connect 실패나 reset도 proxy 502로 보일 수 있다.
- packet capture는 인프라만 하는 일이라고 생각한다.
- 백엔드도 SYN/RST/FIN 정도는 읽을 수 있어야 협업이 빨라진다.
시니어의 설계 판단 기준
- 연결 재사용을 기본으로 설계하되 idle timeout mismatch를 피한다.
- graceful shutdown에서 새 연결 차단, 기존 연결 drain, readiness 해제를 순서화한다.
- TCP 상태 metric과 HTTP client pool metric을 함께 대시보드에 둔다.
- rare reset을 완전히 없애기보다 retry/idempotency/timeout으로 사용자 영향을 줄인다.
- backlog, max connection, worker thread, pool size를 서로 맞춘다.
인프라 협업 포인트
- RST/timeout 이슈를 전달할 때 source/destination, port, 시각, tcpdump 요약, proxy upstream error를 함께 준다.
- LB idle timeout, app keep-alive timeout, client pool max idle time을 인프라와 함께 맞춘다.
- 배포 중 reset이 늘면 readiness, drain delay, terminationGracePeriod, preStop hook을 같이 본다.
- backlog overflow가 보이면 OS backlog, app accept 속도, worker thread, LB surge를 함께 조정한다.
실전 팁
- 부하 테스트를 할 때 p99 latency만 보지 말고 TCP state 분포를 같이 본다.
- CLOSE_WAIT가 증가하면 thread dump와 client library resource close 코드를 함께 본다.
- TIME_WAIT가 많으면 connection pool이 꺼져 있는지, keep-alive가 너무 짧은지, ephemeral port 범위가 충분한지 본다.
- reset 원인은 한쪽 로그만으로 단정하지 말고 양쪽 tcpdump나 proxy error log를 맞춘다.
위험 신호!
- CLOSE_WAIT가 계속 누적된다.
- 배포 직후 client reset이 튄다.
- connect timeout과 read timeout을 같은 timeout으로만 기록한다.
- LB idle timeout과 client pool idle time이 문서화되어 있지 않다.
- listen overflow/drop kernel counter가 증가한다.
확인 질문
확인 질문
- CLOSE_WAIT가 계속 쌓이면 무엇을 의심해야 하는가?
- 상대가 연결을 닫았는데 local application이나 library가 socket close를 마무리하지 못하는 문제를 의심한다.
- TIME_WAIT가 많다는 사실만으로 장애라고 말하면 안 되는 이유는 무엇인가?
- 정상 종료 과정에서도 생기며 traffic 패턴, 연결 재사용, ephemeral port 사용률과 함께 봐야 하기 때문이다.
- connect timeout과 read timeout은 어떻게 다른가?
- connect timeout은 연결 수립 단계의 지연이고, read timeout은 연결 후 응답 데이터를 기다리는 단계의 지연이다.