IT_Programming/Network Programming

[펌] 리눅스 서버의 TCP 네트워크 성능을 결정짓는 커널 파라미터 이야기 1~3편

JJun ™ 2016. 8. 19. 09:46



 출처

 : http://meetup.toast.com/posts/53

 : http://meetup.toast.com/posts/54

 : http://meetup.toast.com/posts/55





연재

리눅스 서버의 TCP 네트워크 성능을 결정짓는 커널 파라미터 이야기 - 2편
리눅스 서버의 TCP 네트워크 성능을 결정짓는 커널 파라미터 이야기 - 3편


목차

  1. 들어가기 전에
  2. 준비
  3. TCP 대역폭(bandwidth) 관련 파라미터
    3.1 BDP
    3.2 TCP window scaling
    3.3 TCP socket buffer size
    3.4 congestion window size
  4. 네트워크 capacity 관련 파라미터
    4.1 maximum file count
    4.2 backlogs
    4.3 port range
  5. TIME_WAIT socket
    5.1 TIME_WAIT 상태의 소켓이 무엇일까요?
    5.2 TIME_WAIT socket buckets
    5.3 TIME_WAIT socket reuse (TW_REUSE)
    5.4 TCP timestamp
    5.5 TIME_WAIT socket recycling (TW_RECYCLE)
    5.6 Socket linger option
  6. 결론
  7. 맺으며



1. 들어가기 전에

안녕하세요. NHN엔터테인먼트 정성환 입니다.
언젠가 꼭 한번 정리하겠다고 마음 먹었는데, 이제서야 정리하게 되었습니다.

먼저, "TCP 네트워크 성능을 결정짓는..." 이라는 수식어는 사실 조금 더 자극적인 제목을 뽑아내기 위한 과욕입니다.
네트워크 성능에 가장 중요한 요소는 결국엔 애플리케이션에 있다는 점을 강조하고 싶습니다.

다만, 워크로드 특성에 따라 기본 설정된 TCP 커널 파라미터가 제약이 되어 제성능을 발휘할 수 없을때도 있는데요.
본문에서는 이러한 내용들을 다루고 있습니다.

매우 많은 커널 파라미터가 있겠지만, 본문에서는 네트워크 대역폭(bandwidth)에 대한 커널 파라미터,
네트워크 수용량(capacity)에 대한 커널 파라미터를 주로 다룹니다.


2. 준비

리눅스는 sysctl 명령어로 손쉽게 커널 파라미터를 런타임 중에 변경할 수 있습니다.
다음과 같은 명령어를 사용하면 현재 커널 파라미터 설정값 전체를 열람할 수 있습니다.

$ sysctl -a

본 문서에서는 네트워크, 특히 TCP의 capacity와 bandwidth 등을 조정(tuning)할 수 있는 커널 파라미터 중 아주 일부 만을 다룹니다.
대개, TCP와 관련된 커널 파라미터는 net.core, net.ipv4, net.ipv6 등의 접두사를 붙이고 있습니다.

또, 다음과 같은 명령어로 현재 설정값을 변경할 수 있습니다.
예를 들어 net.core.wmem_max 라는 설정값을 16777216이라고 변경하려면, 다음과 같이 입력하면 됩니다.

$ sysctl -w net.core.wmem_max="16777216"

시스템 부팅시 설정되도록 하려면, /etc/sysctl.conf 파일에 해당 설정값을 기입하면 됩니다.



3. TCP 대역폭(bandwidth) 관련 파라미터


3.1 BDP

TCP의 대역폭을 이해하려면 먼저 BDP(Bandwidth Delay Product)를 이해할 필요가 있습니다.

먼저, 100Mbps의 대역폭을 가지는 네트워크에서 어떤 두 host A, B 사이 RTT(Round-trip time)가 2초인 네트워크 경로가 있다고 가정합시다.
A에서 B까지 데이터가 계속 전송되고 있을때, A에서 출발하였으나 아직 B에 도착하지 않은 데이터 양(bits in flight)은 얼마일까요?

아래 그림을 보면 조금 이해가 쉬울 것 같습니다.

대역폭을 너비로, 지연시간 RTT를 길이로 생각하면, 이 둘의 곱이 네트워크 경로상 떠다니는 데이터 양의 최대치를 나타낼 것입니다.
즉, 대역폭과 지연시간의 곱, 다시 말해 BDP는 어느 네트워크 경로에 전달중인 데이터(패킷)의 양을 나타냅니다.

이 예제에서 BDP는 100Mb/s * 2s = 200Mb / 8 = 25MB 정도입니다.

다른 예를 들어보자면, 어느 서버와 LTE 네트워크(40Mbps, 40ms RTT)로 연결된 단말이 있다고 가정합시다.
이때 BDP는 (40(10^6))bit/s (40(10^-3))s =1600(10^3)bit = 1.6Mb / 8 = 0.2MB 정도일 것 입니다.

BDP의 수식을 이용하면, 다음과 같은 식을 도출할 수 있을 것입니다.

Bandwidth = BDP / RTT

그런데 말입니다. 실제 인터넷에서 BDP는 매우 충분히 큽니다.
용량이 큰 백본망을 비록한 물리적인 네트워크 환경이 예전과 다르게 비약적으로 좋기 때문이죠.
BDP가 어느 정도로 크냐면 receiver의 receiver window size를 온당히 감당할만큼 크다고 할 수 있습니다.

즉, 인터넷의 경우 Bandwidth = (receiver window size) / RTT 관계도 성립한다는 이야기이죠.

다시 정리하자면, 인터넷의 경우 대역폭을 높이기 위해서는 RTT를 낮추거나 receiver window size를 키우면 됩니다.
그런데 RTT는 peer간 물리적인 거리에 종속적이어서 낮추기 힘듭니다.
즉, 대역폭을 높이려면 receiver window size를 증가시켜야 합니다.

그렇다면, receiver window size를 증가하려면 어떻게 해야 할까요?


3.2 TCP window scaling

기본적으로 TCP 연결을 맺을 때, SYN 패킷에는 receiver window size를 공고(advertising) 하도록 되어 있습니다.
이 값의 범위는 0~65,535까지인데요. 즉, 64KB까지 지정할 수 있습니다.
인터넷이 처음 나올적의 예전이라면 모르겠지만, 지금 세상에서 64KB는 꽤나 작은 데이터양이겠지요.

RFC 1323에서는 TCP window scaling이라는 옵션을 정의하고 있습니다.
TCP 헤더의 옵션 필드에 window scale라는 필드를 정의하여 advertise할 수 있는 receiver window size를 키울 수 있도록 합니다.

이 값은 0~14까지 지정할 수 있습니다. 그리고 이 값을 n이라 한다면, 2^n 값을 window scaling factor라고 합니다.
TCP window scaling을 설정하면, 실제 receiver window size는 기존의 window size 값과 이 window scaling factor의 곱으로 구할 수 있습니다.

예를 들어, window size 값이 8,192이고 window scale 값이 8이라면 실제 receiver window size는 8,192 * (2^8) = 2,097,152 바이트가 됩니다.

참고로, TCP window scaling을 사용할 때 최대 receiver window size는 65,535 * (2^14) = 1,073,725,440 바이트(1GB)입니다.

TCP window scaling을 활성화하려면, 커널 파라미터 'net.ipv4.tcp_window_scaling' 값이 '1'로 설정되어 있어야 합니다.
다음과 같은 명령어를 사용하면 활성화 할 수 있습니다.

$ sysctl -w net.ipv4.tcp_window_scaling="1"

참고로, 통신하는 두 host 양 측 모두 TCP window scaling 옵션을 활성화해야 올바르게 동작하게 됩니다.
그리고 일반적으로 클라이언트 OS(Windows, MAC OS X, iOS, Android)들은 이 옵션이 활성화되어 있습니다.


3.3 TCP socket buffer size

TCP window scaling을 이용하여 receiver window size의 한계치를 증가하더라도, 실제 커널에 설정된 소켓 버퍼 크기보다 커질 수는
없을 것 입니다. 결국 receiver window size를 증가하기 위해서는, 소켓당 버퍼 크기를 증가시켜야 합니다.

이와 관련된 커널 파라미터는 다음과 같습니다.

- net.core.rmem_default
- net.core.wmem_default
- net.core.rmem_max
- net.core.wmem_max
- net.ipv4.tcp_rmem
- net.ipv4.tcp_wmem

여기서 rmem은 receive(read) buffer의 크기, wmem은 send(write) buffer의 크기를 나타냅니다.
지정되는 값들의 단위는 바이트(bytes) 입니다.

먼저 net.core 접두사가 붙은 커널 파라미터 부터 살펴 보겠습니다.
이는 TCP를 포함한 모든 종류의 소켓에 기본적으로 설정되는 버퍼 크기를 나타냅니다.
접미사 default는 그 기본값이고, max는 소켓이 가질 수 있는 최대 크기를 나타냅니다.

net.ipv4 접두사가 붙은 커널 파라미터는 TCP 소켓에 대한 부분을 설정합니다.
참고로, 이 설정값은 ipv6에서도 적용되는데, 리눅스에서는 일부 ipv4 커널 파라미터가 ipv6까지 적용 되기 때문입니다.
(ipv6에도 적용되는 커널 파라미터: net.ipv4.ip_, net.ipv4.ip_local_portrange, net.ipv4.tcp, net.ipv4.icmp_*)

각 커널 파라미터는 min / default / max로 세 정수 값으로 설정 할 수 있습니다.
min은 TCP memory pressure 상태일 때 소켓에 할당되는 버퍼 크기를 나타내고요, max는 TCP 소켓이 가질 수 있는 최대 크기를 나타냅니다.
TCP memory pressure 상태에 대해서는 조금 더 아래에서 다루도록 하겠습니다.
중간값은 default로 net.core에서 설정된 default 값을 TCP 소켓에 한정하여 덮어 씌웁니다.
특히, default 값은 TCP receive window 크기를 결정할 때 가장 주요하게 참조되는 값입니다.

이 커널 파라미터의 기본값은 리눅스 커널에 의하여 자동으로 설정(auto-tuned)되나, 대개 default 값이 128KB 정도로 설정되어 있는데요.
비교적 메모리 양이 적고, 대규모/대용량 패킷처리를 하지 않는 데스크탑에서는 적합한 설정입니다.
서버의 경우 일반적으로 메모리 양이 크기에 적절히 크기를 늘려줘도 좋을 것 같습니다. (네트워크 대역폭 - 메모리 사용량의 trade-off)

TCP receive window size를 증가시키려면, 위에서 나열한 커널 파라미터를 적당히 설정해야 합니다.
어떤 워크로드에서도 적용 가능한 완벽한 커널 파리미터는 없습니다.
다만 이 경우 trade-off 관계가 메모리 사용량 밖에 없기에, 아래와 같은 설정값을 조심스레 제안합니다.
(적당히 보수적으로 상향된 설정값입니다.)

$ sysctl -w net.core.rmem_default="253952"
$ sysctl -w net.core.wmem_default="253952"
$ sysctl -w net.core.rmem_max="16777216"
$ sysctl -w net.core.wmem_max="16777216"
$ sysctl -w net.ipv4.tcp_rmem="253952 253952 16777216"
$ sysctl -w net.ipv4.tcp_wmem="253952 253952 16777216"

이 외에 'net.ipv4.tcp_mem'이라는 커널 파라미터가 있습니다.
이 커널 파라미터는 커널에서 TCP를 위해 사용할 수 있는 메모리 크기를 지정합니다.
위에서 소개한 파라미터들은 개별 TCP 소켓당 지정되는 값이라면, 이 값은 TCP 소켓 전체에 대한 값입니다.

이 설정값은 위에서 소개한 커널 파라미터들과 유사하게 min / pressure / max 값을 지정할 수 있습니다.
이 중 pressure는 net.ipv4.tcp_rmem, net.ipv4.tcp_wmem에서 잠깐 언급한 memory pressure의 threshold 값 입니다.
즉, TCP 소켓 전체에서 사용되는 메모리가 이 값을 초과하면, TCP memory pressure 상태가 되어 이후 소켓은 지정된 min 값의 메모리 버퍼 크기를 가지게 되는 것이죠.

다음과 같은 명령어로 현재 커널 파라미터 설정값을 확인 할 수 있습니다.

$ sysctl net.ipv4.tcp_mem
net.ipv4.tcp_mem = 185688    247584    371376

위 값은 부팅시 시스템의 메모리에 맞추어 자동으로(auto-tuned) 설정됩니다.
한가지 유의할 점은, 되도록 이 커널 파라미터 설정값을 수정하지 말아야 한다는 점 입니다.
왜냐하면, 이미 커널에 의해 시스템 메모리에 맞게 최적화된 값이 설정되기 때문입니다.

구글 검색 등으로 찾을 수 있는 커널 파라미터 설정 관련 문서 중 일부를 살펴보면, 이 값을 대폭 올리도록 가이드 하기도 하는데요.
쉽사리 찾을 수 있는 어떤 문서에는 다음과 같이 가이드 합니다.

$ sysctl -w net.ipv4.tcp_mem="8388608 8388608 8388608"

이 설정값이 왜 말이 안되는 설정값이냐면, 그 단위가 바이트(byte)가 아니라 페이지(page)이기 때문입니다.
리눅스에서는 기본적으로 1 페이지는 4,096 바이트입니다.
즉, 위 설정값대로 라면 8,388,608은 32기가바이트를 뜻 합니다. (커도 너무 크죠.)


3.4 congestion window size

그럼 receiver가 공고(advertising)한 receive window size 만큼 sender는 데이터를 네트워크로 패킷을 보낼 수 있을까요?
결론부터 말하자면, 그렇지 않습니다.

네트워크는 네트워크에 연결된 모든 노드들간 공유하는 공유 자원입니다. 개개 노드가 이를 탐욕적으로 사용하면,
네트워크 전체가 마비될 수 있습니다. 그렇기 때문에, 각 노드들은 적당한 congestion avoidance algorithm을 사용하여 보내는 데이터 양을
자체적으로 조정하고 있습니다.

이 congestion avoidance algorithm은 receiver와는 상관없이 (receiver window size 등 peer가 알려주는 정보와는 무관하게)
독자적으로 네트워크에 보낼 데이터 양을 정합니다.
리눅스에서는 reno, vegas, new reno, bic, cubic 등의 congestion avoidance algorithm을 사용할 수 있는데,
요즈음의 일반적인 리눅스 배포판에서는 cubic가 기본적으로 설정되어 있습니다.

congestion avoidance algorithm은 몇 가지 파라미터를 참조(RTT가 가장 주요한 파라미터가 될 것입니다.)하여
congestion window 크기를 설정하게 되는데요. 이 크기가 한번에 보낼 수 있는 데이터 양의 최대치가 될 것입니다.
congestion window 크기는 애플리케이션이나 커널 파라미터로 설정 될 수 있는 값은 아닙니다.

리눅스에서는 ss와 같은 유틸리티를 통해 각 소켓별 현재 congestion window 크기를 확인할 수 있습니다.

$ ss -n -i
Netid State      Recv-Q Send-Q                                Local Address:Port                                                         Peer Address:Port
tcp    ESTAB      0      0                                                                10.77.57.57:33000                                                           10.77.57.57:47142
     cubic wscale:7,7 rto:208 rtt:5.236/10.107 ato:40 mss:65483 cwnd:10 send 1000.5Mbps rcv_rtt:4 rcv_space:43690

이 소켓은 congestion avoidance algorithm으로 cubic을 사용하고, 현재 congestion window size가 10임을 알 수 있습니다.
즉, 한번에 보낼 수 있는 패킷 개수는 10개이며 약 15KB 정도의 데이터를 한번에 보낼 수 있을 것입니다.

이 congestion window size는 TCP의 혼잡 제어 전략에 따라 slow start 방법으로 최초 연결시 정해진 'initial congestion window size(CWND)'부터 어느 정도 선까지 지속적으로 증가하게 됩니다.
그리고 통신이 지속적으로 진행되면서 receiver로 부터 ACK 패킷을 받으면, congestion window size를 현재 크기의 2배 만큼씩 증가 시킵니다.
(그 와중에 패킷이 유실되거나 한다면 congestion window size를 감소시킬텐데요. 이는 congestion avoidance algorithm에 따라 얼마만큼 경감할지 달라집니다.)

그런데, 이러한 특성 때문에 RTT가 비교적 높은 모바일 환경에서 취약한 면이 있습니다.

예를 들어, receive window size가 64KB인 receiver가 있다고 합시다. sender의 initial congestion window가 1이라면,
congestion window size가 64KB에 도달하기 위해서는 peer로 부터 6번의 ACK을 받아야 합니다.
이 때 RTT가 500ms라면 3초 이후에야 receive window size만큼 congestion window size가 증가될 것입니다.

이러한 이유 때문에 구글에서는 2010년경 TCP initial congestion window size를 10으로 상향(일반적으로 1 혹은 2로 설정) 하자는
의견을 내놓기도 했습니다.

그러나 다시 한번 이야기 하자면, 모든 워크로드와 조건을 만족하는 설정값은 없습니다.
네트워크 상 모든 peer들이 initial congestion window size를 10으로 증가 설정하고, 항상 이 패킷수보다 많은 통신을 빈번히 하게 된다면
전체 네트워크가 혼잡한 상황이 되는 재앙이 발생할 수도 있을 것입니다.
또, 한번에 주고 받는 패킷 크기가 상대적으로 작다면 initial congestion window size를 조정해도 크게 이득이 없을 수 있습니다.

initial congestion window size를 변경하기 위해서는, 커널 파라미터가 아니라 ip route 명령을 사용할 수 있습니다.
먼저, 현재의 라우팅 정보를 확인하려면 아래와 같은 명령어를 사용합니다.

$ ip route show
192.168.1.0/24 dev eth0  proto kernel  scope link  src 192.168.1.100  metric 1 
169.254.0.0/16 dev eth0  scope link  metric 1000 
default via 192.168.1.1 dev eth0  proto static

여기서는 default 라우팅 설정을 변경할텐데요. 위의 설정값을 토대로 아래와 같이 입력하여 initial congestion window size를 변경합니다.

$ ip route change default via 192.168.1.1 dev eth0  proto static initcwnd 10

적용되었음을 확인하려면, 아래와 같은 명령어를 사용합니다.

$ ip route show
192.168.1.0/24 dev eth0  proto kernel  scope link  src 192.168.1.100  metric 1 
169.254.0.0/16 dev eth0  scope link  metric 1000 
default via 192.168.1.1 dev eth0  proto static  initcwnd 10

참고로 특정 커널 버전(2.6.18)에서는 ethernet 설정에서 TSO를 활성화하면 이 수치가 무시되는 버그가 있습니다.
(비교적 오래된 커널이라 최근 배포판에서는 관계가 없지만, Redhat/CentOS 5 버전이 이 커널을 사용합니다.)

또 살펴볼 만한 slow start와 관련된 리눅스 커널 파라미터는 'net.ipv4.tcp_slow_start_after_idle' 입니다.
이 파라미터는 0 혹은 1로 설정할 수 있는데요.

1로 설정되어 있으면 congestion window size가 증가된 소켓이라도 특정 시간 동안의 idle(통신이 없는) 상태에 지속되면,
다시 slow start를 통해 initial congestion window size에서 부터 congestion window size를 증가해야 합니다.
반대로 0으로 설정되어 있으면, 일정 시간 통신이 없더라도 congestion window size가 유지 됩니다.








연재

리눅스 서버의 TCP 네트워크 성능을 결정짓는 커널 파라미터 이야기 - 1편
리눅스 서버의 TCP 네트워크 성능을 결정짓는 커널 파라미터 이야기 - 3편


목차 - 2편

4. 네트워크 capacity 관련 파라미터
    4.1 maximum file count
    4.2 backlogs
    4.3 port range



4. 네트워크 capacity 관련 파라미터


4.1 maximum file count

리눅스를 비롯한 일반적인 유닉스에서 소켓은 마치 파일과 같은 취급을 받습니다.
전체 시스템에서 가질 수 있는 파일 개수가 제한이 있다면, 당연히 소켓의 전체 개수에 영향 미칠 것 입니다.

리눅스에서 전체 시스템이 가질 수 있는 최대 파일 개수 제한은 'fs.file-max' 커널 파라미터에서 설정 됩니다.

현재 설정값을 확인하려면, 아래와 같은 명령어를 사용합니다.

$ sysctl fs.file-max
fs.file-max = 775052

이 값은 일반적으로 적당히 큰 값이 설정되어 있으므로, 웬만하면 손 볼 일이 없을 것입니다.
다만, 시스템이 굉장히 많은 파일과 소켓을 사용하는 경우, 이 값에 의해 시스템이 오동작 할 수 있으니 참고바랍니다.
(이 값을 넘어가면 open() 시스템 콜에서 'Too many open files'와 같은 에러가 발생 될 것 입니다.)

다시 말하자면, 시스템 전체에 대한 허용 소켓 개수는 'fs-file-max' 커널 파라미터 설정값이 적당히 높게 설정되어 있으므로
큰 문제가 안됩니다. 사실, 어떤 프로세스가 가질 수 있는 소켓 개수 제약은 그보다는 프로세스별 제한 설정인 user limit 값을
살펴봐야 할 것 입니다. 다음과 같은 명령어로 이를 확인할 수 있습니다.

$ ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 30473
max locked memory       (kbytes, -l) 64
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 30473
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

여기서 open files가 프로세스가 가질 수 있는 소켓 포함 파일 개수입니다.
적당량 증가시키기 위해서는 다음과 같은 명령어를 사용합니다.

$ ulimit -SHn 65535

많은 개수의 소켓을 사용하는 서버 프로그램은 구동하기 전, ulimit 명령어로 프로세스 당 최대 파일 개수를 증가시켜주어야 할 것 입니다.
(혹은, 해당 애플리케이션 로직내에서 setrlimit() 시스템 콜로 이를 증가시키는 방법도 있습니다.)

'fs.file-max'와 유사한 이름의 'fs.file-nr'이라는 커널 파라미터가 있는데, 사실 이 파라미터는 일반적인 파라미터가 아니라
현재 열려 있는 파일 현황을 나타냅니다. 아래와 같은 명령어로 현재 현황을 알 수 있습니다.

$ sysctl fs.file-nr
fs.file-nr = 5024    0    775052

세 값은 각각 현재 열려 있는 파일의 수, 현재 열려 있으나 사용되지 않는 파일의 수, 열 수 있는 파일의 최대 개수를 뜻합니다.
물론 시스템 전체에 대한 수치입니다.


4.2 backlogs

네트워크 패킷은 그 생성가 전달, 그리고 소모에 이르기까지 많은 처리 과정을 거치게 됩니다.
각각의 처리 과정을 파이프라고 본다면, 모든 처리 과정 앞에는 각각 queue가 존재한다고 할 수 있을 것 입니다.
네트워크 패킷 처리량이 갑자기 급증했을 때, 이 queue의 크기가 이보다 작다면 넘치는 패킷에 대해서는 처리되지 않고 버려질 것 입니다.

서버 커널 설정값에 있어 out-bound queue 보다는 in-bound queue가 더 민감한데, 왜냐하면 out-bound로 보내지는 패킷량은
서버 애플리케이션에서 조절할 수 있기 때문입니다. (각각의 요청은 그 처리 시간이 다르기에 out-bound시 적당히 랜덤하게
분배되는 효과도 있습니다.) 또, in-bound queue가 넘처서 버려지는 패킷은 애플리케이션에서 전혀 알 수 없기 때문에
대규모 패킷 처리가 필요한 서버에서는 적당히 in-bound queue 길이를 증가시켜야 합니다.

먼저 'net.core.netdev_max_backlog' 커널 파라미터에 대해 알아봅시다.
이 파라미터는 각 네트워크 장치 별로 커널이 처리하도록 쌓아두는 queue의 크기를 설정합니다.
커널의 패킷 처리 속도가 이 queue에 추가되는 패킷의 인입 속도보다 떨어진다면 미처 queue에 추가되지 못한 패킷들은 버려질 것입니다.

이 커널 파라미터도 trade-off 관계가 메모리 사용량 밖에 없으므로, 적당히 증가시켜두는 것도 괜찮습니다.
다음과 같은 명령어로 설정값을 적당히 증가 시킬 수 있습니다.

$ sysctl -w net.core.netdev_max_backlog="30000"

구글에서 찾을 수 있는 몇 커널 파리미터 튜닝 관련 글에는, 이 커널 파라미터가 listen backlog라고 잘못 소개되기도 하는 것 같습니다.
listen backlog, 즉 listen()으로 바인딩 된 서버 소켓에서 accept()를 기다리는 소켓 개수에 관련된 커널 파라미터는
'net.core.somaxconn'입니다.

이 값은 listen() 시스템 콜의 매개변수로 설정하는 backlog 값의 hard limit입니다.
서버 애플리케이션에서 listen()시 적당히 설정해야겠지만, 먼저 이 hard limit을 증가시켜야 할 것입니다.

다음과 같은 명령어로 이 설정값을 증가 시킬 수 있습니다.

$ sysctl -w net.core.somaxconn="1024"

참고로, 일반적인 리눅스 배포판의 기본 설정값은 128입니다. 그런데, 아파치 웹 서버의 경우 listen()시 지정되는 backlog의 기본값이 511입니다. 하지만 이 커널 파라미터가 hard limit이기에 애플리케이션에서 511으로 지정되었더라도 실제로 할당되는 listen backlog의 수는 128개가 될 것 입니다.

또, 'net.ipv4.tcp_max_syn_backlog'라는 listen backlog와 연관된 커널 파라미터가 있습니다.
'net.core.somaxconn'이 accept()을 기다리는 ESTABLISHED 상태의 소켓(즉, connection completed)을 위한 queue라면,
'net.ipv4.tcp_max_syn_backlog'는 SYN_RECEIVED 상태의 소켓(즉, connection incompleted)을 위한 queue입니다.

이 설정값도 아래와 같이 적당히 증가 시킵니다.

$ sysctl -w net.ipv4.tcp_max_syn_backlog="1024"

한가지 유의할 점은 커널 파리미터 설정값을 상향하더라도, 실제 서버 애플리케이션에서 listen backlog를 증가시키려면
listen() 시스템 콜 호출시 매개변수 backlog에 필요한 값을 전달해야 합니다.


4.3 port range

TCP 연결을 맺을때, 클라이언트 소켓은 하나의 포트를 선점해야 합니다.
TCP 연결은 출발지(source) 주소, 출발지 포트, 목적지(destination) 주소, 목적지 포트를 그 구분자로 하기 있기 때문입니다.
클라이언트에서 서버로 연결을 맺을때, 특별히 bind() 시스템 콜로 출발지 포트를 지정(bind)하지 않는다면, 커널은 임의의 포트를 할당합니다.
그리고 이러한 포트를 ephemeral port라고 통칭합니다.

즉, 클라이언트 소켓은 서버에 연결을 맺을때 포트라는 자원을 하나 소모하며, 포트는 유한한 자원이기에
한 시스템에서 동시에 가질 수 있는 클라이언트 소켓의 수는 한정적입니다.

반대로, listen()으로 클라이언트의 요청을 기다리고 있는 서버 포트는 TCP 연결을 맺을 때 추가적인 포트를 소모하지 않습니다.
때문에 일반적인 서버에서는 가질 수 있는 포트 수와 서버의 클라이언트 동시 연결 수는 크게 관계가 없습니다.

다만, 서버의 유형 중 proxy 서버에 대해서 생각해 볼 필요가 있습니다.
사용자(클라이언트)로 부터 요청을 받아, 이를 다른 백엔드 서버에 전달하는 유형의 서버를 말하는데요.
이 경우 해당 서버는 다른 백엔드 서버에 연결하기 위한 클라이언트 소켓이 필요합니다.

만약, 사용자 요청이 동시에 10,000개 들어오는데 해당 서버가 가질 수 있는 클라이언트 소켓 수가 동시에 100개라면
9,900개의 요청은 처리되지 못하고 대기해야 할 것 입니다.

어떤 시스템에 동시에 가질 수 있는 클라이언트 소켓 수를 결정하는 커널 파라미터는 'net.ipv4.ip_local_port_range' 입니다.
커널은 ephemeral port를 생성할 때 이 범위내 사용하지 않는 포트를 골라 할당하게 됩니다.

다음과 같은 명령어를 통해 현재 설정값을 확인할 수 있습니다.

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

위에서 두 값은 각각 사용할 포트 범위의 시작과 끝을 나타냅니다. 이 시스템에서는 최대 28,232개의 ephemeral port를 할당할 수 있습니다.
최대한 넓은 ephemeral port 범위를 가지려면 아래와 같은 명령어를 사용할 수 있습니다. (well-known port는 제외)

$ sysctl -w net.ipv4.ip_local_port_range="1024 65535"

그런데, 이렇게 설정하더라도 실제 시스템에서 동시에 가질 수 있는 클라이언트 소켓 수는 이에 미치지 못할 수 있습니다.

TCP 연결은 굉장히 결합도가 낮은 네트워크 환경을 가정하고 있습니다. 때문에 네트워크 상황에 의해 패킷 순서가 뒤바뀌거나 유실 되는 등의 처리를 위해, 소켓 종료시에도 되도록 우아하게 종료(gracefully shutdown)하도록 되어 있는데요.
이 얘기인 즉슨, 소켓이 사용하는 자원을 되도록 늦게 반환한다는 것입니다. ephemeral port를 포함해서요.

특히, 클라이언트 소켓에서 close() 시스템 콜로 먼저 소켓을 닫는 경우 소켓은 TIME_WAIT 상태에 머무르게 됩니다.
이 동안 이 소켓에 할당되어 있는 ephemeral port는 사용될 수 없고 그만큼 동시에 가질 수 있는 클라이언트 소켓 수는 제한되겠죠.










연재

리눅스 서버의 TCP 네트워크 성능을 결정짓는 커널 파라미터 이야기 - 1편
리눅스 서버의 TCP 네트워크 성능을 결정짓는 커널 파라미터 이야기 - 2편


목차 - 3편

5. TIME_WAIT socket
    5.1 TIME_WAIT 상태의 소켓이 무엇일까요?
    5.2 TIME_WAIT socket buckets
    5.3 TIME_WAIT socket reuse (TW_REUSE)
    5.4 TCP timestamp
    5.5 TIME_WAIT socket recycling (TW_RECYCLE)
    5.6 Socket linger option
6. 결론
7. 맺으며
8. FAQ



5. TIME_WAIT socket

앞서 말씀드린 대로, TIME_WAIT 상태의 소켓은 가용한 local port 수를 경감시켜 동시에 가질 수 있는 클라이언트 소켓 수를 제약합니다.
본 장에서는 이러한 TIME_WAIT 상태의 소켓에 대해 이야기하도록 하겠습니다.


5.1 TIME_WAIT 상태의 소켓이 무엇일까요?

그렇다면, 정확히 TIME_WAIT 상태의 소켓은 언제 발생할까요?
먼저, TCP 소켓 상태 전이도를 살펴봅시다. 아래 그림은 위키피디아에서 찾아 볼 수 있는 TCP 소캣 상태 전이도입니다.

위 그림에서 알 수 있듯이, active closing 하는 소켓의 마지막 종착지가 TIME_WAIT 상태입니다.
바꾸어 말하면, 클라이언트 소켓이든 서버 소켓이든 close() 시스템 콜을 먼저 호출한 쪽(active closing)이 최종적으로 가게됩니다.

이후 TIME_WAIT 상태에서는 RFC793에 정의대로라면 2MSL(Maximum Segment Lifetime), 즉 2분 동안 대기하게 됩니다.
그런데, 실제로 대부분의 OS에서는 최적화를 위해 1분 정도를 TIME_WAIT 상태에서 대기하도록 구현되어 있습니다.
리눅스도 이 시간을 1분으로 규정하고 있고, 변경이 불가능합니다. (커널 코드에 상수로 정해져 있습니다.)

일부 구글에서 찾아볼 수 있는 문서에는 'net.ipv4.fin_timeout'을 수정하면, TIME_WAIT에서 대기하는 시간을
수정 가능하다고 말하고 있는데요. 'net.ipv4.fin_timeout'은 FIN_WAIT_2 상태에 머무를 수 있는 최대 시간을 설정합니다.
(RFC에서는 TIME_WAIT 상태 외에는 별도 timeout을 정의하고 있지 않지만, 대부분의 시스템에서는 최적화를 위해 별도 timeout 시간을
둡니다.)

실제 세계에서 FIN_WAIT_2에 머무르는 소켓은 매우 드물고, 어차피 자연스레 TIME_WAIT 상태로 전이되기에
이 설정값은 기본으로 두어도 큰 문제가 없습니다.

한가지 강조하고 싶은 것은, TIME_WAIT 상태는 '
잘못된' 상태가 아니라는 것 입니다.
소켓의 지극히 정상적인 생명 주기 중 한 상태이며, 소켓의 gracefully shutdown을 보장하는 불가결한 요소입니다.
그리고 느슨히 결합된 네트워크 특성상 소켓은 되도록 gracefully shutdown 되어야 데이터의 유실 등의 문제가 없습니다.

먼저, 웹 서버를 예로 들어 TIME_WAIT 상태의 소켓을 확인해 보도록 하겠습니다.

HTTP keepalive 옵션을 사용하지 않는 웹서버가 있다고 합시다.
이 서버에 가서 다음과 같은 명령어를 사용하면 현재 TIME_WAIT 상태의 소켓을 확인할 수 있습니다.

$ netstat -n -t | grep 'TIME_WAIT'
Proto Recv-Q Send-Q Local Address           Foreign Address         State
tcp6       0      0 10.77.57.106:10080      10.77.94.26:37901       TIME_WAIT  
tcp6       0      0 10.77.57.106:10080      10.77.94.26:37900       TIME_WAIT  
tcp6       0      0 10.77.57.106:10080      10.77.94.26:37905       TIME_WAIT  
tcp6       0      0 10.77.57.106:10080      10.77.56.86:41811       TIME_WAIT  
tcp6       0      0 10.77.57.106:10080      10.77.56.82:48168       TIME_WAIT

위 예제는 서버 포트 10080을 사용하는 웹서버에서 발생한 TIME_WAIT 상태의 소켓들입니다.

HTTP 1.1에서는 스펙상 누가 먼저 소켓을 끊을지 정의하고 있지 않습니다.
하지만 일반적인 구현에서 keepalive 설정을 사용하지 않는다면, 서버에서 클라이언트로 데이터를 전송 후
즉시 close() 시스템 콜을 불러 연결을 끊습니다. 그래서 위 경우처럼 서버에 TIME_WAIT 상태의 소켓이 남게 되는 것 입니다.

그렇다면, 이 경우(서버 소켓에서 TIME_WAIT이 발생하는 경우) TIME_WAIT 소켓이 많아지는 것이 문제가 될까요?
결론부터 말하자면, 아주 크게는 문제 되지 않는다는 것 입니다.


5.2 TIME_WAIT socket buckets

먼저, 어떤 시스템이 가질 수 있는 TIME_WAIT 상태 소켓 개수 제한을 확인해 봅시다.
'net.ipv4.tcp_max_tw_buckets' 커널 파라미터에 이 값이 설정되어 있습니다.
다음과 같은 명령어로 이 설정값을 확인할 수 있습니다.

$ sysctl net.ipv4.tcp_max_tw_buckets
net.ipv4.tcp_max_tw_buckets = 65536

이 시스템에서 TIME_WAIT 상태의 소켓은 동시에 65,536개 존재할 수 있습니다.
그렇다면, 만약 TIME_WAIT 상태의 소켓 개수가 이보다 많아지면 어떻게 될까요? 서버 애플리케이션에 문제가 발생할까요?

TIME_WAIT 상태의 소켓은 위 설정된 값 보다 많아질 수 없습니다.
이미 이 설정값 만큼의 TIME_WAIT 상태의 소켓이 있다면, TIME_WAIT 상태로 전이되어야 할 소켓은 더이상 대기하지 않고
파괴(destroy)되어 버립니다. 즉, gracefully shutdown하지 않고 과격하게 닫혀버리는 것이지요.
그리고 서버 애플리케이션은 이를 알지 못합니다. 일반적으로 이런 경우, 소켓은 파괴되고 '/var/log/messages'와 같은 곳에
다음과 같은 로그 메시지가 남을 것입니다.

TCP: time wait bucket table overflow

이렇게 파괴된 소켓은 TCP의 gracefully shutdown 원칙에 벗어나지만, 서버 성능이나 수용량에 큰 영향을 주지 않습니다.
그러나, 소켓이 gracefully shutdown 되지 않으므로 서버에서 클라이언트로 아직 내보내지지 않는 데이터도 즉시 사라지게 됩니다.
이 경우, 클라이언트에서는 불완전한 데이터를 수신하게 될 수 있습니다.

따라서, 되도록 소켓은 gracefully shutdown 되어야 하며, 요청량이 많은 서버의 경우 이 값을 적당히 높여두는 것이 좋습니다.
역시 메모리양이 주요 trade-off 요인이기에, 상대적으로 메모리 양이 충분한 서버라면 적당히 높여도 문제 되지 않습니다.

다음과 같은 명령어로 적당량 증가 시킵니다.

$ sysctl -w net.ipv4.tcp_max_tw_buckets="1800000"

또 한가지 의문을 가져볼 수 있는데, 어떤 특정 foreign address와 foreign port를 가지는 소켓이 TIME_WAIT 상태에 있다고 할 때
똑같은 주소와 포트로 클라이언트에서 연결 요청이 오면 어떻게 될까요?

RFC 1122에서는 같은 주소와 주소를 사용하는 TIME_WAIT 상태의 소켓이 있더라도, SYN을 받으면 이 소켓을 재사용하도록 되어 있습니다.
때문에, 특별한 다른 설정이 없어도 문제 될 것이 없습니다.

정리하자면, 서버 소켓만 있는 서버에서는 TIME_WAIT 소켓이 많아지더라도 성능 및 수용량 측면에서는 문제가 없습니다.
이러한 유형의 서버인 경우, TIME_WAIT 소켓 수에 크게 민감할 필요가 없습니다.
다만, TCP 소켓은 되도록 gracefully shutdown 되어야 하므로 'net.ipv4.tcp_max_tw_buckets' 커널 파라미터를
적당히 상향할 수 있겠습니다.


5.3 TIME_WAIT socket reuse (TW_REUSE)

그런데, 대규모 서비스에서는 웹서버라고 할지라도 다른 서버에 질의하는 경우가 있습니다.
이 경우, 서버는 또 다른 서버의 클라이언트가 되는 것이지요.
이럴때는 TIME_WAIT 상태의 소켓 개수가 성능에 영향을 미칠 수 있습니다.

어떤 서버가 다른 서버로 질의하는데, 별도의 connection pool을 두지 않고 HTTP RESTful API로 질의한다고 가정합시다.
그리고 이때 HTTP keepalive를 사용하지 않는다고 가정합시다.
가정한다고 했지만, 굉장히 일반적으로 사용되는 형태일 것 입니다.
일반적으로 TCP keepalive의 경우 L4 스위치에 의해 무용지물이 되기에 대개는 사용하지 않기 때문입니다.

이 때 TIME_WAIT 상태의 소켓 수가 local port를 선점하여 생성할 수 있는 클라이언트 소켓 개수를 제한시키며,
이는 다시 말해 다른 서버에 동시 질의할 수 있는 연결 수를 줄입니다. 최악의 경우 다른 서버로 1분내 질의 수가 가질 수 있는
ephemeral port 수보다 많으면, HTTP 연결 시점에 요청은 실패할 것입니다.

그럼 이러한 경우 어떻게 해야 할까요?
그리고 이미 필요 없는 소켓을 닫았을 뿐인데, 이 때문에 새로운 소켓을 만들지 못하는 것이 합당할까요?
이를 해소하기 위한 방법으로는 크게 네가지 정도가 있을 것 입니다.


(1) 애플리케이션에서 connection pool을 사용

     : 성능과 유연성면에서 가장 추천하는 방법이지만, 애플리케이션을 고쳐야 합니다.

(2) TW_REUSE 옵션을 사용
     : 사용할 수 있는 local port 수가 모자라면, 현재 TIME_WAIT 상태의 소켓 중 프로토콜상 사용해도 무방해 보이는 소켓을 재사용합니다.

(3) TW_RECYCLE 옵션을 사용
     : TIME_WAIT 상태에 머무르는 시간을 변경하여 TIME_WAIT 상태의 소켓 수를 줄입니다.
       1분 대신 RTO(Retransmission Timeout) 시간만큼으> 로 TIME_WAIT 상태에 머무르는 시간이 경감되는데,
       리눅스에서는 200ms까지 이 시간이 줄 수 있습니다. (최소 RTO가 200ms)

(4) Socket linger 옵션을 굉장히 짧은 시간을 매개변수로 주어 사용
     : FIN 대신 RST를 보내게 유도하며, 이 때 소켓을 파괴하여 소켓이 TIME_WAIT 상태에 머무르지 않게 합니다.


TW_RECYCLE, socket linger 옵션은 TW_REUSE 보다는 상대적으로 과격한 방법이며 되도록 권장하지 않습니다.
관련된 내용은 이후 별도의 절에서 서술하겠습니다.

TW_REUSE를 사용하려면 'net.ipv4.tcp_tw_reuse' 커널 파라미터를 설정해야 합니다.
그런데, 위에서 소개한 대로 TW_REUSE는 특정 상황에서만 TIME_WAIT 상태의 소켓을 재사용하게 되는데요.
이 판단 로직에서는 TCP timestamp라는 확장 옵션을 사용합니다. 때문에,
TW_REUSE 옵션을 활성화 하려면 먼저 TCP timestamp 옵션도 활성화 되어야 합니다.

TCP timestamp가 사용되면 TIME_WAIT 상태의 소켓에 통신이 이루어진 마지막 시간(timestamp)를 기록할 수 있습니다.
TW_REUSE 옵션을 활성화 한 상태에서 클라이언트 소켓 생성시, TIME_WAIT 상태의 소켓 중 현재 timestamp 보다 확실히 작은 값의
timestamp를 가지는 소켓은 재사용(reuse) 할 수 있습니다.

일반적으로 리눅스에서 timestamp의 단위는 초 이므로, TIME_WAIT 상태로 전이된지 1초 이후의 소켓들은 재사용 할 수 있게 됩니다.

다음과 같은 명령어로 TW_REUSE 옵션을 활성화 할 수 있습니다.

$ sysctl -w ipv4.tcp_timestamps="1"
$ sysctl -w net.ipv4.tcp_tw_reuse="1"

한가지 유의할 점은, TW_REUSE 옵션은 통신을 하는 양측 모두 TCP timestamp 옵션이 설정되어 있어야 활성화된다는 것 입니다.
한쪽에 TCP timestamp 옵션이 활성화 되지 않은 경우, TIME_WAIT 상태의 소켓을 재사용 할 수 없습니다.


5.4 TCP timestamp

TCP에서는 sequence number로 패킷의 순서(ordering)을 판별합니다. 이 값은 32비트 unsigned int 형으로 0에서 약 40억까지의 표현범위를
가지고, 매우 빠른 네트워크 환경이 있다고 가정해봅시다. 그리고 불행히도 receiver는 이 네트워크보다는 조금 느린 처리 속도를 가지고 있다고 가정해 봅시다. 이런 상황에서 sequence number는 중첩(wrapping) 될 수 있습니다.

예를 들자면, 어느 순간 receiver는 어느 sender로부터 100이라는 sequence number를 가진 패킷을 받았다고 합시다.
이후 receiver는 100 이상의 sequence number를 가지는 패킷들을 기대합니다.

이후 receiver는 순식간에 40억개 이상의 패킷이 수신 되었다고 합시다. 그런데 아직 receiver는 이 패킷들을 까보지 않았습니다.
NIC, 즉 네트워크 어댑터에 의해 수신되었으나 아직 커널의 TCP stack에서는 이 패킷을 열어보지 않은 상태입니다.
(10G의 네트워크라면 불가능한 일이 아닙니다.)

이 때 마지막쯤 수신된 패킷의 sequence number는 unsigned int 형의 표현 범위 때문에 필시 overflow 되었을 것입니다.
편의상 TCP stack이 까봐야할 패킷의 sequence number가 101~4,294,967,296까지, 그리고 이후 overflow되어서
0~200 이라고 가정합시다.

receiver는 100 이상의 sequence number를 기대하고 있었는데, 이보다 작은 수인 0~100의 sequence number를 받은 상황이 됩니다.
즉 순서(ordering)에 위배되므로 이 패킷들은 새로 수신된 패킷임에도 불구하고 조용히 버려집니다. (silently dropped)

또, 이 경우 중복된 sequence number의 패킷도 존재하게 되는데요. 예컨대 sequence number가 101인 패킷이 두 개 존재합니다.
네트워크 상 패킷은 reordering될 수 있음을 감안한다면, 이 두 개의 패킷의 선후 관계를 확정 지을 수 있을까요?

당연히 확정할 수 없습니다. TCP stack의 구현에 따라 다르겠지만, 둘 중 어느 한 패킷은 버려질 것입니다.

이러한 문제, 즉 wrapped sequence number 문제를 해결하기 위해, RFC 1323에서는 PAWS(Protection against Wrapped Sequence Numbers)라는 방법을 제안합니다. 이는 reordering을 판별하는 요소에 sequence number 뿐만 아니라 timestamp 값을 사용하겠다는 것입니다.
PAWS가 구현된 TCP stack은 timestamp가 포함된 어느 패킷을 받았을 때 이 timestamp를 기록해두며 connection 별로 관리합니다.

이름에서 유추할 수 있듯이 timestamp는 단방향으로 증가되는 어느 값입니다.
일반적으로 timestamp는 밀리초(millisecond) 단위로 system clock에서 가져다 쓰는 경우가 많습니다.

여기서 한가지 생각해 볼만한 것이 있는데요.
그럼 sender와 receiver간 통신에서 이 timestamp 값이 동기화(synchronization)되어 있어야 할까요?

결론부터 이야기 하자면 그럴 필요가 없습니다. 사실 timestamp는 단방향으로 증가하는 어느 값이면 되며
system clock과 연동될 필요가 없습니다. 논리적으로는 sequence number의 high order로서 사용되기 때문입니다.

timestamp를 둔다면, 위와 같은 예제(단시간에 sequence number가 overflow되는 상황)에서 새로 수신한 패킷을 버리는 문제를
해결 할 수 있습니다. sequence number가 중첩(wrap)되었더라도 timestamp는 더 높은 값이기에 새로 수신된 패킷이라고 인지할 수 있습니다.
(위에서 언급한대로 sequence number의 high order라 생각하면 쉽게 이해될 것 같습니다.)

리눅스에서 TCP timestamp를 사용하기 위해서는 'net.ipv4.tcp_timestamps'라는 커널 파라미터를 1로 설정해야 합니다.
현재 설정값을 확인하려면 다음과 같은 명령어를 사용할 수 있습니다.

$ sysctl net.ipv4.tcp_timestamps

최신의 일반적인 리눅스 배포판에서는 기본값으로 활성화되어 있습니다.
참고로, 올바르게 동작하려면 sender/receiver 모두 활성화 상태이어야 합니다.


5.5 TIME_WAIT socket recycling (TW_RECYCLE)

TW_REUSE 옵션보다 과격한 방법은 TW_RECYCLE 옵션을 사용하는 것입니다.
TW_RECYCLE은 소켓이 TIME_WAIT 상태에 머무르는 시간을 RTO 만큼 재정의하게 되는데요.
RTO 시간은 RTT에 영향을 받으며, 일반적으로 1분 보다는 짧습니다.

다음과 같은 명령어로 TW_RECYCLE 옵션을 활성화 할 수 있습니다.

$ sysctl -w ipv4.tcp_timestamps="1"
$ sysctl -w net.ipv4.tcp_tw_reuse="1"
$ sysctl -w net.ipv4.tcp_tw_recycle="1"

TW_RECYCLE 옵션을 활성화하려면, 먼저 TCP timestamp와 TW_REUSE를 활성화시켜야 하는 것에 유의해야 합니다.

그런데, TW_RECYCLE은 크게 권장하지 않습니다.
클라이언트가 NAT 환경인 경우 일부 클라이언트로 부터의 SYN 패킷이 유실될 수 있기 때문입니다.
즉, TW_RECYCLE 사용시 일부 TCP 연결 요청은 실패하게 됩니다.

왜 이런 현상이 발생할까요?

TW_RECYCLE 사용시, TIME_WAIT 상태로 대기하는 시간이 극히 짧아져 TIME_WAIT에 남아 있는 소켓이 거의 없다고 해도 될 것 입니다.
(물론 peer가 timestamp를 사용하지 않는다면 남아있습니다.) 그렇지만, 커널은 되도록 TIME_WAIT 상태의 소켓이 있는 것처럼
행동해야 합니다. 소켓의 gracefully shutdown을 보장하기 위해서죠.

예를 들어, 어떤 소켓이 있고 active closing 되었다고 합시다. TIME_WAIT 상태에서 매우 짧은 시간 머무른 후 삭제될 것 입니다.
이윽고 같은 foreign address / port로 부터 연결 요청이 와서 새로운 연결이 맺어졌습니다.

이때 sequence number가 역전된 패킷이 도착했다고 합시다.
그런데, 이 패킷이 이전에 연결이 끊어진 소켓에 대한 패킷인지, 지금 연결이 맺어진 소켓에 대한 패킷인지 알 수 있을까요?
이런 경우에 어떻게 해야 할까요?

이러한 문제점을 해결하기 위해 TW_RECYCLE 옵션을 사용하면 TIME_WAIT 상태의 소켓이 있는 것처럼 몇가지 모사를 하게 됩니다.

이러한 로직을 간단히 서술하면 아래와 같습니다.

(1) TIME_WAIT 상태로 들어가면, 다른 구조체에 현재 소켓의 foreign address와 timestamp 값을 기록합니다.

(2) 해당 foreign address/port의 패킷이 오면, (1) 과정에서 알고 있는 timestamp와 대조하여
    이보다 작은 timestamp라면 조용히 버려버립니다.

(3) 이 구조체에 저장된 값은 원래 TIME_WAIT 상태로 체류하는 시간 만큼(즉 1분) 유지됩니다.

즉, timestamp가 역전된 패킷은 버립니다.

그런데, 위에서 설명한대로 timestamp는 peer 별로 동기화되어 있지 않습니다. NAT를 사용하는 클라이언트들은 서버 입장에서는
모두 같은 주소를 사용하는 것처럼 보일 것입니다. 그런데, 보내는 패킷의 timestamp는 각 클라이언트 별로 다른 값을 가질 수 있습니다.
때문에 기존 소켓이 마지막으로 기록한 패킷의 timestamp 값이 새로이 소켓 연결을 요청하는 SYN 패킷에 포함된 timestamp 보다
클 수 있습니다. 그리고 이 경우, 위에서 서술한대로 조용히 버려지게 될 것 입니다.

특히, front-end 서버에서는 TW_RECYCLE 옵션을 (절대) 사용하면 안되는데, 이 서버는 주로 인터넷에서 수신되는 클라이언트의 요청을
받게될 것이기 때문입니다.

인터넷의 클라이언트들은 NAT 환경일 확률이 큽니다. 3G/LTE 등의 모바일 네트워크 환경은 일종의 거대한 NAT 환경이고요. 
중국과 같이 IP가 모자라는 환경인 경우 NAT를 많이 사용 할 것 입니다. 이런 경우, 많은 수의 클라이언트에서 서버로 접속 못하는 장애가
발생할 수 있습니다.


5.6 Socket linger option

TIME_WAIT 상태의 소켓을 줄이는 또 하나의 "극단적" 방법은, 극히 짧은 시간(극단적으로 0초)으로 linger 옵션을 활성화하는 것입니다.
소켓당 설정할 수 있는 옵션인데, 기본 설정은 비활성화입니다.

원래 TCP 소켓은 close() 호출하는 거의 즉시 반환됩니다. 그리고 소켓을 닫기 위한 실제 동작(아직 소켓 버퍼에 남아 있는 데이터를
보낸다던가, FIN/ACK를 보낸다던가)은 커널에 의하여 진행 되죠. 이 소켓은 결국 TIME_WAIT 상태가 되어 주어진 시간(리눅스에서는 1분)
대기하게 됩니다.

linger 옵션은 close()의 반환을 즉각적으로 하지 않고, 즉 non-blocking으로 반환되지 않고 blocking manner로 처리하겠다는 것을 의미합니다.
애플리케이션은 close()시 linger 옵션 활성화시 주어진 매개변수 시간까지 block 됩니다. 이 시간 동안 커널은 소켓 버퍼에 남아 있는 데이터들을 내보내려 노력합니다. 그리고 주어진 시간안에 이러한 처리가 완료되면, 즉 정상 처리되면 일반적인 방법과 똑같이 커널은 FIN을 보내고 소켓도 결국 TIME_WAIT 상태가 되죠. 그러나 주어진 시간내 정상 처리되지 못하면, 커널은 FIN 대신 RST을 보내고 즉시 소켓은 파괴됩니다.

TIME_WAIT 상태의 소켓을 줄이는 극단적인 방법은, 이 linger 옵션에서의 block 될 수 있는 시간을 '0'으로 설정하는 것 입니다. 그러면, close()시 커널은 한순간도 기다리지 않고 FIN 대신 RST를 보내며 소켓을 파괴할 것입니다. 때문에 TIME_WAIT 상태의 소켓도 남아있지 않는 것이죠.

그러나, linger 옵션은 결코 TIME_WAIT 상태의 소켓을 제거하기 위한 옵션이 아닙니다. 데이터를 peer에게 완전히 전달(delivery)하기 위해
애플리케이션에 보다 통제권을 주는 하나의 옵션일 뿐이죠. 일반적인 처리에서 소켓은 커널에 의해 gracefully shutdown하는 것이 올바릅니다.

이렇게까지 하는 저변에는 TIME_WAIT 상태의 소켓을 해악으로 보기 때문인 것 같습니다.
그러나 패킷의 reordering이나 유실이 있을 수 있는 네트워크의 특성상, 소켓은 되도록 gracefully shutdown 되어야 합니다.

이번장에서 반복해서 말씀드리는 건, TIME_WAIT 상태의 소켓은 크나큰 해악이 아니라는 것입니다. 소켓이 정상 종료되는 과정인 것이지요.
서버 소켓에서는 일반적으로 아무런 문제가 없으며, 클라이언트 소켓이라도 TW_REUSE 옵션으로 극복이 가능합니다.

TW_REUSE 옵션이 사용 불가능한 극히 예외적인 상황(예를 들어, timestamp를 사용하지 않는 다수의 서버와 통신)에서라도 TW_RECYCLE,
linger 옵션 같은 극단적인 처치는 지양해야 할 것 입니다. 이러한 경우에 정말로 TIME_WAIT 상태의 소켓 개수가 성능의 병목점이라면,
keepalive를 사용하거나, connection pool 등을 구현하는 등 애플리케이션 자체를 고쳐야 할 것 입니다.



6. 결론

생각보다 꽤 긴 글이 되었는데, 적당히 요약하자면 다음과 같습니다.

  • TCP 대역폭을 증가시키려면 receive window size를 늘려야 한다.
    $ sysctl -w net.ipv4.tcp_window_scaling="1"
    $ sysctl -w net.core.rmem_default="253952"
    $ sysctl -w net.core.wmem_default="253952"
    $ sysctl -w net.core.rmem_max="16777216"
    $ sysctl -w net.core.wmem_max="16777216"
    $ sysctl -w net.ipv4.tcp_rmem="253952 253952 16777216"
    $ sysctl -w net.ipv4.tcp_wmem="253952 253952 16777216" 
  • 애플리케이션에서 알지 못하는 네트워크 패킷 유실을 방지하기 위해서는 in-bound queue 크기를 늘려야 한다.

    $ sysctl -w net.core.netdev_max_backlog="30000"
    $ sysctl -w net.core.somaxconn="1024"
    $ sysctl -w net.ipv4.tcp_max_syn_backlog="1024"
    $ ulimit -SHn 65535
    `
    
  • TIME_WAIT 상태의 소켓은 일반적으로 서버 소켓의 경우 신경쓸 필요 없지만, 다른 서버로 다시 질의하는 경우
    (예를 들어 프록시 서버) 성능을 제약할 수 있으며 이 상황에서 TW_REUSE 옵션은 고려할 만하다.
     
    (TW_RECYCLE 옵션은 NAT 환경에서 문제가 있고, socket linger 옵션은 데이터 유실이 있을 수 있으니 사용하지 말자.)

    $ sysctl -w net.ipv4.tcp_max_tw_buckets="1800000"
    $ sysctl -w ipv4.tcp_timestamps="1"
    $ sysctl -w net.ipv4.tcp_tw_reuse="1"
    
    다시금 말하자면, 모든 워크 로드를 만족하는 설정값은 있을 수 없습니다.
    위 파라미터 값들은 본문에서 예시를 든 값을 그대로 옮겨온 것에 불과합니다.
    보다 정밀히 튜닝하기 위해서는 서버 애플리케이션 특성(워크로드)에 대해 보다 자세히 알아둘 필요가 있습니다.



7. 맺으며...

글을 쓰다보면, 단편적인 지식이 문장이 되고 다시 그 문장들이 모여 하나의 장이 되는 것을 경험하게 되는데요.
그 와중에 미처 들춰보지 못했던 지식의 치부들을 곧잘 발견하게 됩니다.
충분히 이해했다고 생각했던 부분이 사실은 전부 이해하지 못했으며, 옳다고 생각했던 부분이 사실은 그르다는 것을 발견하는 것이지요.

특히 TCP 소켓의 timewait과 관련된 부분은 굉장히 빈번한 오해를 사는 일이 많아서 한번쯤 정리하고 싶었는데요.
국내에도 저와 비슷한 생각을 가진 분들이 몇 있어, 올해(2015년)에 잘 작성된 글을 2건이나 찾을 수 있었습니다.

http://docs.likejazz.com/time-wait/
https://brunch.co.kr/@alden/3

충분히 이해했다고 생각했던 부분에서 발견된, 사실은 이해한 게 없었던 지식의 치부들은 위 글들을 참고하여 어느 정도 가려진 것 같습니다.

긴 글 읽어주셔서 고맙습니다!



8. FAQ

Q1) 언급하신데로 최근 서버의 경우 cubic 방식을 많이 사용하는데요.
     이전에 많이 사용되던 reno 로 커널 컴파일 없이 변경해서 사용할 수 있나요?
     만약 가능하다면 몇가지 알고리즘을 제공하는지도 궁금합니다.

A1) 현재 즉시 사용 가능한 congestion control algorithm 목록은 다음과 같이 확인할 수 있습니다.

$ cat /proc/sys/net/ipv4/tcp_available_congestion_control 
(OR) $ sysctl net.ipv4.tcp_available_congestion_control

제가 사용하는 ubuntu 14.04.2 LTS에서는 기본적으로 cubic, reno가 탑재되어 있는데요.
현재 congestion control algorithm을 변경하려면 다음과 같이 하면 됩니다.

$ echo "reno" > /proc/sys/net/ipv4/tcp_congestion_control 
(OR) $ sysctl -w net.ipv4.tcp_congestion_control="reno"

tcp congestion control algorithm은 pluggable kernel module로 되어 있습니다.
위 목록 외 가용한 목록을 찾으려면 다음과 같은 명령어를 사용할 수 있습니다.

ls /lib/modules/`uname -r`/kernel/net/ipv4/

여기서 tcp_vegas.ko, tcp_westwood.ko와 같은 파일들이 TCP congestion control algorithm을 구현하는 커널 모듈인데요.
다음과 같이 별다른 조치 없이 이 목록에 있는 커널 모듈들을 사용할 수 있습니다.

echo "westwood" > /proc/sys/net/ipv4/tcp_congestion_control 
(OR) $ sysctl -w net.ipv4.tcp_congestion_control="westwood"

그리고 'sysctl net.ipv4.tcp_available_congestion_control' 명령을 사용하면 위에서 추가된 알고리즘이 이 목록에도 추가되어 있음을
알 수 있습니다.


References

http://www.tldp.org/HOWTO/Linux+IPv6-HOWTO/proc-sys-net-ipv4..html
https://developers.google.com/speed/articles/tcp_initcwnd_paper.pdf
http://tools.ietf.org/html/draft-ietf-tcpm-initcwnd-00
http://www.cdnplanet.com/blog/tune-tcp-initcwnd-for-optimum-performance/
http://d2.naver.com/helloworld/47667
http://packetbomb.com/understanding-throughput-and-tcp-windows/
http://stackoverflow.com/questions/8893888/dropping-of-connections-with-tcp-tw-recycle
http://blogs.technet.com/b/thenetworker/archive/2008/04/20/of-tcp-sequence-numbers-and-paws.aspx
http://unix.stackexchange.com/questions/210367/changing-the-tcp-rto-value-in-linux
http://vincent.bernat.im/en/blog/2014-tcp-time-wait-state-linux.html
http://veithen.github.io/2014/01/01/how-tcp-backlog-works-in-linux.html
https://www.frozentux.net/ipsysctl-tutorial/chunkyhtml/tcpvariables.html


작성자

  • 작성자 프로필 사진
    NHN엔터테인먼트 / P-Flat개발팀   정성환

    Mac OS X, Linux, Freebsd를 좋아하며, 각종 IT의 잡다한 지식들을 사랑하는 개발자입니다.
    다양한 프로그래밍 언어들을 스위스칼처럼 갈무리해두었다가, 필요할때 뽑아드는걸 취미로 하고 있습니다.