본문 바로가기
삵
Operating System

TIME_WAIT 폭증으로 포트가 고갈될 때

aappsroot·2026년 10월 12일·조회 0

Tomcat이 뒤쪽 DB나 외부 API로 연결을 열고 바로바로 닫는 구조, 또는 Nginx가 upstream으로 짧은 연결을 계속 맺는 구조에서 자주 보게 되는 증상이다. 평소엔 멀쩡하다가 트래픽이 조금만 올라가면 java.net.BindException: Cannot assign requested address 같은 로그가 찍히기 시작한다. 막상 서버에 붙어 ss로 세어 보면 TIME_WAIT 소켓이 수만 개 쌓여 있다. 포트가 모자라서 나가는 연결 자체를 못 만드는 상태다.

결론부터 말하자면, 밖으로 나가는 연결(아웃바운드) 때문에 포트가 고갈된 거라면 net.ipv4.tcp_tw_reuse를 켜고 net.ipv4.ip_local_port_range를 넓히는 게 정석이다. 들어오는 연결(인바운드)의 수락 대기열이 넘치는 문제라면 net.core.somaxconn과 WAS의 backlog 설정을 같이 올려야 한다. 두 문제는 원인도 해법도 다르니 먼저 어느 쪽인지부터 가려야 한다.

이 글은 커널 5.4 이상(RHEL 9, Ubuntu 20.04 이상 계열)을 기준으로 한다. 기본값은 커널 버전에 따라 다를 수 있으니 실제 값은 각 항목에서 sysctl로 확인하는 명령을 함께 적는다.

1. TIME_WAIT가 무엇이고 왜 쌓이는가

TCP 연결을 끝낼 때 먼저 FIN을 보내는 쪽, 즉 연결을 능동적으로 닫는 쪽이 TIME_WAIT 상태로 들어간다. 뒤늦게 도착하는 패킷이 다음 연결에 섞여 들어가는 것을 막기 위해, 소켓을 바로 없애지 않고 한동안 붙잡아 둔다. 리눅스에서 이 시간은 커널에 TCP_TIMEWAIT_LEN으로 60초가 박혀 있고, sysctl로는 바꿀 수 없다.

흔히 tcp_fin_timeout으로 TIME_WAIT 시간을 줄일 수 있다고 알려져 있는데, 이 값은 FIN_WAIT_2 상태에 적용되는 것이지 TIME_WAIT에는 영향이 없다. 혼동하기 쉬운 지점이다.

문제는 아웃바운드 연결에서 생긴다. Tomcat이 DB 커넥션 풀 없이 매 요청마다 연결을 열고 닫거나, Nginx가 upstream으로 keepalive 없이 짧은 연결을 반복하면, 로컬 쪽이 능동적으로 닫는 입장이 되어 TIME_WAIT가 로컬 포트에 쌓인다. 나가는 연결은 (로컬 IP, 로컬 포트, 원격 IP, 원격 포트) 네 값으로 구분되는데, 로컬 포트는 커널이 ip_local_port_range 범위에서 임시로 하나 골라 쓴다. 이 임시 포트(ephemeral port)가 TIME_WAIT로 다 묶이면 새 연결에 배정할 포트가 없어 EADDRNOTAVAIL, 자바에서는 BindException: Cannot assign requested address로 터진다.

2. 어느 쪽 문제인지 ss로 확인한다

ss는 소켓 상태를 보는 도구로 예전 netstat을 대체한다. 먼저 요약으로 TIME_WAIT 개수부터 본다.

$ ss -s
Total: 5821
TCP:   5604 (estab 142, closed 5312, orphaned 0, timewait 5301)

Transport Total     IP        IPv6
TCP       292       280       12

timewait 수치가 수천, 수만으로 올라가 있으면 TIME_WAIT 누적이 맞다. 다음으로 어느 목적지로 쌓이는지 본다. 상태 필터로 TIME_WAIT만 추려서 세어 본다.

$ ss -tan state time-wait | wc -l
5301

$ ss -tan state time-wait dst :3306 | head
State       Recv-Q  Send-Q   Local Address:Port    Peer Address:Port
TIME-WAIT   0       0        10.0.0.5:41022        10.0.0.9:3306
TIME-WAIT   0       0        10.0.0.5:41044        10.0.0.9:3306
TIME-WAIT   0       0        10.0.0.5:41050        10.0.0.9:3306

Local Address의 포트가 전부 임시 포트 범위(32768 이상)이고 Peer가 DB 포트(3306)라면, 이 서버가 능동적으로 닫는 아웃바운드 연결이 원인이다. 3절과 4절의 튜닝이 바로 듣는 경우다.

반대로 Local Address 포트가 서비스 포트(8080 등)로 고정돼 있고 TIME_WAIT가 쌓인다면, 클라이언트 쪽이 아니라 이 서버가 받은 연결을 닫는 상황이다. 이때는 애플리케이션이 연결을 재사용하도록 keepalive를 손보는 게 먼저다. 수락 대기열이 넘치는지는 5절에서 본다.

3. 임시 포트 범위를 넓힌다

현재 범위부터 확인한다.

$ sysctl net.ipv4.ip_local_port_range
net.ipv4.ip_local_port_range = 32768	60999

기본값은 32768부터 60999까지, 약 2만 8천 개다. 60초 동안 2만 8천 개를 다 소진할 만큼 짧은 연결을 쏟아내면 고갈된다. 시작 포트를 낮춰 범위를 넓히면 여유가 늘어난다.

$ sudo sysctl -w net.ipv4.ip_local_port_range="10240 65535"
net.ipv4.ip_local_port_range = 10240	65535

10240 아래는 잘 알려진 서비스 포트와 겹칠 수 있으니 그보다 낮추지 않는다. 범위를 넓히는 건 증상을 완화할 뿐 근본 해결은 아니다. 연결 재사용이 안 되는 구조라면 범위를 넓혀도 트래픽이 더 오르면 다시 고갈된다. 그래서 4절을 같이 적용한다.

4. tcp_tw_reuse로 TIME_WAIT 소켓을 재사용한다

net.ipv4.tcp_tw_reuse는 TIME_WAIT 상태의 소켓을 새 아웃바운드 연결에 안전하게 재사용하도록 허용하는 값이다. TCP 타임스탬프를 이용해 옛 연결과 새 연결을 구분할 수 있을 때만 재사용하므로, 아무 때나 포트를 돌려쓰는 것과는 다르다.

값은 세 가지다.

  • 0 - 재사용 안 함
  • 1 - 전역 활성화
  • 2 - 루프백 트래픽에만 활성화(커널 5.x대 기본값)

현재 값을 확인한다.

$ sysctl net.ipv4.tcp_tw_reuse
net.ipv4.tcp_tw_reuse = 2

$ sysctl net.ipv4.tcp_timestamps
net.ipv4.tcp_timestamps = 1

기본값 2는 루프백에만 적용되므로, 다른 호스트의 DB나 API로 나가는 연결에는 효과가 없다. 아웃바운드 포트 고갈을 풀려면 1로 올린다.

$ sudo sysctl -w net.ipv4.tcp_tw_reuse=1
net.ipv4.tcp_tw_reuse = 1

전제 조건이 하나 있다. tcp_tw_reuse는 TCP 타임스탬프가 켜져 있어야(tcp_timestamps = 1, 기본값) 동작한다. 타임스탬프를 끈 환경이라면 reuse도 듣지 않으니 먼저 확인한다.

한 가지 더. 예전에 쓰던 tcp_tw_recycle은 NAT 뒤 클라이언트의 연결을 깨뜨리는 문제가 있어 커널 4.12에서 제거됐다. 지금 커널엔 아예 없으니 설정하려 하지 말고 tcp_tw_reuse만 쓴다. reuse는 아웃바운드 연결에만 작용하고 수신 측 연결을 건드리지 않아 recycle 같은 부작용이 없다.

TIME_WAIT 소켓 수 자체에 상한을 두고 싶으면 tcp_max_tw_buckets를 조정한다. 이 상한을 넘으면 TIME_WAIT 소켓이 즉시 파기되고 커널 로그에 경고가 찍힌다. 안전장치로는 쓸 수 있지만, 상한을 낮추는 것이 포트 고갈의 해법은 아니다. 근본은 reuse와 연결 재사용이다.

5. 인바운드라면 somaxconn과 backlog를 맞춘다

2절에서 수신 측 문제로 판명됐거나, 순간 접속 폭주로 연결이 거부된다면 수락 대기열(accept queue)을 본다. net.core.somaxconn은 listen() 백로그의 상한이다. 애플리케이션이 backlog를 아무리 크게 요청해도 이 값으로 잘린다.

$ sysctl net.core.somaxconn
net.core.somaxconn = 4096

기본값은 커널 5.4부터 4096이다. 그 이전 커널에서는 128이었으니, 오래된 서버라면 128에 묶여 있을 수 있다. 대기열이 넘치는지는 ss -ltn의 Send-Q로 본다. 리스닝 소켓에서 Send-Q는 백로그 상한, Recv-Q는 현재 대기 중인 연결 수다.

$ ss -ltn
State    Recv-Q   Send-Q   Local Address:Port   Peer Address:Port
LISTEN   0        128      *:8080               *:*

Send-Q가 128로 보이면 Tomcat이 backlog 128로 열었다는 뜻이다. Tomcat은 server.xml의 Connector에서 acceptCount가 이 백로그에 해당한다. 기본값이 100이므로, 커널만 4096으로 올려도 Tomcat이 100으로 여는 한 100에 묶인다. 양쪽을 같이 올려야 한다.

<Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol"
           connectionTimeout="20000"
           acceptCount="4096"
           maxConnections="8192" />

Nginx는 listen 지시어의 backlog 파라미터로 지정한다. 리눅스 기본은 511이다.

server {
    listen 80 backlog=4096;
    ...
}

커널의 somaxconn과 애플리케이션의 backlog 중 작은 쪽이 실제 상한이 된다는 점만 기억하면 된다.

6. 영구 적용과 검증

sysctl -w로 바꾼 값은 재부팅하면 사라진다. 영구 적용은 /etc/sysctl.d/ 아래에 파일로 남긴다.

$ sudo tee /etc/sysctl.d/99-tcp-tuning.conf <<'EOF'
net.ipv4.tcp_tw_reuse = 1
net.ipv4.ip_local_port_range = 10240 65535
net.core.somaxconn = 4096
EOF

$ sudo sysctl --system

적용 뒤 부하가 올라간 시점에 다시 ss로 센다. TIME_WAIT가 늘어도 포트 고갈 에러가 사라지고, 임시 포트가 재사용되며 수치가 평평하게 유지되면 튜닝이 듣는 것이다.

$ watch -n1 'ss -tan state time-wait | wc -l'

마지막으로, 커널 튜닝은 증상을 버티게 해 주는 것이고 가장 확실한 해결은 애플리케이션 쪽 연결 재사용이다. Tomcat이라면 DB 커넥션 풀을 제대로 쓰고, 외부 API 호출에는 HTTP 클라이언트의 keepalive를 켠다. Nginx라면 upstream 블록에 keepalive를 두고 proxy_http_version 1.1, proxy_set_header Connection ""를 설정해 연결을 재사용한다. 연결을 재사용하면 애초에 TIME_WAIT가 덜 생긴다. 커널 값은 그 위에 얹는 여유분으로 보면 된다.

자주 묻는 질문

tcp_fin_timeout을 줄이면 TIME_WAIT가 빨리 사라지나?

아니다. tcp_fin_timeout은 FIN_WAIT_2 상태에 적용되는 값이고 TIME_WAIT와는 무관하다. 리눅스에서 TIME_WAIT 지속 시간은 커널에 TCP_TIMEWAIT_LEN으로 60초가 고정돼 있어 sysctl로 바꿀 수 없다. 재컴파일 없이는 조정되지 않는다.

tcp_tw_reuse와 tcp_tw_recycle 중 무엇을 켜야 하나?

tcp_tw_reuse만 쓴다. tcp_tw_recycle은 NAT 뒤 클라이언트의 연결을 깨뜨리는 문제가 있어 커널 4.12에서 제거됐고 지금 커널엔 존재하지 않는다. reuse는 아웃바운드 연결에만 작용하고 TCP 타임스탬프로 옛 연결과 구분하므로 recycle 같은 부작용이 없다.

tcp_tw_reuse를 1로 했는데 효과가 없다.

두 가지를 확인한다. 첫째, tcp_timestamps가 1(기본값)이어야 reuse가 동작한다. 타임스탬프를 끈 환경이라면 듣지 않는다. 둘째, reuse는 나가는 연결에만 적용된다. TIME_WAIT가 서비스 포트(8080 등)에 쌓이는 수신 측 문제라면 reuse로는 풀리지 않고 애플리케이션의 keepalive를 손봐야 한다.

somaxconn을 올렸는데 ss -ltn의 Send-Q가 그대로다.

실제 백로그는 커널 somaxconn과 애플리케이션이 listen()에 넘긴 backlog 중 작은 값으로 정해진다. Tomcat은 Connector의 acceptCount(기본 100), Nginx는 listen의 backlog(기본 511)가 그 값이다. 커널만 올리고 애플리케이션 값을 그대로 두면 작은 쪽에 묶이므로 양쪽을 함께 올려야 한다.

Cannot assign requested address 에러의 원인은 무엇인가?

나가는 연결에 배정할 임시 포트가 없을 때 나는 EADDRNOTAVAIL 에러다. 짧은 아웃바운드 연결을 반복해 TIME_WAIT가 ip_local_port_range 범위를 모두 점유하면 발생한다. ip_local_port_range를 넓히고 tcp_tw_reuse를 1로 켜고, 근본적으로는 커넥션 풀이나 keepalive로 연결을 재사용해 해결한다.

관련 글

댓글 0

로그인 후 댓글을 남길 수 있습니다.

아직 댓글이 없습니다.