TIME-WAIT가 수만 개 보이면 포트가 다 떨어진 것 같죠? 사실은 개수만으로 포트 고갈을 확정할 수 없어요.
TCP Teardown과 TIME-WAIT에서는 먼저 연결을 닫은 쪽이 마지막 ACK와 늦게 도착한 패킷을 안전하게 처리하려고 잠시 기다린다는 큰 그림을 봤어요.
그리고 ss와 netstat에서 TCP 상태 읽기에서는 TIME-WAIT가 흔히 정상 종료 뒤 남는 흔적이라는 것도 확인했죠.
이번에는 그 정상적인 흔적이 아주 빠르게 쌓일 때, 실제 장애와 어떻게 이어지는지 볼게요.
- API 프록시가 upstream 연결을 매번 새로 열어요.
- 요청은 짧아서 금방 끝나고, 연결도 바로 닫혀요.
- 트래픽이 늘수록
TIME-WAIT수가 가파르게 올라가요. - 어느 순간 새 upstream 연결이 간헐적으로 실패해요.
- 앱 로그에는
Cannot assign requested address,EADDRNOTAVAIL같은 로컬 연결 오류가 보여요. - 원격 서버는 멀쩡하고, 기존 연결의 요청은 계속 성공하기도 해요.
“새 연결에 붙여줄 로컬 임시 포트를 운영체제가 찾지 못한 건 아닐까요?”
여기서는 Linux 호스트가 외부 서버로 TCP 연결을 많이 여는 클라이언트·리버스 프록시·NAT 앞단 장면을 중심으로 봐요. 포트 선택과 재사용 조건은 운영체제 버전, 네트워크 namespace, 바인딩 방식, 목적지 분산, NAT 구현에 따라 달라질 수 있어요. 그래서
TIME-WAIT 개수 하나가 아니라 실제 연결 오류, 로컬 포트 범위, 목적지 집중도, 연결 생성률을 함께 읽는 데 집중할게요.먼저 장애 장면을 한 줄로 줄여볼게요
프록시 한 대가 같은 API 서버의443 포트로 초당 수백 개의 짧은 연결을 새로 만든다고 해볼게요.
응답은 금방 끝나지만 프록시가 먼저 연결을 닫아서 로컬에 TIME-WAIT가 남아요.
이 그림에서 중요한 건 TIME-WAIT가 생겼다는 사실 자체가 아니에요.
연결을 새로 만드는 속도가 포트를 다시 안전하게 쓸 수 있게 되는 속도보다 빠른지가 핵심이에요.
대략적인 감각은 이렇게 잡을 수 있어요.
TIME-WAIT도 대체로 더 많이 쌓일 수 있어요.
하지만 이 계산은 압력을 이해하는 근사치일 뿐이에요. 실제 포트 재사용 가능 여부는 로컬 주소, 원격 주소, 원격 포트, TCP 구현 조건까지 함께 봐야 해요.
기본 감각을 실제 장애 신호로 바꿔봐요
기본편에서 본 포트와 종료 감각을 이번 사례에 옮기면 이렇게 돼요.
여기서 connection churn은 연결 수가 많다는 말과 조금 달라요.
오래 유지되는 연결 1만 개보다, 1초마다 열고 닫는 연결 수천 개가 임시 포트와
TIME-WAIT에 더 강한 압력을 줄 수 있어요.
포트 하나만이 아니라 4-tuple을 봐야 해요
TCP 연결은 보통 아래 네 값을 묶어서 구분해요.41001로 같지만 원격 IP가 달라요.
운영체제와 소켓 조건에 따라 이런 연결은 서로 다른 4-tuple이므로 동시에 존재할 수 있어요.
그래서 아래 두 장면은 압력이 달라요.
즉
TIME-WAIT 30,000개라는 숫자만 봐서는 부족해요.
그 연결들이 어느 로컬 IP에서, 어느 원격 IP:포트로 몰렸는지를 같이 봐야 해요.
이 그림이 필요한 이유는 포트 고갈을 0번부터 65535번까지 번호를 모두 한 번씩 썼다는 단순한 문제로 보면 안 되기 때문이에요.
실제 연결 가능성은 이 네 값의 조합과 운영체제의 재사용 규칙으로 결정돼요.
먼저 읽을 신호 일곱 가지
TIME-WAIT가 많아 보여도 아래 신호가 함께 맞지 않으면 아직 포트 고갈로 확정하면 안 돼요.
Linux의
connect(2) 문서는 소켓이 미리 로컬 주소에 바인딩되지 않은 상태에서 임시 포트를 고르려 했지만 범위 안의 포트를 사용할 수 없으면 EADDRNOTAVAIL이 날 수 있다고 설명해요.
그러니까 이 오류는 강한 단서예요. 다만 애플리케이션이나 라이브러리가 오류를 다른 문구로 감쌀 수 있으니, 가능하면 원래 errno까지 확인하는 편이 좋아요.
ss 화면에서는 개수보다 분포를 봐요
먼저 전체 소켓 요약을 볼 수 있어요.
TIME-WAIT만 줄여서 봐요.
- 로컬 IP가 모두
10.0.1.20으로 같아요. - 원격 IP와 포트도
203.0.113.10:443으로 같아요. - 로컬 임시 포트만 빠르게 바뀌어요.
- 같은 목적지로 짧은 연결이 반복됐다는 흔적에 가까워요.
임시 포트 범위는 현재 호스트에서 직접 확인해요
Linux에서는 현재 network namespace의 자동 할당 범위를 이렇게 확인할 수 있어요.ip_local_reserved_ports에 들어간 포트는 자동 할당 후보에서 제외될 수 있어요.
따라서 범위의 양 끝만 빼서 계산한 숫자와 실제 후보 수가 다를 수 있어요.
컨테이너 환경에서는 명령을 어디서 실행했는지도 중요해요.
호스트, Pod, sidecar, 프록시가 서로 다른 network namespace에 있다면 실제로 outbound 연결을 만드는 위치의 값을 봐야 해요.
캡처에서는 SYN이 아예 나갔는지 봐요
포트 고갈이 로컬connect() 단계에서 일어나면, 실패한 시도는 네트워크 인터페이스에 SYN을 남기지 않을 수 있어요.
그래서 앱 로그 한 줄과 한 지점의 캡처만으로 끝내지 말고, 실패한 요청의 정확한 시각과 대상 주소를 맞춰서 봐야 해요.
왜 기존 연결은 되는데 새 연결만 실패할까요?
임시 포트는 새 TCP 연결을 만들 때 필요해요. 이미ESTABLISHED인 연결은 자기 4-tuple을 이미 가지고 있으므로, 그 연결 위에서 데이터를 계속 주고받는 데 새 임시 포트를 하나 더 뽑지 않아요.
- 이미 열린 DB connection pool의 쿼리는 성공해요.
- 새로 만드는 외부 API 연결만 실패해요.
- HTTP/2로 오래 유지된 연결은 괜찮아요.
- HTTP/1.1 연결을 매번 닫는 경로만 에러가 나요.
- 재시작 직후 잠깐 회복됐다가 다시 실패해요.
복구는 TIME-WAIT를 없애는 것보다 연결 churn을 줄이는 데서 시작해요
가장 먼저 볼 것은TIME-WAIT 타이머를 줄이는 설정이 아니에요.
왜 요청마다 새 연결이 생기는지부터 봐야 해요.
1
실제로 포트가 고갈됐는지 확인해요
새 연결 실패, ephemeral port 사용량,
TIME-WAIT 수를 같은 시각에 확인해요. TIME-WAIT가 많다는 사실만으로 고갈 원인을 확정하지 않아요.2
새 연결을 만드는 호출 경로를 찾아요
요청마다 client와 connection을 새로 만드는 코드, 짧은 retry, health check를 추적해요. 어느 목적지에서 연결 churn이 커지는지 4-tuple 기준으로 나눠 봐요.
3
연결 재사용 가능성을 확인해요
keep-alive, connection pool, HTTP/2 multiplexing으로 새 연결 수를 줄일 수 있는지 봐요. 재사용 설정은 client와 앞단 proxy의 timeout 정책까지 함께 맞춰야 해요.
4
로컬 포트 범위를 검토해요
OS의 ephemeral port 범위와 예약 포트를 확인해 실제 사용 가능한 수를 계산해요. 범위 확대는 완화책일 수 있지만 연결 churn의 원인을 대신 해결하지는 않아요.
5
목적지와 source IP 분산을 검토해요
한 목적지 4-tuple에 연결이 몰리는지 확인하고 필요하면 source IP나 endpoint를 분산해요. 용량을 늘리기 전에 애플리케이션의 재사용이 정상인지 먼저 봐요.
6
NAT와 conntrack 한도를 확인해요
호스트 포트가 남아도 중간 NAT나 firewall의 conntrack table이 먼저 찰 수 있어요. 각 관측 지점의 한도와 drop·reset 지표를 함께 비교해요.
7
원래 트래픽 모양으로 재검증해요
작은 요청 한 번이 아니라 실제 burst와 동시성으로 다시 부하를 줘요. 새 연결 수,
TIME-WAIT, 오류율이 함께 안정되는지 확인해요.
연결 재사용은 포트 압력을 줄이는 데 효과적이지만, 무조건 오래 들고 있으면 되는 건 아니에요.
한동안 조용한 뒤 첫 요청만 502가 나는 사례처럼 앞단과 upstream의 idle timeout이 어긋나면 닫힌 연결을 다시 쓰려다가 다른 장애가 생길 수 있어요.
즉 목표는 연결을 무조건 짧게 만들기도, 무조건 오래 유지하기도 아니에요.
트래픽 모양과 peer 정책에 맞는 pool 수명, 최대 연결 수, 재시도, 관측을 함께 설계하는 거예요.
잘못 읽기 쉬운 함정 여덟 가지
하나,TIME-WAIT가 많으면 무조건 포트 고갈이라고 보기.정상 종료가 많으면
TIME-WAIT도 많아져요. 새 연결 실패와 포트 범위 압력이 함께 보여야 해요.
둘, 전체 TIME-WAIT 수만 보고 목적지 분포를 안 보기.같은 원격 IP:포트에 몰렸는지, 여러 목적지로 흩어졌는지에 따라 4-tuple 압력이 달라져요. 셋, 서버의 listening 포트가 부족해졌다고 생각하기.
이 사례에서 부족한 후보는 보통 outbound 연결을 만드는 쪽의 로컬 임시 포트예요. 원격 서버의
443 포트가 여러 번 소모되는 게 아니에요.
넷, tcp_fin_timeout을 낮추면 TIME-WAIT가 줄어든다고 믿기.Linux 커널 문서에서
tcp_fin_timeout은 orphaned FIN-WAIT-2 연결을 다루는 값이에요. 이름에 fin이 들어갔다고 TIME-WAIT 타이머 설정으로 읽으면 안 돼요.
다섯, tcp_max_tw_buckets를 낮춰 강제로 지우기.Linux 커널 문서는 이 값을 단순 튜닝 손잡이가 아니라 시스템을 보호하는 한도로 설명하고, 인위적으로 낮추지 말라고 경고해요. 안전장치를 없애면 낡은 패킷과 새 연결의 혼선을 키울 수 있어요. 여섯,
tcp_tw_reuse를 원인 확인 없이 켜기.TIME-WAIT 재사용은 타임스탬프와 연결 조건을 포함한 TCP 안전성 위에서 판단해야 해요. 커널 버전별 의미와 기본값도 달라질 수 있으므로, 연결 churn과 pool 문제를 남겨둔 채 첫 해결책으로 쓰면 안 돼요. 일곱, 포트 범위만 크게 넓히고 끝내기.
범위를 넓히면 임계점을 늦출 수 있지만, 요청마다 새 연결을 만드는 구조가 그대로면 더 큰 트래픽에서 다시 만날 수 있어요. NAT나 upstream 한도가 먼저 터질 수도 있고요. 여덟, 재시작 뒤 회복됐으니 원인이 사라졌다고 보기.
프로세스 재시작으로 연결 패턴이나 pool 상태가 잠깐 바뀌었을 뿐일 수 있어요. 같은 요청률과 지속 시간으로 다시 임계점을 통과해봐야 해요.
복구 뒤에는 같은 연결 생성률로 다시 확인해요
평소보다 낮은 트래픽으로 몇 번 성공했다고 끝내면 안 돼요. 이번 장애는 짧은 연결이 빠르게 쌓일 때 나타났으니까요. 검증은 원래 장애의 모양을 다시 만들어야 해요.
이렇게 해야 포트 범위를 넓혀 장애 시각만 늦춘 것과 연결 churn 자체를 줄인 것을 구분할 수 있어요.
자, 정리해볼까요?
-
TIME-WAIT는 먼저 연결을 닫은 쪽에 남는 정상적인 TCP 안전 상태예요. - 장애가 되는 건 상태 이름 자체보다 짧은 새 연결 생성률이 임시 포트 조합의 회복 속도를 앞지르는 장면이에요.
- 포트 고갈은 전체 상태 수만 보지 말고 로컬 IP, 로컬 포트, 원격 IP, 원격 포트로 이루어진 4-tuple 분포를 함께 봐야 해요.
-
EADDRNOTAVAIL, 임시 포트 범위, 같은 목적지 집중, SYN 출발 여부, NAT 상태를 같은 시간축에서 확인해야 해요. - 우선순위는 TIME-WAIT 강제 제거가 아니라 connection reuse, pooling, multiplexing으로 연결 churn을 줄이는 것이에요.
- 복구 뒤에는 원래와 같은 연결 생성률로 다시 부하를 주고, 포트 오류와 재사용 부작용이 모두 사라졌는지 확인해야 해요.
이어서 보면 좋은 글
- TCP Teardown과 TIME-WAIT - 대화가 끝난 뒤의 깔끔한 마무리 — TIME-WAIT가 왜 필요한지 종료 절차의 큰 그림부터 다시 볼 수 있어요.
- TCP 상태 머신: 연결의 탄생부터 소멸까지의 일대기 — active close 쪽이 어떤 상태를 거쳐 TIME-WAIT에 들어가는지 상태 지도에서 확인할 수 있어요.
- ss와 netstat에서 TCP 상태는 어떻게 읽어야 할까요? — 상태, 로컬·피어 주소, 큐를 실제 운영 화면에서 읽는 순서를 익힐 수 있어요.
- Connection reuse, Keep-Alive, Pooling은 왜 같이 봐야 할까요? — 요청마다 새 연결을 만들지 않고 기존 연결을 다시 쓰는 구조를 더 깊게 볼 수 있어요.
- 한동안 조용한 뒤 첫 요청만 502가 나는 이유는 뭘까요? — 연결 재사용을 적용한 뒤 idle timeout이 어긋날 때 생기는 반대쪽 함정을 이어서 볼 수 있어요.