로컬 포트 부족과 TIME_WAIT

이번 글은 카프카와 관련된 주제가 아닌 시스템 엔지니어로서 제가 담당하는 서비스에서 실제로 트러블슈팅했던 내용을 공유하고자 합니다. 이 글 중간에 나오는 TIME_WAIT이나 tw_reuse, tw_recycle에 대한 이야기는 제가 가진 지식보다 정리를 잘 해주신 분들이 있기 때문에 자세히 다루지 않고, 경험한 내용을 바탕으로 필요한 부분만 간략하게 설명하겠습니다. 저와 같이 유사한 사례가 발생했을 때 빠른 원인 파악 후 간단하게 해결하였으면 하는 바람입니다.

현상

제가 담당하고 있는 서비스에서 기이한 현상에 대해 문의가 왔습니다. 먼저 해당 서비스 구조에 대해 간략하게 살펴보겠습니다.

그림1 서비스 구조 요약

클라이언트가 웹서버로 정보를 요청하면 웹서버는 API 서버로 정보를 가져와서 클라이언트에게 전달하는 구조입니다. 문제가 되고 있는 증상에 대해 요약하자면 다음과 같습니다.

  • 클라이언트는 웹서버로 요청을 보낸 다음, 지정된 시간 안에 응답이 오지 않으면 연결을 종료
  • 클라이언트가 연결을 종료하게 되면, 웹서버에는 특정 응답 코드가 남게 됨
  • 이러한 증상은 특정 시간에만 발생(ex 6시, 6시 30분, 7시, 7시 30분)

추적

웹서버를 거쳐, API 서버들과의 통신에 문제가 되는 것은 아닌지 파악을 해보기 위해 각 API 서버들의 상태를 확인해보기로 했습니다. 사실 위에서 간단하게 서비스 구조를 말씀드렸지만, 조금 자세히 보면 다음과 같습니다. 클라이언트로부터 하나의 요청을 받아서, 웹서버는 여러 대의 API 서버로 요청/응답을 받는 구조입니다. 원인 파악을 위해 각각의 API 서버들의 상태나 통신 오류 등의 문제를 확인하였지만, 원인을 찾기는 어려웠습니다. 또한 웹서버를 포함 API 서버들의 리소스 사용량의 문제는 없는지도 확인하였지만, 문제가 될만한 현상은 없었습니다. 결국 문제가 발생하는 시간의 패킷 상태를 확인하기 위해 웹서버의 tcpdump를 뜨게 되었습니다.

확인

해당 시간대의 웹서버 tcpdump를 확인해보니, TCP 3-Way Handshake가 무수히 많이 발생하고 있었고, TCP retransmission 등도 발생하고 있었습니다. 패킷의 상태를 보고 나서, 직감적으로 로컬 포트의 고갈 문제라는 생각이 들었고, 해당 시간대의 소켓 사용 상태를 확인하기 위해 sar를 이용하였습니다. sar 명령어는 다음과 같습니다.

sar -n SOCK

그림2 문제가 된 소켓 사용 상태

그림처림 시간대별로 소켓 사용량을 확인하니, 문제가 발생하는 7시 01분과 7시 31분에만 소켓이 3만 개 넘게 사용되고 있었고, 그 3만 개는 TIME_WAIT으로 사용되고 있었습니다. 리눅스의 기본값으로 로컬 포트는 약 3만 개 가량 사용이 가능하도록 설정되어 있고, 리눅스에서 현재 설정되어 있는 로컬 포트 범위를 확인하는 명령어는 다음과 같습니다.

cat /proc/sys/net/ipv4/ip_local_port_range

분석

왜 이렇게 로컬 포트를 많이 사용하게 되었는지 조금 자세히 보겠습니다. 앞선 그림의 클라이언트와 웹서버 구간은 클라이언트와 서버통신 구간임을 모두 아실 것 같고, 웹서버와 API 서버 구간을 살펴보겠습니다.

그림3 로컬 포트 사용 예제

클라이언트로부터 요청을 받은 웹서버(1.1.1.1)는 API 서버로부터 필요한 정보를 전달받기 위해  로컬 아이피+포트를 이용하여 서버 아이피+포트로 새로운 연결을 시도하게 됩니다. 여기에서 API의 TCP 포트는 443이라고 가정하고, 웹서버의 로컬 포트는 33000과 33001로 가정합니다.

그림처럼 웹서버의 로컬 아이피가 1.1.1.1을 가지고 있다면, 로컬 아이피 1.1.1.1와 사용 가능한 로컬 포트 33000을 이용하여 API-1 서버(2.2.2.2)의 443과 연결을 시도하게 됩니다. 즉 1.1.1.1:33000 -> 2.2.2.2:443 연결이 된 것입니다.

 API-1 서버(2.2.2.2)와 연결을 주고받는 상태에서 추가로 API-2 서버(3.3.3.3)의 443과 연결이 필요하다면, 웹서버는 사용 가능한 로컬 포트 33001을 이용하여 API-2 서버(3.3.3.3)의 443과 연결을 시도하게 됩니다. 즉 1.1.1.1:33001 -> 3.3.3.3:443 연결이 된 것입니다.

결국 하나의 클라이언트 요청으로 웹서버(1.1.1.1)는 2개의 로컬 포트(33000,33001)를 사용하게 됩니다. 만약 하나의 클라이언트 요청으로 API를 4곳에 요청/응답을 받아야 한다면, 총 4개의 로컬 포트를 사용하게 됩니다.

이제 그림 2의 소켓 사용에서 왜 TIME_WAIT이 가장 많이 로컬 포트를 사용하고 있는지 보겠습니다. TIME_WAIT은 TCP 연결의 최종단계이며, 패킷의 오작동을 막기 위해 2MSL(60초) 동안 유지되게 됩니다. 설정되어 있는 60초는 하드 코딩되어 있어 변경할 수 없습니다. 1) 참고자료 깃허브

하나의 TCP 연결이 맺어지고 데이터를 주고받은 후 연결이 완전하게 소멸되는데 최소 60초 이상이 소요됩니다. 따라서 TCP TIME_WAIT 상태로 60초 동안 연결을 유지하고 있어야 하기 때문에 로컬 포트를 많이 사용하게 됩니다. 2) 참고자료 TCP TIME_WAIT

로컬 포트도 3만 개로 제한이 있고, TIME_WAIT 시간도 60초 동안 유지해야 하는데, 웹서버 한대가 처리 가능한 요청수가 얼마나 되는지 궁금해집니다. 클라이언트가 웹서버(1.1.1.1)로 하나의 요청을 보냈을 때 최대 처리 가능한 요청수를 한번 계산해보겠습니다. 조건은 웹서버의 사용 가능한 로컬 포트는 3만 개이고, 하나의 클라이언트 요청에 2개의 로컬 포트를 사용하게 되고, TIME_WAIT으로 60초 동안 유지되는 것입니다. 계산 방법은 로컬 포트 수 % 2 % 60초 를 하게 되면 초당 최대 처리 가능한 요청 수인 250이 나옵니다.

30,000 % 2 % 60 = 250

만약 하나의 클라이언트 요청으로 웹서버가 요청/응답을 받아야 할 API 서버가 총 10개라면 어떻게 될까요?

30,000 % 10 % 60 = 50

웹서버의 최대 처리량은 250에서 50으로 떨어지게 됩니다. 웹서버의 CPU, MEM 등의 사용량 부족 때문이 아닌 단순히 로컬 포트의 제한 때문에 최대 처리량이 떨어지면 너무 억울하겠죠? 그래서 이러한 문제를 해결하기 위한 몇 가지 해결방법들이 있습니다.

해결방법

로컬 포트 늘리기

약 3만 개로 설정되어 있는 로컬 포트를 5만 개 이상으로 늘릴 수 있습니다. 명령어는 매우 간단합니다.

echo 1024 65535 > /proc/sys/net/ipv4/ip_local_port_range

위의 명령어는 리눅스의 1024 ~ 65535까지의 포트를 사용 가능한 로컬 포트로 변경하는 예제입니다. 하지만 이렇게 로컬 포트를 증가시킨다고 해서 2배가량 좋아질 뿐 여전히 부족하다고 생각됩니다.

TCP TIME_WAIT 재활용

리눅스의 tcp_tw_reuse와 tcp_tw_recycle를 이용하는 방법이고, 명령어는 아래와 같습니다.

echo 1 > /proc/sys/net/ipv4/tcp_tw_reuse

echo 1 > /proc/sys/net/ipv4/tcp_tw_recycle

reuse, recycle 두 가지를 간단하게 비교해보겠습니다. tcp_tw_reuse는 TIME_WAIT 상태 커넥션을 다시 사용하기 전에, 최근 연결에 사용한 timestamp보다 확실히 커야 포트 재활용 여부를 결정하는 한편, tcp_tw_recycle은 retransmission timeout(RTO)으로 TIME_WAIT 값을 가지고, 포트 재활용을 합니다.

tcp_tw_recycle은 매우 빠르게 정리되지만, NAT환경이나  load-balancers, 방화벽 같은 환경에서 너무 빨리 재활용되어 오히려 문제가 발생할 수도 있습니다. 해당 옵션의 튜닝도 TIME_WAIT를 조금 빠르게 재활용하는 옵션일 뿐이지 근본적인 원인 해결은 아니라고 생각합니다.  3) 참고자료 tw_reuse, tw_recycle 

Keep-Alive 또는 소켓 재사용

클라이언트와 서버가 연결을 맺은 뒤 해당 연결을 끊지 않고 지속한다면 매 요청마다 로컬 포트를 이용하여 새로운 연결을 맺지 않아도 되고, 연결 종료로 인하여 TIME_WAIT 상태에 빠지는 수도 매우 줄어들게 됩니다. 제가 트러블슈팅했던 부분도 바로 이 부분이었습니다. 클라이언트 -> 웹서버(1.1.1.1) -> API(2.2.2.2)으로 통신할 때, 웹서버 -> API구간 통신이 필요할 때마다 매번 새로운 연결을 하는 것이 아니라, 기존의 연결을 재활용하도록 하여 근본적인 원인 해결을 할 수 있었습니다. 다음은 그림은 기존의 연결을 재활용하도록 변경한 후의 TIME_WAIT 소켓의 상태 변화입니다.

그림4 소켓 재사용 적용 직후의 소켓 변화

만개가 넘어가던 TIME_WAIT 소켓이 약 10여 개 수준으로 변경되었습니다. 변경 이후 실제로 문제가 발생했던 오전 7시, 7시 30분 시간대의 상태를 보겠습니다.

그림5 소켓 재사용 적용 후 문제의 시간 소켓 사용 상태

그림과 같이 해당 시간에 문제가 발생했던 이슈는 완벽하게 클리어 되었습니다. 야호!!

결론

실제 서비스에서 로컬 포트 부족과 같은 문제가 발생한다고 하면, 순서대로 하나씩 확인하고 조치해야 한다고 생각합니다. 저는 가장 먼저 소켓을 재사용하도록 하는 것이 가장 중요하다고 생각하고, 그다음 순서로 TIME_WAIT 재활용하도록 하고, 마지막 순서로 로컬 포트를 확장하도록 하는 것이 문제를 해결하는데 도움이 된다고 생각합니다.

PS. 제가 엔지니어이긴 하지만, 저보다 경험과 지식이 풍부한 시스템 엔지니어분들과 같이 근무하고 있어 항상 많은 도움을 받고 있습니다. 제가 로컬 포트와 TIME_WAIT에 대해 올바르지 않은 내용을 적지 않도록 몇 가지의 참고 사이트를 공유받았는데, 같이 공유드립니다.

참고자료

1) 깃허브 : https://github.com/torvalds/linux/blob/master/include/net/tcp.h#L120

2) TCP TIME_WAIT : http://docs.likejazz.com/time-wait/

3) tw_reuse, tw_recycle : https://support.hpe.com/hpsc/doc/public/display?docId=emr_na-c00782457

http://docs.likejazz.com/time-wait/

https://brunch.co.kr/@alden/3

4) 커널이 로컬 포트를 선택하는 과정 : https://brunch.co.kr/@alden/19


Popit은 페이스북 댓글만 사용하고 있습니다. 페이스북 로그인 후 글을 보시면 댓글이 나타납니다.