본문 바로가기
컴퓨터

[번역] TIME_WAIT

by dbadoy 2023. 3. 6.

Coping with the TCP TIME-WAIT state on busy Linux servers

Vincent Bernat 

2014. 2. 25.

The license CC BY-NC-SA 3.0

원문

Linux 4.12 이후에는 더 이상 존재하지 않는 net.ipv4.tcp_tw_recycle 을 활성화하지 마세요. 대부분의 경우 TIME_WAIT 소켓은 무해합니다. 그렇지 않은 경우 권장 솔루션에 대한 요약으로 이동하세요.

Linux 커널 문서는 net.ipv4.tcp_tw_recyclenet.ipv4.tcp_tw_reuse의 기능에 대해 그다지 도움이 되지 않습니다. 이러한 문서 부족으로 인해 수많은 튜닝 가이드에서 이 두 설정을 모두 1로 설정하여 TIME-WAIT 상태의 항목 수를 줄이라고 조언합니다. 그러나 tcp(7) 메뉴얼 페이지에 명시된 바와 같이, net.ipv4.tcp_tw_recycle 옵션은 동일한 NAT 장치 뒤에 있는 서로 다른 두 컴퓨터의 연결을 처리하지 않기 때문에 퍼블릭 대면 서버에 상당히 문제가 되며, 이는 감지하기 어렵고 사용자를 물기 위해 기다리는 문제입니다:

 

TIME_WAIT 소켓의 빠른 재활용을 활성화합니다. 이 옵션을 활성화하면 NAT(네트워크 주소 변환)로 작업할 때 문제가 발생할 수 있으므로 권장하지 않습니다.

 

여기서는 TIME-WAIT 상태를 올바르게 처리하는 방법에 대해 더 자세히 설명하겠습니다. 또한, 우리는 리눅스의 TCP 스택을 살펴보고 있다는 점을 명심하세요. 이는 다른 방식으로 조정할 수 있는 [1]Netfilter 연결 추적과는 전혀 관련이 없습니다.

 

[1] 특히, net.netfilter.nf_conntrack_tcp_timeout_time_wait를 조작해도 TCP 스택이 TIME-WAIT 상태를 처리하는 방식에는 아무런 변화가 없습니다.

TIME_WAIT 의 상태

잠시 되돌아가서 이 TIME_WAIT 상태를 자세히 살펴봅시다. 이게 뭘까요? 아래 TCP 상태 다이어그램을 살펴봅시다:

 

TCP state diagram

연결을 먼저 종료하는 쪽만 TIME_WAIT 상태에 도달합니다. 다른 쪽 끝은 일반적으로 연결을 빠르게 제거할 수 있는 경로를 따릅니다.

ss -tan으로 현재 연결 상태를 확인할 수 있습니다:

$ ss -tan | head -5
LISTEN     0  511             *:80              *:*
SYN-RECV   0  0     192.0.2.145:80    203.0.113.5:35449
SYN-RECV   0  0     192.0.2.145:80   203.0.113.27:53599
ESTAB      0  0     192.0.2.145:80   203.0.113.27:33605
TIME-WAIT  0  0     192.0.2.145:80   203.0.113.47:50685

 

목적

TIME-WAIT 상태에는 두 가지 목적이 있습니다:

가장 잘 알려진 것은 한 연결의 지연된 세그먼트가 동일한 quadruplet (소스 주소, 소스 포트, 대상 주소, 대상 포트)에 의존하는 후속 연결에 의해 수락되는 것을 방지하는 것입니다. 시퀀스 번호도 특정 범위 내에 있어야 수락됩니다. 이렇게 하면 문제가 약간 좁혀지지만, 특히 수신 윈도우가 큰 고속 연결에서는 여전히 존재합니다. RFC 1337에서는 TIME-WAIT 상태가 부족할 때 어떤 일이 발생하는지 자세히 설명합니다. [3]다음은 TIME-WAIT 상태가 짧아지지 않았을 때 피할 수 있는 상황의 예입니다:

 

[3] RFC 1337에서 제안한 첫 번째 해결 방법은 TIME-WAIT 상태의 RST 세그먼트를 무시하는 것입니다. 이 동작은 net.ipv4.rfc1337에 의해 제어되는데, 이는 RFC에 설명된 문제에 대한 완전한 해결책이 아니기 때문에 Linux에서는 기본적으로 활성화되어 있지 않습니다.

단축된 TIME-WAIT 상태로 인해 관련 없는 연결에서 지연된 TCP 세그먼트가 수락됨

다른 목적은 원격 쪽에서 연결을 종료했는지 확인하는 것입니다. 마지막 ACK가 손실되면 원격 끝은 LAST-ACK 상태로 유지됩니다. [4]TIME-WAIT 상태가 없으면 원격 끝이 여전히 이전 연결이 유효하다고 생각하는 동안 연결이 다시 열릴 수 있습니다. SYN 세그먼트를 수신하고 시퀀스 번호가 일치하면 해당 세그먼트를 예상하지 않았으므로 RST로 응답합니다. 새 연결은 오류와 함께 중단됩니다:

 

[4] LAST-ACK 상태에 있는 동안 연결은 예상되는 ACK 세그먼트를 받을 때까지 마지막 FIN 세그먼트를 재전송합니다. 따라서 이 상태가 오래 유지될 가능성은 거의 없습니다.

마지막 ACK가 손실되어 원격 끝이 LAST-ACK 상태에 있는 경우, 동일한 쿼드러플을 사용하여 새 연결을 열어도 작동하지 않음

RFC 793에서는 TIME-WAIT 상태가 MSL 시간의 두 배로 지속될 것을 요구합니다. Linux에서는 이 지속 시간을 조정할 수 없으며 include/net/tcp.h에 1분으로 정의되어 있습니다:

#define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-WAIT
                                  * state, about 60 seconds     */

이를 조정 가능한 값으로 바꾸자는 제안이 있었지만, TIME_WAIT 상태가 좋은 것이라는 이유로 거부되었습니다.

 

문제

이제 많은 연결을 처리하는 서버에서 이 상태가 왜 문제가 될 수 있는지 살펴보겠습니다. 이 문제에는 세 가지 측면이 있습니다:

  • 연결 테이블에 사용된 슬롯이 같은 종류의 새로운 연결을 막는 경우;
  • 커널의 소켓 구조가 차지하는 메모리, 그리고
  • 추가 CPU 사용량

ss -tan state time-wait | wc -l 의 결과는 그 자체로 문제가 되지 않습니다!

 

Connection table slot

TIME-WAIT 상태의 연결은 연결 테이블에서 1분 동안 유지됩니다. 즉, 동일한 quadruplet (소스 주소, 소스 포트, 대상 주소, 대상 포트)을 가진 다른 연결이 존재할 수 없습니다.

웹 서버의 경우 대상 주소와 대상 포트는 일정할 가능성이 높습니다. 웹 서버가 L7 로드 밸런서 뒤에 있는 경우 소스 주소도 일정합니다. Linux에서 클라이언트 포트는 기본적으로 약 30,000개의 포트 범위로 할당됩니다(net.ipv4.ip_local_port_range를 튜닝하여 변경 가능). 즉, 웹 서버와 로드밸런서 간에 분당 30,000개의 연결만 설정할 수 있으므로 초당 약 500개의 연결이 가능합니다.

TIME-WAIT 소켓이 클라이언트 측에 있는 경우 이러한 상황을 쉽게 감지할 수 있습니다. connect()를 호출하면 EADDRNOTAVAIL이 반환되고 애플리케이션은 이에 대한 오류 메시지를 기록합니다. 서버 측에서는 로그와 카운터가 없기 때문에 더 복잡합니다. 확실하지 않은 경우에는 사용된 quadruplet의 수를 나열할 수 있는 합리적인 방법을 사용해야 합니다:

$ ss -tan 'sport = :80' | awk '{print $(NF)" "$(NF-1)}' | \
>  sed 's/:[^ ]*//g' | sort | uniq -c
    696 10.24.2.30 10.33.1.64
   1881 10.24.2.30 10.33.1.65
   5314 10.24.2.30 10.33.1.66
   5293 10.24.2.30 10.33.1.67
   3387 10.24.2.30 10.33.1.68
   2663 10.24.2.30 10.33.1.69
   1129 10.24.2.30 10.33.1.70
  10536 10.24.2.30 10.33.1.73

 

해결책은 더 많은 quadruplet입니다. [5]이 작업은 여러 가지 방법으로 수행할 수 있습니다(설정하기 어려운 순서대로):

  • net.ipv4.ip_local_port_range를 더 넓은 범위로 설정하여 더 많은 클라이언트 포트를 사용합니다;
  • 웹 서버에 여러 추가 포트(81, 82, 83, ...)를 수신하도록 요청하여 더 많은 서버 포트를 사용합니다;
  • 로드 밸런서에 추가 IP를 구성하여 더 많은 클라이언트 IP를 사용하고 라운드 로빈 방식으로 사용합니다.
  • [6]웹 서버에 추가 IP를 구성하여 더 많은 서버 IP를 사용합니다.[7]

[5] 클라이언트 측에서도 구형 커널은 각 발신 연결에 대해 무료 로컬 튜플(소스 주소 및 소스 포트)을 찾아야 합니다. 이 경우 서버 포트나 IP 수를 늘리는 것은 도움이 되지 않습니다. Linux 3.2는 서로 다른 대상에 대해 동일한 로컬 튜플을 공유할 수 있을 만큼 최신 버전입니다. 이 부분에 대한 통찰력을 제공해주신 Willy Tarreau에게 감사드립니다.

 

[6] 로드 밸런서가 EADDRINUSE 오류를 방지하려면 bind(), connect()를 호출하기 전에 SO_REUSEADDR 옵션을 사용해야 합니다.

 

[7] 이 마지막 해결책은 포트를 더 많이 사용할 수 있기 때문에 약간 멍청해 보일 수 있지만 일부 서버는 이런 식으로 구성할 수 없습니다. 앞의 방법도 부하 분산 소프트웨어에 따라 설정이 상당히 번거로울 수 있지만 마지막 방법보다 IP를 덜 사용합니다.

 

마지막 해결책은 net.ipv4.tcp_tw_reusenet.ipv4.tcp_tw_recycle을 조정하는 것입니다. 이 설정은 나중에 다룰 예정이므로 아직은 하지 마세요.

 

메모리

처리해야 할 연결이 많은 경우 소켓을 1분 동안 더 열어두면 서버의 메모리가 소모될 수 있습니다. 예를 들어 초당 약 10,000개의 새 연결을 처리하려는 경우 TIME-WAIT 상태의 소켓이 약 600,000개가 됩니다. 얼마나 많은 메모리를 차지하나요? 그리 많지 않습니다!

첫째, 애플리케이션 관점에서 볼 때 TIME-WAIT 소켓은 소켓이 닫혀 있기 때문에 메모리를 전혀 소비하지 않습니다. 커널에서 TIME-WAIT 소켓은 세 가지 구조로 존재합니다(세 가지 다른 용도로):

 

1) "TCP 설정 해시 테이블"이라는 이름의 연결 해시 테이블(다른 상태의 연결이 포함되어 있음에도 불구하고)은 예를 들어 새 세그먼트를 수신할 때 기존 연결을 찾는 데 사용됩니다. 이 해시 테이블의 각 버킷에는 TIME-WAIT 상태의 연결 목록과 일반 활성 연결 목록이 모두 포함됩니다. 해시 테이블의 크기는 시스템 메모리에 따라 다르며 부팅 시 인쇄됩니다:

$ dmesg | grep "TCP established hash table"
[    0.169348] TCP established hash table entries: 65536 (order: 8, 1048576 bytes)

 

커널 명령줄에서 thash_entries 파라미터로 항목 수를 지정하여 재정의할 수 있습니다.

TIME-WAIT 상태의 연결 목록의 각 요소는 구조체 tcp_timewait_sock이고, 다른 상태의 유형은 구조체 tcp_sock입니다[8].

 

[8] TIME_WAIT에서 소켓을 위한 전용 메모리 구조체를 사용하는 것은 리눅스 2.6.14부터 시작되었습니다. 구조체 sock_common은 좀 더 장황하므로 여기서는 복사하지 않겠습니다.

 

struct tcp_timewait_sock {
    struct inet_timewait_sock tw_sk;
    u32    tw_rcv_nxt;
    u32    tw_snd_nxt;
    u32    tw_rcv_wnd;
    u32    tw_ts_offset;
    u32    tw_ts_recent;
    long   tw_ts_recent_stamp;
};

struct inet_timewait_sock {
    struct sock_common  __tw_common;

    int                     tw_timeout;
    volatile unsigned char  tw_substate;
    unsigned char           tw_rcv_wscale;
    __be16 tw_sport;
    unsigned int tw_ipv6only     : 1,
                 tw_transparent  : 1,
                 tw_pad          : 6,
                 tw_tos          : 8,
                 tw_ipv6_offset  : 16;
    unsigned long            tw_ttd;
    struct inet_bind_bucket *tw_tb;
    struct hlist_node        tw_death_node;
};

 

2) 'death row'라고 하는 연결 목록 집합은 TIME_WAIT 상태의 연결을 만료하는 데 사용됩니다. 만료까지 남은 시간 순으로 정렬됩니다.

연결 해시 테이블의 항목과 동일한 메모리 공간을 사용합니다. 이것은 구조체 inet_timewait_sock의 구조체 hlist_node tw_death_node 멤버입니다[9].

 

[9] Linux 4.1부터는 성능과 병렬성을 높이기 위해 TIME-WAIT 소켓을 추적하는 방식이 수정되었습니다. 이제 death row는 해시 테이블일 뿐입니다.

 

3) 로컬로 바인딩된 포트와 관련 파라미터가 포함된 바인딩된 포트의 해시 테이블은 동적 바인딩의 경우 특정 포트를 수신해도 안전한지 또는 사용 가능한 포트를 찾는 데 사용됩니다. 이 해시 테이블의 크기는 연결 해시 테이블의 크기와 동일합니다:

$ dmesg | grep "TCP bind hash table"
[    0.169962] TCP bind hash table entries: 65536 (order: 8, 1048576 bytes)

각 엘리먼트는 구조체 inet_bind_socket입니다. 로컬로 바인딩된 각 포트마다 하나의 요소가 있습니다. 웹 서버에 대한 TIME-WAIT 연결은 포트 80에 로컬로 바인딩되며 형제 TIME-WAIT 연결과 동일한 항목을 공유합니다. 반면에 원격 서비스에 대한 연결은 임의의 포트에 로컬로 바인딩되며 해당 항목을 공유하지 않습니다.

우리는 구조체 tcp_timewait_sock과 구조체 inet_bind_socket이 차지하는 공간에만 관심이 있습니다. 인바운드 또는 아웃바운드에 관계없이 TIME-WAIT 상태의 각 연결에 대해 하나의 구조체 tcp_timewait_sock이 있습니다. 각 아웃바운드 연결에는 전용 구조체 inet_bind_socket이 하나씩 있고 인바운드 연결에는 구조체가 없습니다.

구조체 tcp_timewait_sock은 168바이트인 반면 구조체 inet_bind_socket은 48바이트입니다:

$ sudo apt-get install linux-image-$(uname -r)-dbg
[…]
$ gdb /usr/lib/debug/boot/vmlinux-$(uname -r)
(gdb) print sizeof(struct tcp_timewait_sock)
 $1 = 168
(gdb) print sizeof(struct tcp_sock)
 $2 = 1776
(gdb) print sizeof(struct inet_bind_bucket)
 $3 = 48

TIME-WAIT 상태에서 약 40,000개의 인바운드 연결이 있는 경우 10MiB 미만의 메모리를 사용해야 합니다. TIME-WAIT 상태에서 아웃바운드 연결이 약 40,000개인 경우 2.5 MiB의 추가 메모리를 고려해야 합니다. 슬랩탑의 출력을 통해 이를 확인해 보겠습니다. 다음은 TIME-WAIT 상태의 연결이 약 50,000개이고 이 중 45,000개가 아웃바운드 연결인 서버의 결과입니다:

$ sudo slabtop -o | grep -E '(^  OBJS|tw_sock_TCP|tcp_bind_bucket)'
  OBJS ACTIVE  USE OBJ SIZE  SLABS OBJ/SLAB CACHE SIZE NAME
 50955  49725  97%    0.25K   3397       15     13588K tw_sock_TCP
 44840  36556  81%    0.06K    760       59      3040K tcp_bind_bucket

여기서 변경할 사항은 없습니다. TIME_WAIT 연결에 사용되는 메모리는 매우 작습니다. 서버가 초당 수천 개의 새 연결을 처리해야 하는 경우, 클라이언트에 데이터를 효율적으로 푸시하려면 훨씬 더 많은 메모리가 필요합니다. TIME-WAIT 연결의 오버헤드는 무시할 수 있는 수준입니다.

 

CPU

CPU 측에서는 사용 가능한 로컬 포트를 검색하는 데 약간의 비용이 발생할 수 있습니다. 이 작업은 잠금을 사용하고 사용 가능한 포트가 발견될 때까지 로컬로 바인딩된 포트를 반복하는 inet_csk_get_port() 함수에 의해 수행됩니다. 이 해시 테이블에 많은 수의 항목이 있는 것은 일반적으로 TIME_WAIT 상태의 아웃바운드 연결이 많은 경우(예: 멤캐시 서버에 대한 임시 연결)에는 문제가 되지 않습니다. 연결은 일반적으로 동일한 프로필을 공유하므로 함수가 순차적으로 반복하면서 사용 가능한 포트를 빠르게 찾을 수 있기 때문입니다.

 

다른 솔루션

이전 섹션을 읽은 후에도 여전히 TIME_WAIT 연결에 문제가 있다고 생각되면 세 가지 추가 해결 방법을 통해 문제를 해결할 수 있습니다:

Socket lingering을 비활성화합니다;

  • net.ipv4.tcp_tw_reuse; 그리고
  • net.ipv4.tcp_tw_recycle

Socket lingering

close()가 호출되면 커널 버퍼에 남아있는 모든 데이터가 백그라운드로 전송되고 소켓은 결국 TIME-WAIT 상태로 전환됩니다. 애플리케이션은 즉시 작업을 계속할 수 있으며 모든 데이터가 결국 안전하게 전달될 것이라고 가정할 수 있습니다.

그러나 애플리케이션은 소켓 지연으로 알려진 이 동작을 비활성화하도록 선택할 수 있습니다. 두 가지 방식이 있습니다:

  1. 첫 번째의 경우, 남아있는 데이터는 모두 삭제되고 일반적인 4패킷 연결 종료 시퀀스로 연결을 종료하는 대신 RST로 연결이 종료되고(따라서 피어가 오류를 감지하게 됨) 즉시 파괴됩니다. 이 경우 TIME_WAIT 상태가 발생하지 않습니다.
  2. 두 번째 방식에서는 모든 데이터가 소켓 전송 버퍼에 남아 있으며, 모든 데이터가 전송되어 피어에 의해 승인되거나 구성된 링거 타이머가 만료될 때까지 close()를 호출할 때 프로세스가 절전 모드로 전환됩니다. 소켓을 비차단 상태로 설정하면 프로세스가 잠자기 상태가 되지 않을 수 있습니다. 이 경우 백그라운드에서 동일한 프로세스가 발생합니다. 이 경우 구성된 시간 초과 동안 나머지 데이터를 전송할 수 있지만 데이터가 성공적으로 전송되면 정상적인 닫기 시퀀스가 실행되고 TIME-WAIT 상태가 됩니다. 그리고 다른 경우에는 RST로 연결이 닫히고 나머지 데이터가 버려집니다.

두 경우 모두 소켓 링거링을 비활성화하는 것이 모든 경우에 적합한 해결책은 아닙니다. 상위 프로토콜 관점에서 사용하기에 안전할 때 HAProxy 또는 Nginx와 같은 일부 애플리케이션에서 사용할 수 있습니다. 무조건 비활성화하지 않는 데에는 충분한 이유가 있습니다.

 

net.ipv4.tcp_tw_reuse

TIME_WAIT 상태는 관련 없는 연결에서 지연된 세그먼트가 수락되는 것을 방지합니다. 그러나 특정 조건에서는 새 연결의 세그먼트가 이전 연결의 세그먼트로 잘못 해석되지 않는다고 가정할 수 있습니다.

RFC 1323은 고대역폭 경로에서 성능을 개선하기 위한 일련의 TCP 확장을 제시합니다. 무엇보다도 4바이트 타임스탬프 필드 2개를 포함하는 새로운 TCP 옵션을 정의합니다. 첫 번째 필드는 옵션을 전송하는 TCP의 타임스탬프 시계의 현재 값이고, 두 번째 필드는 원격 호스트에서 수신한 가장 최근의 타임스탬프입니다.

net.ipv4.tcp_tw_reuse를 활성화하면 Linux는 새 타임스탬프가 이전 연결에 대해 기록된 가장 최근 타임스탬프보다 엄격하게 큰 경우 TIME-WAIT 상태의 기존 연결을 새 발신 연결에 재사용하며, TIME-WAIT 상태의 발신 연결은 단 1초 후에 재사용할 수 있습니다.

어떻게 안전한가요? TIME-WAIT 상태의 첫 번째 목적은 관련 없는 연결에서 중복 세그먼트가 허용되는 것을 방지하는 것이었습니다. 타임스탬프를 사용하기 때문에 이러한 중복 세그먼트는 오래된 타임스탬프와 함께 제공되므로 삭제됩니다.

두 번째 목적은 마지막 ACK의 손실로 인해 원격 끝이 LAST-ACK 상태가 되지 않도록 하는 것입니다. 리모트 엔드는 FIN 세그먼트를 재전송합니다:

  • 연결을 포기하거나(연결을 끊을 때), 또는
  • 대기 중인 ACK를 수신(그리고 연결을 끊음), 또는
  • RST를 수신할 때까지(그리고 연결을 끊을 때까지) 재전송합니다.

FIN 세그먼트가 제시간에 수신되면 로컬 엔드 소켓은 여전히 TIME-WAIT 상태가 되고 예상되는 ACK 세그먼트가 전송됩니다.

새 연결이 TIME-WAIT 항목을 대체하면 타임스탬프로 인해 새 연결의 SYN 세그먼트는 무시되고 RST로 응답하지 않고 FIN 세그먼트의 재전송으로만 응답합니다. 그러면 FIN 세그먼트는 (로컬 연결이 SYN-SENT 상태에 있기 때문에) RST로 응답되어 LAST-ACK 상태에서 벗어날 수 있습니다. 응답이 없었기 때문에 초기 SYN 세그먼트는 결국 1초 후에 재전송되며 약간의 지연을 제외하고는 명백한 오류 없이 연결이 설정됩니다:

마지막 ACK가 손실되어 원격 끝이 LAST-ACK 상태로 유지되는 경우, 로컬 끝이 SYN-SENT 상태로 전환되면 원격 연결이 재설정됨

연결이 재사용되면 TWRecycled 카운터가 증가한다는 점에 유의하세요(이름과 상관없이).

 

net.ipv4.tcp_tw_recycle

이 메커니즘도 타임스탬프 옵션에 의존하지만 들어오는 연결과 나가는 연결 모두에 영향을 줍니다. 일반적으로 서버가 먼저 연결을 닫을 때 유용합니다 10^.

TIME_WAIT 상태는 더 빨리 만료되도록 예약되어 있으며, RTT와 그 편차에서 계산된 재전송 시간 초과(RTO) 간격 후에 제거됩니다. ss 명령으로 실시간 연결에 적합한 값을 확인할 수 있습니다:

10^: 서버가 먼저 연결을 닫으면 클라이언트가 해당 quadruplet을 사용 가능한 것으로 간주하여 새 연결에 재사용할 수 있는 동안 서버는 TIME-WAIT 상태를 갖게 됩니다.

$ ss --info  sport = :2112 dport = :4057
State      Recv-Q Send-Q    Local Address:Port        Peer Address:Port
ESTAB      0      1831936   10.47.0.113:2112          10.65.1.42:4057
         cubic wscale:7,7 rto:564 rtt:352.5/4 ato:40 cwnd:386 ssthresh:200 send 4.5Mbps rcv_space:5792

만료 타이머를 줄이면서 TIME-WAIT 상태가 제공하던 동일한 보장을 유지하기 위해 연결이 TIME-WAIT 상태가 되면 이전에 알려진 목적지에 대한 다양한 메트릭이 포함된 전용 구조에 최신 타임스탬프가 기억됩니다. 그러면 Linux는 타임스탬프가 가장 최근에 기록된 타임스탬프보다 엄격하게 크지 않은 원격 호스트의 세그먼트는 TIME-WAIT 상태가 만료되지 않는 한 모두 삭제합니다:

if (tmp_opt.saw_tstamp &&
    tcp_death_row.sysctl_tw_recycle &&
    (dst = inet_csk_route_req(sk, &fl4, req, want_cookie)) != NULL &&
    fl4.daddr == saddr &&
    (peer = rt_get_peer((struct rtable *)dst, fl4.daddr)) != NULL) {
        inet_peer_refcheck(peer);
        if ((u32)get_seconds() - peer->tcp_ts_stamp < TCP_PAWS_MSL &&
            (s32)(peer->tcp_ts - req->ts_recent) >
                                        TCP_PAWS_WINDOW) {
                NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_PAWSPASSIVEREJECTED);
                goto drop_and_release;
        }
}

원격 호스트가 NAT 장치인 경우 타임스탬프 조건은 동일한 타임스탬프 시계를 공유하지 않기 때문에 NAT 장치 뒤에 있는 호스트를 제외한 모든 호스트가 1분 동안 연결하는 것을 금지합니다. 이 옵션은 문제를 감지하기 어렵고 진단하기 어렵기 때문에 이 옵션을 비활성화하는 것이 훨씬 좋습니다.

Linux 4.10(커밋 95a22caee396)부터 Linux는 각 연결에 대해 타임스탬프 오프셋을 무작위로 지정하여 이 옵션이 NAT 사용 여부에 관계없이 완전히 중단됩니다. 이 옵션은 Linux 4.12에서 완전히 제거되었습니다.

LAST-ACK 상태는 net.ipv4.tcp_tw_reuse와 같은 방식으로 처리됩니다.

 

요약

보편적인 해결책은 예를 들어 더 많은 서버 포트를 사용하여 가능한 쿼드러플 수를 늘리는 것입니다. 이렇게 하면 TIME-WAIT 항목으로 가능한 연결을 모두 소진하지 않을 수 있습니다.

 

서버 측에서는 부작용이 있으므로 net.ipv4.tcp_tw_recycle을 활성화하지 마세요. 들어오는 연결에 대해 net.ipv4.tcp_tw_reuse를 활성화하면 쓸모가 없습니다.

클라이언트 측에서 net.ipv4.tcp_tw_reuse를 활성화하는 것은 거의 안전한 또 다른 해결책입니다. net.ipv4.tcp_tw_reuse 외에 net.ipv4.tcp_tw_recycle을 활성화하는 것은 대부분 쓸모가 없습니다.

 

또한 프로토콜을 설계할 때 클라이언트가 먼저 닫히지 않도록 하세요. 클라이언트는 이를 처리하는 데 더 적합한 서버에 책임을 떠넘기는 TIME_WAIT 상태를 처리할 필요가 없습니다.

 

그리고 마지막 인용문은 유닉스 네트워크 프로그래밍에 나오는 W. 리차드 스티븐스의 글입니다:

 

TIME_WAIT 상태는 우리의 친구이며 우리를 돕기 위해 존재하는 것입니다(즉, 오래된 중복 세그먼트가 네트워크에서 만료되도록 하는 것). 이 상태를 피하려고 하지 말고 이해해야 합니다.

The TIME_WAIT state is our friend and is there to help us (i.e., to let old duplicate segments expire in the network). Instead of trying to avoid the state, we should understand it.