TCP 연결 종료를 진단할 때는 양방향 byte stream의 종료와 애플리케이션 요청의 성공을 분리해야 합니다. FIN·EOF·CLOSE_WAIT·TIME_WAIT는 서로 다른 방향과 책임을 나타내며, FD·임시 포트·NAT 상태·외부 처리 결과는 각각 별도의 자원과 증거입니다.
한쪽 FIN과 반대 방향 응답의 공존
클라이언트가 모든 입력을 보낸 뒤 송신 방향을 닫고 서버 결과를 기다리는 프로토콜을 생각해 보겠습니다. 서버가 FIN을 받았다고 연결 전체를 즉시 닫으면 남은 응답을 보내지 못합니다. TCP의 두 방향은 독립적이므로 한쪽 송신 종료와 전체 연결 종료를 구분해야 합니다.
FIN은 해당 방향의 앞선 데이터를 마친 뒤 더 보내지 않겠다는 뜻입니다. 수신 애플리케이션은 버퍼의 앞선 데이터를 읽은 뒤 EOF를 관찰할 수 있습니다. RST는 다른 비정상 종료 경로입니다. TCP EOF만으로 상위 메시지가 완전하다고 판단하지 말고 길이·TLS 종료 등 상위 규칙도 확인합니다.
상태를 읽을 때는 “A→B byte stream의 EOF”와 “B→A stream의 송신 가능”을 별도 축으로 적습니다. half-close 입력 뒤 서버가 응답을 끝까지 보내는 trace가 성공해야 하며, EOF가 도착했다는 사실만으로 길이 prefix·TLS close·상위 checksum이 맞다고 판단하지 않습니다. 상위 프로토콜의 완료 프레임과 TCP 종료를 각각 로그로 남깁니다.
한쪽 선행 종료의 TCP 상태 전이
| 시점 | 먼저 닫는 A | 나중에 닫는 B |
|---|---|---|
| A 송신 종료 | FIN-WAIT-1 | ESTABLISHED |
| B가 FIN 수신·ACK | FIN-WAIT-2로 진행 가능 | CLOSE-WAIT, 응답 송신 가능 |
| B도 로컬 종료 | FIN 대기 | LAST-ACK |
| A가 FIN 확인·ACK | TIME-WAIT | ACK 뒤 CLOSED |
그림을 글로 읽기
- 상대 FIN 수신
- CLOSE_WAIT: 입력 EOF 처리 · 남은 응답·자원 정리
- 로컬 송신 종료·close
- 종료 확인: 진행 I/O 참조도 정리
- 상대 FIN 수신 → CLOSE_WAIT: 한 방향 종료
- CLOSE_WAIT → 로컬 송신 종료·close: 로컬 책임 수행
- 로컬 송신 종료·close → 종료 확인: 상태·완료 확인
실제 상태는 동시 종료·RST·재전송 등으로 달라질 수 있습니다. 표는 대표 흐름이지 모든 연결이 반드시 같은 방향·상태를 거친다는 뜻이 아닙니다.
TIME_WAIT와 자원 누수의 구분
마지막 ACK가 유실되면 상대가 FIN을 재전송할 수 있습니다. TIME_WAIT는 다시 응답하고 지연된 과거 세그먼트가 새 연결과 섞일 가능성을 줄이는 보호 상태입니다. 일반적으로 능동 종료한 쪽에서 보이며 동시 종료에서는 양쪽에 생길 수 있습니다.
TIME_WAIT 항목 하나마다 애플리케이션 FD 하나가 열려 있다고 계산하지 않습니다. 짧은 연결을 많이 만들면 정상적으로 많아질 수 있습니다. 문제는 새 연결 실패·포트 압박·사용자 지연과 함께 나타나는지입니다. 개수를 0으로 만드는 것을 목표로 안전하지 않은 재사용 옵션을 적용하지 않습니다.
TIME_WAIT 진단은 상태 수 하나가 아니라 생성률·목적지별 분포·새 connect 오류·사용자 p99를 함께 비교합니다. 정상적인 능동 종료 직후 수가 늘고 생성률이 낮아지면 보호 상태일 수 있지만, 포트 할당 실패와 함께 증가하면 임시 포트·NAT·연결 재사용을 조사합니다. 안전하지 않은 재사용으로 숫자만 줄인 결과를 건강한 종료로 간주하지 않습니다.
CLOSE_WAIT 지속과 소켓 소유자 추적
CLOSE_WAIT는 상대 FIN을 받았지만 로컬이 아직 송신 종료를 하지 않은 상태입니다. 남은 응답을 만드는 짧은 동안은 정상일 수 있습니다. 그러나 작업 완료 후에도 지속 증가하면 EOF·예외·취소 경로의 close 누락이나 다른 핸들의 참조를 조사합니다.
소켓 owner와 요청 ID, 생성·EOF·로컬 close 시각을 연결합니다. 연결 풀 반환과 실제 close는 다르고, 참조가 여러 개면 하나를 놓아도 자원이 남을 수 있습니다. 비동기 I/O의 버퍼는 취소 요청이 아니라 실제 완료·정리 계약까지 유지해야 합니다.
CLOSE_WAIT 항목을 나이순으로 정렬해 짧게 사라지는 정상 처리와 완료 후 남는 leak를 분리합니다. EOF·예외·cancel 각각의 finally 경로에서 실제 socket close가 호출되고, pool return이 close를 대신한다고 잘못 가정하지 않는지 확인합니다. 같은 FD를 여러 비동기 작업이 참조하는 경우 마지막 참조가 놓이는 시각까지 추적해야 합니다.
NAT 포트 자원과 앱 FD의 분리
여러 Pod가 하나의 공인 NAT 주소로 같은 외부 서버에 짧은 연결을 계속 만들면 NAT의 매핑·포트·상태 테이블이 먼저 포화될 수 있습니다. 각 앱의 FD가 남아 있어도 새 아웃바운드 연결은 실패할 수 있습니다.
| 관찰 대상 | 필요한 지표 |
|---|---|
| 애플리케이션 | 열린 FD·실행 요청·풀 대기·close 경로 |
| 호스트 TCP | 상태별 수·목적지·연결 생성·실패 |
| NAT·방화벽 | 매핑 할당 실패·활성 상태·보존 시간 |
| 외부 서비스 | 실제 연결·요청 수신 여부 |
사용 가능한 연결 수를 공인 IP당 무조건 65,536개라고 일반화하지 않습니다. 목적지 분포·매핑 규칙·장비 한도가 영향을 줍니다. 연결 재사용·다중화로 생성률을 줄일 수 있지만 스트림 한도와 하위 서비스 용량은 남습니다.
유휴 연결 재사용 실패와 결과 불확정
중간 장치가 끊은 연결을 풀에서 다시 쓰면 첫 변경 요청이 실패할 수 있습니다. 실패가 서버 적용 전인지 응답 유실인지 구분되지 않으면 같은 주문을 새 ID로 재시도하지 않습니다. 멱등 키·결과 조회와 전체 재시도 예산이 필요합니다.
유휴 수명·최대 수명·재연결 지터와 목적지별 생성률을 조정하고 전후 새 연결 실패·p99를 비교합니다. 용량 증설이 필요하면 NAT 주소·라우팅·장애 영역과 비용도 함께 검토합니다.
재사용 연결에서 write가 실패한 입력은 서버 적용 전 실패와 응답 유실을 모두 만들 수 있는 경계 사례입니다. 주문 같은 변경 요청은 새 attempt ID가 아니라 멱등 키 또는 결과 조회로 먼저 상태를 확인하고, 연결 재생성 정책은 서버 오류율뿐 아니라 connect rate·NAT 매핑·풀 대기·p99를 함께 비교합니다.
정상 종료와 자원 고갈의 분리 재현
입력 뒤 half-close하고 서버 응답을 끝까지 받는 사례, 부분 프레임 뒤 EOF, close 누락, 짧은 연결 집중과 연결 풀 재사용을 각각 시험합니다. 상태별 수뿐 아니라 항목의 나이와 실제 요청 결과를 봅니다. 연결이 끝났다는 사실과 주문이 성공·실패·불확정이라는 사실을 같은 로그 상태로 합치지 않아야 합니다.
실험 결과에는 상태별 개수와 항목의 age histogram, FD 수, ephemeral port/NAT 실패, 상위 요청 outcome을 같이 남깁니다. half-close 성공 사례에서 TIME_WAIT가 존재해도 요청이 성공할 수 있고, CLOSE_WAIT가 적어도 서버 commit 뒤 응답이 유실될 수 있다는 두 반례를 포함해야 종료 상태와 업무 결과를 혼동하지 않습니다.