[번역] 바쁜 리눅스 서버에서 TCP TIME-WAIT 상태 대처하기.

이 문서는 원작자의 동의를 얻어 다음이 포스트를 발 번역한 것 입니다.

그리고 이 글의 라이센스는 “CC BY-NC-SA 3.0” 입니다. 글 아래의 라이센스는 이 글에서는 해당되지 않음을 알려 드립니다. 다시 한번 원작자 Vincent Bernat 씨에게 감사 합니다.

net.ipv4.tcp_tw_recycle 를 활성화하지 말라!

리눅스 커널 문서는 net.ipv4.tcp_tw_recycle 이 무엇인지에 대해서 별다른 도움이 안된다.

TIME-WAIT 소켓의 빠른 재사용을 활성화. 기본값은 0이다. 기술적인 전문가의 충고나 요구없이 함부로 바꿔서는 안된다.

이와 비슷한, net.ipv4.tcp_tw_reuse 는 좀더 문서화되었지만 하는 말은 똑같다.

프로토콜 입장에서 안전한때에 새로운 접속에 대해 TIME-WAIT 소켓의 재사용을 허용하라. 기본값은 0 이다.기술적인 전문가의 충고나 요구없이 함부로 바꿔서는 안된다.

이렇게 부족한 문서로 인해서 우리는 TIME-WAIT 숫자를 줄이기위해서 양쪽다 모두 1로 세팅하라고 충고하는 수많은 튜닝 가이드를 찾게된다. 그러나 tcp(7) 메뉴얼 페이지에 의해하면, net.ipv4.tcp_tw_recycle 옵션은 같은 NAT 장치 뒤에 있는 서로 다른 컴퓨터들로부터 접속을 핸들링할 수 없는것처럼, 이는 찾기도 힘들고 그것이 당신을 집어삼킬거다, 대중적인 서버들에게 아주 많은 문제가 있다.

TIME-WAIT 소켓의 빠른 재사용의 활성하기. 이 옵션의 활성화는 NAT(Network Address Translation) 을 가지고 동작할때에 문제를 발생시킬수 있어서 권장하지 않는다.

나는 인터넷에 잘못된 정보를 주는 사람들을 위해서 좀 더 상세한 설명을 여기서 제공할 것이다.

참고로, 이름에 ipv4를 사용함에도, net.ipv4.tcp_tw_recycle 은 IPv6 에함께 적용되어 제어된다, 우리는 리눅스의 TCP 스택에서 찾고 있다는 것을 유념해야 한다. This is completely unrelated to Netfilter connection tracking which may be tweaked in other ways

TIME_WAIT 상태에 대해.

TIME-WAIT 상태를 찾아보고 기억해보자. 이것이 무엇일까? 아래의 TCP 상태 다이어그램을 보라.

TCP State Diagram
TCP State Diagram

오직 처음으로 접속을 차단하는 끝부분에서 TIME-WAIT 상태에 도달한다. 다른 끝단은 빠르게 접속을 태우는것을 허용되는 경로를 따를것이다.

여러분은 ss -tan 를통해서 현재 접속 상태를 살펴볼 수 있다.

목적 TIME-WAIT 상태에 대한 목적은 두가지가 있다.

  • 아주 잘 알고 있듯이 지연된 세그먼트(Delayed Segments)를 같은 네쌍둥이(소스 주소, 소스 포트, 목적지 주소, 목적지 포트) 에 의존하는 뒤늦은 접속을 받아들일려고 하는 접속을 차단하는 것이다. 순차적인 번호(sequence number) 는 또한 접속을 받을 수 있는 어떤 구간에 있어야 한다. 이것은 아주 작은 문제를 야기하지만 여전히 특히 아주 큰 윈도우즈를 받는 빠른 접속에서는 존재하게 된다. RFC 1337 은 TIME-WAIT 상태가 부족해지면 무슨일이 벌어지는지를 상세히 설명하고 있다. Here is an example of what could be avoided if the TIME-WAIT state wasn’t shortened
duplicate-segment
Due to a shortened TIME-WAIT state, a delayed TCP segment has been accepted in an unrelated connection.
  •  또 다른 목적은 원격 끝에 접속을 닫을 수 있도록 한다. 마지막 ACK 를 잃었을때, 원격 끝은 LAST-ACK 상태에 머문다. TIME-WAIT 이 없다면 원격 끝이 여전히 이전 접속이 유효하다고 생각하는데도 연결은 재개될 수 있다. SYN 세그먼트를 받았을때에 이것은 세그먼트와 같은 것을 기대하지 않는 것처럼 RST 를 응답하게된다. 새로운 연결은 에러와 함께 중단된다.
last-ack
If the remote end stays in LAST-ACK state because the last ACK was lost, opening a new connection with the same quadruplet will not work.

RFC 793 은 적어도 2MSL 시간에 TIME-WAIT 상태가 필요하다. 리눅스에서 이 기간은 튜닝할수 없으며 include/net/tcp.h 에 정의된 것처럼 1분이다.

이 값을 튜닝하도록 하는 제안들이 있었지만, 실제 현장에서는 TIME-WAIT이 좋은 것이기에 거절되어져 왔다.

문제점

이제, 이 상태가 많은 접속을 핸들링하는 서버에서 왜 성가신 존재일수 있는지를 보자. 세가지 측면에서 문제점이 있다.

  • 슬롯(slot)은 같은 종류의 새로운 연결을 차단하기 위해서 연결테이블에 가져다 놓는다.
  • 메모리(Memory)는 커널의 소켓 구조(socket structure) 의 의해서 소모되며,
  • 이에 더해서 CPU도 사용된다.

‘ss -tan state time-wait | wc -l’ 의 결과는 문제가 아니다.

Connection table slot

TIME-WAIT 상태에 접속은 접속 테이블에서 1분동안 유지된다. 이것은 같은 네 쌍둥이(소스 주소, 소스 포트, 목적지 주소, 목적지 포트)에 다른 연결은 존재할 수 없다는 것을 뜻한다.

웹서를 예를들면, 목적지 주소와 목적지 포트는 상수와 같다.(변하지 않는 값이라는 뜻) 만약 웹 서버가 L7 로드 밸런서 뒤에 있다면 소스 주소 또한 상수 일수 있다. 리눅스에서 클라이언트 포트는 기본적으로 30,000 개의 포트 범위에서 할당된다. (이것은 net.ipv4.ip_local_port_range 를 튜닝함으로써 바꿀 수 있다) 이것은 오직 웹 서버와 로드 밸런서 사이에 연결이 매분당 30,000 개, 약 초당 500 개 연결이 맺어질수 있다는 것을 의미한다.

만약 TIME-WAIT 소켓이 클라이언트측에 존재한다면 이러한 상황은(30,000개 포트를 모두 할당한 나머지 더 이상 접속을 받을 수 없는 상황) 쉽게 찾을 수 있다. connect() 시스템 콜을 호출하면 EADDRNOTAVAIL 을 리턴하고 애플리케이션은 그것에 대한 어떤 에러메시지를 기록할 것이다. 서버측에서 이것은 로그도 없고 계산할 수 있는 카운터도 없어서 좀 더 복잡하다. 이상하게도, 당신은 사용되어지는 네 쌍둥이의 숫자 리스트에서 판단할 수 있는 무언가 오기를 노력해야 한다.

이에 대한 해결책은 좀 더 많은 네 쌍둥이들이다. 이것은 몇가지 방법으로 이룰 수 있다. (설정하는데 어려운 순서대로)

  • net.ipv4.ip_local_port_range 를 좀 더 넓게 세팅함으로써 좀 더 많은 클라이언트 포트를 사용.
  • 리스닝 하기위한 웹서버에게 몇개의 추가적인 포트(81,82,83,…)를 할당함으로써 좀 더 많은 서버 포트를 사용.
  • 로드 밸런서에 아이피를 추가하거나 라운드 로빈 기능으로 그들을 사용하게 함으로써 좀 더 많은 클라이언트 아이피를 사용.
  • 웹 서버에 추가적인 아이피를 추가함으로써 좀 더 많은 서버 아이피 사용.

물론, 마지막 해결책은 net.ipv4.tcp_tw_reuse 나 net.ipv4.tcp_tw_recycle 를 트윅하는 거다. 아직 하지는 말자. 우리는 뒤에서 그것들의 세팅에 대해서 이야기할 할 거다.

메모리

핸들링을 위해서 많은 접속을 가지는 경우에, leaving a socket open for one additional minute may cost your server some memory. 예를들어, 만약 당신이 약 초당 10,000 개의 새로운 접속을 핸들링하길 원한다면 당신은 TIME-WAIT 상태에 약 600,000 소켓을 가지게 될 것이다. 그러면 얼마나 많은 메모리를 소모할까? 그렇게 많지 않다.

첫째로, 애플리케이션 입장에서, TIME-WAIT 소켓은 그 어떤 메모리도 소모하지 않는다. 소켓은 닫혔다. 커널에서 TIME-WAIT 소켓은 세개의 다른 목적에 대해 세개의 구조로 제공된다.

  1. 다른 상태의 연결이 포함되어 있음에도 “TCP established hash table” 로 불리우는 접속 해쉬 테이블(A hash table of connection) 은, 예를들어 새로은 세그먼트를 받아들일때에 존재하는 연결을 위치시키는 장소 사용되어진다.이 해쉬테이블의 각각의 버킷(bucket)은 TIME-WAIT 상태에서 접속 리스트와 정규적인 활성 접속 리스트가 포함 된다. 이 해쉬테이블의 크기는 시스템 메모리에 의존적이며 부팅시에 볼수 있다.

    이것은 thash_entries 파라메터를 가지는 커널 명령어 라인에 숫자 엔트리를 지정함으로써 값을 재지정할 수 있다. TIME-WAIT 상태에서 접속 리스트의 각 요소들은 ‘struct tcp_timewait_sock’ 이며, 다른 상태들을 위한 타입은 ‘struct tcp_sock’ 이다.
  2. 접속 리스트의 집합은, “death row” 로 불리우는, TIME-WAIT 상태에서 접속 만료(expire)를 위해서 사용되어진다. 그들은 만료되기전에 얼마나 시간이 남았는지에 따라서 정렬되어진다. 접속 해쉬 테이블에 엔트리처럼 같은 메모리 공간을 사용한다. 이것은 ‘struct inet_timewait_sock’ 의 ‘struct hlist_node tw_death_node’ 멤버이다.
  3. 바운드 포트 해쉬 테이블은, 로컬 바운드 포트들과 연관된 파라메터들을 들고 있는, 주어진 포드가 리슨(Listen)하기위해 안전한지 혹은 다이나믹 바운드 케이스의 경우에 자유 포트를 찾을수 있는지를 결정하는데 사용되어진다. 이 해쉬 테이블의 크기는 접속 해쉬테이블의 크기와 똑같다.

    각 요소들은 ‘struct inet_bind_socket’ 이다. 그것은 각 로컬적인 바운드 포트당 하나의 요소다. 웹 서버에 TIME-WAIT 접속은 로컬적으로 80포트에 바운드 되고 TIME-WAIT 접속들의 형제들(sibling) 처럼 같은 엔트리들을 공유한다. 다시 말해서, 원격 서비스에대한 하나의 접속은 로컬적으로 어떤 랜덤 포트에 바운드 되고 그 엔트리는 공유되지 않는다.

그래서, 우리는 오직 ‘struct tcp_timewait_sock’ 과 ‘struct inet_bind_socket’ 에 의해서 소모되는 용량만을 생각하면 된다. 하나의 ‘struct tcp_timewait_sock’ 은 TIME-WAIT 상태에서 각 접속에대한, inbound or outbound, 것이다. 하나의 전용의 ‘struct inet_bind_socket’ 은 각 outbound 접속에 대한 것이며 inboud 접속에 대한 것은 없다.

하나의 ‘struct tcp_timewait_sock’ 은 168 bytes 인 반면에 ‘struct inet_bind_socket’ 은 48 bytes 이다.

그래서, 만약 약 40,000 개의 TIME-WAIT 상태의 inboud 접속을 가지고 있다면 10MB 보다 적은 메모리를 소모한다. 만약 dir 40,000개의 TIME-WAIT 상태의 outbound 접속을 가지고 있다면 2.5MB 추가적인 메모리를 위해 계산이 필요하다. ‘slabtop’의 출력을 보고 체크해보자. 여기에 TIME-WAIT 상태에서 약 50,000 개의 접속과 약 45,000개의 outbound 접속을 가지는 서버의 결과가 있다.

여기서 바뀐 것은 아무것도 없다. TIME-WAIT 접속으로 인해 사용되어지는 메모리는 정말 작다. 만약 당신의 서버가 초당 수천개의 새로운 접속을 처리해야 해야 한다면, 클라이언트에게 데이터를 효과적으로 전송하기 위한 좀 더 많은 메모리가 필요할 뿐이다. TIME-WAIT 접속 오버헤드는 무시해도 좋다.

CPU

CPU 측면에서 free local port 를 찾는 것은 조금 비싼작업일 수 있다. 이 작업은 락(lock)을 사용하고 free port 를 찾을때까지 로컬 바운드 포트들을 반복하는 ‘inet_csk_get_port’ 함수에 의해서 이루러진다. 이 해쉬 테이블의 아주 많은 엔트리들은 TIME-WAIT 상태의 outbound 연결을 가졌다면 일반적으로 문제가 되지 않는다. 접속들은 보통 같은 프로파일을 공유하고 함수들은 그들을 순차적으로 반복함으로써 아주 빠르게 free port 를 찾는다.

다른 해결법

만약 여전히 이전 섹션을 읽었는데도 TIME-WAIT 접속들이 문제가 있다라고 생각한다면 이 문제를 해결하기 위해 세 가지 추가적인 해결법이 있다.

  • socket lingering 비활성.
  • net.ipv4.tcp_tw_reuse.
  • net.ipv4.tcp_tw_recycle

Socket lingering

close() 가 호출되면, 커널 버퍼에 남아있는 데이터는 백그라운드로 보내질 것이고 소켓(Socket)은 최종적으로 TIME-WAIT 상태로 변한다. 애플리케이션은 즉각적으로 계속 일을 할 수 있고 모든 데이터는 안전하게 분배되도록 다루어진다.(주, 소켓이 빨리 닫힘에따라 애플리케이션은 소켓관련 작업을 끝내고 다른 일을 할 수 있다는 말)

그러나 애플리케이션은 이러한 행동을 하지못하도록 선택할 수 있는데, 그것이 바로 Socket lingering 이다. 거기에는 두가지 특징이 있다.

  1. 첫째로, 어떤 남아있는 데이터는 폐기처분되고 일반적인 4번의 패킷 접속 차단 시퀀스를 갖는 차단 접속 대신에 연결은 RST 를 가지고 닫힐 것이며 즉각적으로 파괴될 것이다.
  2. 두번째로, 여전히 소켓의 보내는 버퍼에 데이터가 남아있다면 프로세스는 close() 가 호출될때에 모든 데이터 보내지고 접속자로부터 응답을 수신받거나 설정된 링거 타임머가 만료될때까지 sleep 될 것이다. 이것은 소켓이 non-blocking 세팅되었을때에 프로세스가 sleep 되지 않을 가능성이 있다. 이것은 설정된 타임아웃 동안 보내어질 수 있는 데이터가 남아있는것을 허용하지만 만약 데이터가 성공적으로 보내지면, 일반적인 차단 시퀀스는 실행되고 TIME-WAIT 상태를 얻게된다. 그리고 다른 게이스로, RST 를 가지는 접속 차단을 얻을 것이고 남은 데이터는 폐기되어진다.

양쪽의 경우에, 비활성 Socket lingering 은 one-size-fits-all 해결방법이 아니다. 이것은 상위 프로토콜 지점으로부터 사용하는 것이 안전한 HAProxy 나 Nginx 처럼 몇몇 애플리케이션에서 사용되어질 수 있다. (주, Nginx 의 경우 lingering 관련 옵션들이 존재한다.) There are good reasons to not disable it unconditionnaly

net.ipv4.tcp_tw_reuse

TIME-WAIT 은 별 상관없는 접속을 받는 지연된 세그먼트를 차단한다. 그러나, 어떤 조건에서, 새로운 접속의 세그먼트가 오래된 접속의 세그먼로 잘못 해석할 수 없도록 해준다.

RFC 1323 은 고대역폭 경로를 넘는 성능향상을 위한 TCP 확장 셋을 제공한다. 다른 것들 사이에서, 이것은 두개의 4 바이트 타임 스탬프 필드를 전달하는 새로운 TCP 옵션을 정의한다.  하나는 TCP 보내기 옵션의 현재 타임스탬프 값이며 다른 하나는 원격 호스트로부터 아주 최근에 받은 타임스탬프 이다.

net.ipv4.tcp_tw_reuse 를 활성화 하면, 리눅스는 새롭게 외부로나가는 접속을 위해 새로운 타임스탬프가 이전 접속에서 기록된 최근의 타임스탬프보다 현격하게 크다면 TIME-WAIT 상태에서 존재하는 접속을 재사용한다: TIME-WAIT 상태에서 외부로나가는 접속은 정확히 1초 후에 재사용되어질 수 있다.

어떻게 이게 안전한가? TIME-WAIT 의 첫번째 목적은 똑같은 세그먼트가  아무런 연관이 없는 접속을 받는 것을 피하는 것이다. 타임스탬프 사용 덕분에, 이러한 똑같은 세그먼트들은 시간이 벗어난 타임스탬프로부터 오게되었고 결국 폐기된다.

두번째 목적은 원격 끝점이 마지막 ACK 를 잃었을때에 발생되는 LAST-ACK 상태가 안되게 해준다. 원격 끝점은 다음의 경우가 있을때까지 FIN 세그먼트를 재전송한다.

  1. 포기하고 접속이 해제될때까지
  2. 기다리던 중에 ACK 를 수신하고 접속이 해제될때까지
  3. RST 를 수신하고 접속이 해제될때까지

만약 FIN 세그먼트가 제때에 수신이 되면 로컬 소켓은 여전히 TIME-WAIT 상태에 있을 것이고 기대한 ACK 세그먼트를 보낼 것이다.

한번 새로운 접속이 TIME-WAIT 엔트리로 바뀌면, 새로운 접속의 SYN 세그먼트는 timestamps 덕분에 무시되어지고 RST 에 의한 응답은 없지만 FIN 세그먼트의 재전송에 의한 응답만 있다. FIN 세그먼트는 LAST-ACK 상태 중 전환을 허용할 RST 를 가진 응답이 있을 것이다.( 왜냐하면 로컬 접속은 SYN-SENT 상태이다) 응답이 없기 때문에 초기 SYN 세그먼트는 결국 (일초 후) 재전송되고 연결이 약간의 지연을 제외하고, 명백한 오류없이 설정됩니다.

If the remote end stays in LAST-ACK state because the last ACK was lost, the remote connection will be reset when the local end transition to the SYN-SENT state.
If the remote end stays in LAST-ACK state because the last ACK was lost, the remote connection will be reset when the local end transition to the SYN-SENT state.

접속이 재사용될때에 TWRecycled 카운트가 증가하는거에 주목해야 한다.

net.ipv4.tcp_tw_recycle

이 메커니즘은 타임스탬프 옵션에 의존하지만 서버는 일반적으로 먼저 연결을 종료할 때 incoming 과 outgoing 접속 모두에 영향을 미친다.

TIME-WAIT  상태는 곧 만료 될 예정이다: 이것은 RTT와 그 편차로부터 산출되는 재송신 타임아웃 기간 후에 제거되어진다. 여러분은 ss 명령어를 통해서 살아있는 접속에 대한 알맞은 값들을 발견 할 수 있다.

만료 타이머를 감소시키면서, 접속이 TIME-STATE 상태로 들어갈때, TIME-WAIT 상태가 제공하는 것과 동일한 보장을 유지하려면 맨 마지막 타임스탬프는 이전에 알고있는 목적지에 대해서 다양한 매트릭을 포함하는 전용구조에 기억되어 진다. 그리고, 리눅스는 TIME-WAIT 상태가 만료되지 않는 한, 최종적으로 기록된 타임스탬프보다 훨씬 큰 타임스탬프가 아닌 리모트 호스트로부터 모든 세그먼트를 드랍(Drop) 할 것이다.

리모트 호스트는 NAT 장치에 있을때, 타임스탬프 조건은 1분 동안 연결된 NAT 장치 뒤에 하나를 제외하고 모든 호스트는 금지되기 때문에 그들은 같은 타임스탬프 클럭을 공유할 수 없다. 이상하게도, 이것은 문제를 진단하고 감지하기 힘든다면 이 옵션을 비활성화 하는것이 훨씬 좋다.

LAST-ACK 상태는 net.ipv4.tcp_tw_recycle 와 같이 정확하게 같은 방법으로 다루어진다.

요약

보편적인 해결책은 더 많은 서버 포트들을 사용하는  네 쌍둥이 숫자를 증가시키는 것이다. 이것은 TIME-WAIT 엔트리들을 가진 가능한 접속들을 고갈되지 않도록 해준다.

서버측에서, NAT 장치들이 가지고 있지 않는지 확신이 서지 않는다면 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 활성화는 대부분 쓸모가 없다.

마지막으로 Unix Network Programming 을 쓴 W.Richard Stevens 의 말을 인용한다.

TIME-WAIT 상태는 우리의 친구이고 우리에 도움을 준다. (i.e, to let old duplicate segments expire in the network). 이 상태를 피하기 노력하기 보다는 그것을 이해해야 한다.

One comment

Post a comment

You may use the following HTML:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">