TCP TIME_WAIT 증가의 원인

1,640 단어·4 분·원문(.md)

TIME_WAIT 현상은 tcp 연결 종료 과정인 4-way-handshake의 마지막 단계에서 발생한다.

연결을 먼저 끊자고 제안한 active close 쪽이 마지막 ack를 보낸후 일정시간(기본 2msl, 약 2~4분)동안 소켓을 바로 정리해두지 않고 남겨두는 상태를 말한다.

이를 통해 지연 패킷 처리가 가능한데 네트워크 지연으로 뒤늦게 도착한 old duplicate이 새로운 연결의 데이터로 오인되는것을 방지한다.

연결 종료 보장도 가능하고 마지막 ack가 유실되었을 경우 상대방이 fin을 재전송하면 이를다시 처리해주기 위함이다.

문제 상황 #

주요 원인은 누가 연결을 먼저 끊는가인데

보통 서버 환경에서 time wait이 폭증하는 이유는 서버가 active close의 주체가 되기 때문이다

  • HTTP/1.0 및 keep-alive 미사용: 요청마다 연결을 맺고 끊으면 매번 time wait이 발생한다.
  • 단기 커넥션 (short-lived connection): redis, db 등 외부의 api와 빈번하게 짧은 통신을 할 때 발생한다.
  • Reverse Proxy 설정: nginx같은 리버스 프록시 서버가 업스트림(app)과 통신할때 keep-alive를 쓰지 ㅇ낳으면 프록시 서버의 로컬 포트가 고갈된다.

예를들어 초당 5,000건의 요청을 처리하는 서버가 시스틈의 로컬 포트 범위 32,768 ~ 60,999 약 2만 8천개라고 가정해보겠다.

TIME_WAIT이 60초라면, 1초에 500개만 소비해도 1분에 포트가 고갈된다. 결과적으로 Can't assign requested address 에러가 발생함.

netstat -ant | awk '/^tcp/ {print $6}' | sort | uniq -c | sort -n

2 CLOSE_WAIT
15 ESTABLISHED
8 LISTEN
12 SYN_SENT
15420 TIME_WAIT


# 또는 ss 명령어 (더 빠름)
ss -s

Total: 15600 (kernel 16000)
TCP:   15500 (estab 15, closed 15420, orphaned 0, timewait 15420)

Transport Total     IP        IPv6
RAW	      0         0         0        
UDP	      5         3         2        
TCP	      80        15500     5

위 명령어로 전체 상태에 관해 TIME_WAIT 소켓이 얼마나 있는지 확인한다.

netstat -atn | grep TIME_WAIT | awk '{print $5}' | cut -d: -f1 | sort | uniq -c | sort -nr

8500 10.0.1.50
6200 172.16.0.12
700 127.0.0.1
20 211.234.xx.xx

어떤 ip port에서 많이 발생하는지 확인도 가능하다.

해결방법 #

근본적인 해결책은 연결 재사용이다.

  1. HTTP Keep-Alive 활성화: 클라이언트와 서버 간 연결을 유지해 핸드셰이크 횟수 줄이기
  2. Conenction Pool 사용: db, redis, 외부서버와 연결시 매번 맺고 끊는것이 아니라 미리 만들어진 풀을 사용하도록

커널 파라미터 튜닝으로도 할 수 잇는데

  1. net.ipv4.tcp_tw_reuse=1: time_wait 상태의 소켓을 새 연결에 재사용할 수 있게 한다.
  2. net.ipv4.ip_local_port_range: 사용할 수 있는 로컬 포트 범위를 넓힌다.
sysctl -w net.ipv4.ip_local_port_range="1024 65535"
  1. net.core.somaxconn: LISTEN 백로그 큐 크기를 늘려 연결 요청 수용력을 늘린다.

net.ipv4.tcp_tw_recycle는 사용하지 마세요 절대. 리눅스 커널 4.12 이후 삭제되었으며 NAT 환경에서 패킷드랍 문제가 있음

마지막으로 아키텍처 레벨에서 LB를 두고 커넥션 관리하게 해둔뒤에 실제 서비스는 가벼운 통신만 유지하도록 구성할수도 있다.

지연 패킷 처리, 연결 종료 보장 #

이런 궁금증이 생길 수 있다. keep alive나 connection pool을 활용해서 연글을 끊지 않고 계속 유지하거나 재사용하면

지연 패킷 처리, 연결 종료 보장이라는 처음에 정의했던 TIME WAIT의 존재이유에 관한 문제가 발생하지 않을까 싶다

연결 종료 보장 #

TIME_WAIT은 연결을 조료할 때 발생하는 상태다. 하지만 keep-alive, connection pool을 사용하면 애초에 연결 종료를 하지 않는다

  • 기존 방식은 요청 - 응답 - 연결 종료 FIN - TIME_WAIT 발생
  • 개선 방식은 요청 - 응답 - 연결유지 - 다음요청 - 응답 ...

지연 패킷 처리 old duplicate #

연결이 유지되고 있는 동안에는 tcp sequnece number가 계속 이어진다

만약 아주 오래전 지연되었던 패킷이 뒤늦게 도착하더라도, 현재 활성화된 sequence number 범위와 맞지 않기 때문에 커널 수준에서 자연스럽게 버려진다.

즉, time wait 상태에서 기다릴 필요 없이, 살아있는 연결 안에서 tcp의 표준 시퀀스 검증 로직이 중복 패킷을 걸러낸다.


그리고 지연 패킷 및 종료 이슈들은 언젠가 끊을때 처리되기도 한다.

물론 connection pool에 있는 연결도 영원히 유지는 되지 않는다. idle timeout, server restart같은거 있으면 초기화됨

연결이 끊길때 메커니즘은

  1. 최소화된 발생: 1,000번 요청을 보낼때 기존은 1,000개의 time wait이 생겼다면 풀링을 쓰면 가끔있는 1개의 연결 끊김이 time_wait이 발생한다. 시스템이 충분히 감당 가능함
  2. 안전한 종료: 연결을 끊기로 결정하면 그때서야 정상적인 4-way-handshake가 일어나고 time wait로 진입해 지연 패킷 처리하고 종료를 보장한다.

본질적으로는 keepalive, connectionpool 전략은 위험한 종료 active close의 횟수 자체를 획기적으로 줄이는 것이고

지연 패킷 처리를 포기하는것이 아니라 처리해야할 상황 자체를 관리 가능한 수준으로 통제하는 것이다.

SRE/question/q_21.md