위험하다고 알려진 TIME_WAIT 소켓 알아보기
과거부터 많은 콘텐츠들에서는 TIME_WAIT 소켓의 개수를 최소화해야 하는 것이라고 알려져 있었다. 정말로 TIME_WAIT이 많이 존재하는 건 문제를 일으킬 수 있고, 우리는 이것을 줄이기 위해 많은 노력을 해야 하는 것일까?
먼저 TIME_WAIT 소켓에 대해서 알아보겠습니다. 항상 어딘가에서 들었던 질문인 TCP와 UDP의 차이는 무엇인가?라는 질문에서 TCP는 연결지향이다라는 말을 많이 들어보았을 것입니다. UDP와는 다르게 TCP는 서로 데이터를 주고받기 전에 연결을 하며 통신이 끝난 이후에는 연결을 끊습니다. 이때 연결을 할 때는 3 Way Handshake를 하고 연결을 끊을 때는 4 Way Handshake를 수행합니다. TIME_WAIT 소켓은 연결을 끊을 때 하는 4 Way Handshake 과정에서 발생합니다.
위 그림은 4 Way Handshake 과정을 나타낸 그림입니다. 우선 우리는 연결을 끊기를 요청하는 측, 즉 FIN을 먼저 보내는 측을 Active Closer, 그리고 그 반대쪽에 있는 끊기를 당하는 측을 Passive Closer라고 부릅니다. TIME_WAIT 상태의 소켓은 항상 Active Closer 측에서 발생합니다. 이 말은 서버와 클라이언트 어떤 시스템이든 TIME_WAIT 상태의 소켓을 가질 수 있다는 의미입니다.
4 Way Handshake 과정을 살펴보겠습니다. 간단하게 Active Closer는 A, Passtive Closer는 P로 표시해 보죠.
1. A가 P에게 연결을 끊을 것을 알리는 FIN을 보냅니다. (A는 FIN_WAIT_1, P는 CLOSE_WAIT)
2. P가 A에게 FIN을 받았다는 것을 알리는 ACK를 보냅니다. (P는 FIN_WAIT_2)
3. P가 A에게 자신도 연결을 끊겠다는 것을 알리는 FIN을 보냅니다. (P는 TIME_WAIT, A는 LAST_WAIT)
4. A가 P에게 FIN을 받았다는 것을 알리는 ACK를 보냅니다.
보다시피 P의 소켓은 FIN을 A에게 보낼 때 TIME_WAIT 상태로 전환됩니다. 그리고 TIME_WAIT 상태에서 리눅스 커널에서 정의된 TCP_TIMEWAIT_LEN 상수에 의해서 60초간 TIME_WAIT 상태로 대기한 뒤 소켓을 정리하게 됩니다.
즉, TIME_WAIT 상태의 소켓은 연결을 끊을 때마다 추가되게 됩니다. 일반적인 문헌에서는 이런 TIME_WAIT 소켓이 많아질 경우 대표적으로 두 가지 문제가 발생할 수 있다고 합니다.
TIME_WAIT을 많이 발생시키면 로컬 포트가 부족해질 수 있다.
TCP 소켓을 유지하려면 메모리가 필요하며, 메모리 부족이 일어날 수 있다.
우선 첫 번째인 "TIME_WAIT을 많이 발생시키면 로컬 포트가 부족해질 수 있다"는 충분히 문제가 될 수 있습니다. 이 문제는 클라이언트 측에서 발생할 수 있는 문제입니다. 어떤 의미인지 조금 더 자세히 설명해 보겠습니다.
이 문제는 클라이언트 측에서 발생할 수 있는 문제라고 말씀드렸습니다. 클라이언트는 TCP 요청을 하는 측을 의미합니다. 리눅스 시스템에서 TCP 요청을 할 때는 리눅스 커널에서 Outgoing 포트를 할당받고, (SRC_IP, SRC_PORT, DST_IP, DST_PORT)라는 네 개의 값을 가진 튜플을 생성하여 리눅스 커널에게 이 값들을 가지고 TCP 소켓을 생성해 달라는 요청을 합니다.
하지만 일반적으로 리눅스 시스템에서는 이 네 값의 조합을 가진 소켓은 항상 고유합니다. 즉, 동일한 값을 가진 다른 소켓이 생성될 수 없다는 의미입니다. 방금 커널에서 Outgoing 포트를 할당받는다고 하였는데, 이 포트가 SRC_PORT가 됩니다. 커널은 Outgoing 포트를 할당할 때 커널 파라미터로 정의된 포트 범위 내에서 사용하지 않고 있는 포트를 할당해 줍니다.
즉, 우리가 동일한 목적지로 한 시스템에서 지속적으로 요청을 보내는 경우 리눅스 커널이 할당해 주는 SRC_PORT가 계속 변경되며 고유한 여러 소켓을 만들게 됩니다. 하지만 이 경우를 생각해 봅시다. 지속적으로 클라이언트 측이 Active Closer 역할을 수행하면서 TIME_WAIT 소켓이 계속 발생되고, 그 사이에 TIME_WAIT 소켓이 최대로 할당할 수 있는 로컬 포트 개수에 도달한다면?
이렇게 되면 더 이상 커널은 Outgoing 포트를 생성할 수 없게 되고, 중복을 허용하지 않는 소켓은 더 이상 생성될 수 없을 것입니다. 이런 현상을 로컬 포트 부족이라고 부릅니다. 이것은 분명 문제가 될 수 있습니다. 리눅스 커널을 기본 옵션으로 사용하여 이 로컬 포트의 net.ipv4.ip_local_port_range 파라미터를 따로 지정하지 않았다면 아마도 32768-60999의 범위를 가질 것이고, 약 3만 개 정도의 로컬 포트를 만들 수 있다는 것을 의미합니다. 대규모 트래픽을 감당하는 서비스라면 60초 내에 분명 이 범위를 다 차지할 여지가 다분하기에 이는 분명 고려해야 할 문제로 다가올 수 있습니다.
하지만 이 문제는 tcp_tw_reuse 커널 파라미터를 활성화하여 생각보다 쉽게 해결할 수 있습니다. 이 파라미터를 활성화하면 로컬 포트가 부족할 시 커널은 사용하지 않을 만한 소켓을 재사용하게 됩니다. 조금 더 구체적으로 소켓들은 timestamp를 가지고 있고 현재 timestamp보다 작은 소켓을 우선적으로 재활용하게 됩니다. timestamp의 차이가 크면 클수록 지연된 세그먼트가 도착할 확률이 낮다는 의미여 TIME_WAIT 상태를 해제하더라도 문제가 발생할 여지가 적어진다는 것을 의미합니다.
클라이언트 측에서만 발생하는 로컬 포트는 분명 문제가 될 수 있습니다. 하지만 커널 파라미터를 수정하는 것으로 완벽하지는 않으나 생각보다 쉽게 문제를 해결할 수 있습니다. 서버 측의 경우 어떤 문제가 발생할 수 있을까요? 서버는 특정 포트를 고정해 둔 소켓으로 소켓을 생성하게 되고, 요청을 받는 클라이언트의 IP와 포트가 변경되며 요청이 들어오니 서버 입장에서는 로컬 포트를 생성하지 않습니다. 하지만 서버 측이 계속 Active Closer가 되면 TIME_WAIT이 많아지고, 이 많아진 TIME_WAIT은 실제로 메모리상에 존재하는 소켓으로 유지가 되고 있으니 메모리 부족 이슈가 발생할 수 있다고 여러 문헌에서 이야기하고 있습니다.
이 글에서 이야기하고 싶은 부분이 이 영역입니다. 정말로 TIME_WAIT이 많아지면 성능상에 영향을 끼치게 되고, 우리는 이런 영향을 줄이기 위해서 서버 측에서 TIME_WAIT 소켓을 최소화해야 할까요? 결론부터 말씀드리자면 영향을 끼치겠지만 우리가 크게 신경 쓸 필요는 없다고 봅니다. 현재 리눅스 커널에서 구현된 TIME_WAIT 소켓 구조체의 정의는 다음과 같습니다.
상황에 따라 크기가 다르지만 소켓 하나당 150~300 Byte 사이의 메모리 점유를 하게 됩니다. 물론 소켓 하나당 구조체 하나의 크기만을 점유하지는 않습니다. 소켓을 관리하는 커넥션 해시 테이블 같은 시스템 운영을 위해서 추가적으로 할당되는 메모리들이 있을 수 있습니다. 이런 것들을 감안하더라도 소켓 하나가 차지하는 메모리는 500Byte 언더입니다. 그러면 커널 파라미터를 수정하지 않았다는 것을 가정하면 약 30,000개의 소켓이 최대로 존재할 수 있을 거고, 이 모든 소켓이 TIME_WAIT 상태라고 가정하더라도 기껏해야 몇 메가바이트 밖에 차지하지 않습니다. 이 크기는 절대로 시스템이 영향을 끼칠 만큼 크리티컬 한 수치가 아닙니다. 오히려 TIME_WAIT을 줄이는 방법이라고 알려져고 리눅스 커널 4.12부터 제거되어 어차피 사용도 할 수 없는 tcp_tw_recycle 파라미터를 사용하더라도 연결에 문제가 생기는 부작용만 얻을 수 있습니다.
TIME_WAIT 상태의 소켓으로 인해 클라이언트 측에서는 로컬 포트 부족으로 인한 타임아웃이 발생할 여지는 충분히 있지만 tcp_tw_reuse 파라미터를 통해서 생각보다 쉽게 해결할 수 있으며 여러 문헌에서 말하는 메모리 과부하 이슈는 현실과는 거리가 멀고 사실상 기우에 가깝습니다.
TIME_WAIT 상태의 소켓은 분명히 필요합니다. 그리고 분명히 문제를 일으킬 수 있는 여지도 있습니다. 하지만 시스템을 설계하는 단계에서 많은 트래픽이 인입될 때 발생하는 문제 중 TIME_WAIT을 우선 고려하고 이를 해결하기 위해 처음부터 많은 시간을 소비하는 건 큰 도움이 되지 않는 기우에 불과할 수 있습니다.