https://sunyzero.tistory.com/m/198
TCP 소켓 네트워크 프로그래밍을 하다 보면 TIME_WAIT 상황에 대한 고민을 하는 시점이 오게 된다. 학부 시절 네트워크 프로그래밍 수업을 듣고 실습실에서 열심히 프로그래밍 해봤다면 학부 때 맞닥뜨리게 되는 경우도 있다. 만일 학생 때 고민하지 않고 넘어갔다면 회사에서 주먹구구식으로 혼동을 일으키는 내용이기도 하다. 그래서 이에 대해 좀 정확한 정보를 전달하고자 이 글을 쓴다.
아래 글은 각종 책과 표준안의 레퍼런스와 실제 코딩으로도 검증했지만, 그래도 혹시 틀린 점이 있다면 개의치 말고 지적해주면 감사하겠다. ^^
1. TIME_WAIT란 무엇인가?
TCP의 TIME_WAIT는 TCP 연결을 종료 할 때 신뢰성을 높이기 위해 존재하는 것으로 자연스럽게 발생하는 상태다. TCP/IP 네트워크 교과서인 TCP/IP Illustrated volume1에 보면 TCP 상태 전이도(TCP state transition diagram)에 표시되어있다.1 TCP 상태 전이도는 복잡할 수도 있으니 본인의 저서에 좀 쉽게 풀어서 그린 그림을 편집해서 붙여보겠다.2
TCP state transition (출처 : 내 책)
위 그림을 보면 클라이언트측의 마지막 상태에 TIME_WAIT가 발생하는데, 이는 클라이언트측에서 active close를 했다고 가정했기 때문이다. 만일 서버측에서 먼저 active close를 했다면 그림의 아래 부분의 좌우는 바뀌게 되어 , 서버측에 TIME_WAIT가 발생하게 된다. 아주 드물게 simultaneous close 경우에는 서버, 클라이언트 양측에 TIME_WAIT가 발생한다.
- active close 접속 종료를 먼저 하는 행위를 말한다. TCP 프로토콜에서 active close는 FIN 세그먼트를 먼저 전송하는 측을 의미한다. 소켓 프로그래밍에서는 close 혹은 shutdown 함수를 먼저 호출하는 측이 active close를 하는 측이다.
일반적으로 클라이언트가 active close 하도록 설계하므로 TIME_WAIT는 클라이언트측에 발생한다. 하지만 타임 아웃이나 비정상 종료를 처리하는 경우에는 서버측에서 active close하는 경우가 있다. 그렇지만 세상일은 항상 일반적인 케이스만 있는 것은 아니다. 간혹 비정상이 일반적인 케이스도 있는데, 웹서버가 그런 경우다. 웹서비스에서 사용하는 HTTP 프로토콜은 구현의 특성상 여러 클라이언트 커넥션을 빠르게 받기 위해 서버측에서 active close를 시도하는 상황이 일반적이다. 그래서 대부분의 TIME_WAIT 이슈는 웹서비스에서 주로 발생한다. 게임이나 증권 시스템들은 time_wait보단 fin_wait2나 close_wait 상태가 문제가 되는 경우가 많다.
참고로 TIME_WAIT 상태의 타임아웃은 시스템마다 다르지만 리눅스의 경우에는 60초로 고정되어있다. 커널 설정으로 바꿀 수 있다고 하는 경우도 있는데 사실과 다르다. 리눅스 커널 코드를 보면 60초로 고정이다. 다만 커널 설정으로 TIME_WAIT 상태를 재사용 하도록 설정할 수 있다.
위의 그림으로 제대로 이해가 안간다면 TCP/IP Illustrated Volume1을 보던가 아니면 설명이 잘 나와있는 다른 사람의 블로그를 소개하니 읽고 오자. : TIME_WAIT를 남기지 않는 세션종료 (Graceful Shutdown) 참고로 TIME_WAIT를 소개한 http://kuaaan.tistory.com/118 은 거의 다 맞는 내용이지만 약간 혼동을 줄 수 있는 내용도 있어 보완 설명을 하겠다. 문제가 되는 부분은 중간에 "2. linger 옵션에 대하여"의 윗 단락 부분이다. 아래 인용 부분을 보자. "이 TIME_WAIT라는 상태가 중요한 이유는, 만약 종료절차가 잘못 진행되어 서버쪽에 TIME_WAIT가 남게 되면 심각한 문제가 발생할 수도 있기 때문입니다. 일단 TIME_WAIT가 시작되면 2분여 이상 상태가 지속되게 되는데 모든 클라이언트들의 세션 종료시마다 서버 측에 TIME_WAIT가 발생한다면, 서버측에 부하가 될 뿐만 아니라 최악의 경우 서버에서는 더이상 새로운 연결을 받아들일 수 없는 상황이 발생할 수 있습니다. 말하자면.. 장애 상황이 발생하는 거죠. (실제로 실 운영서버에 이런 일이 발생하는 것을 직접 목격한 적이 있습니다. ) - http://kuaaan.tistory.com/118 인용한 부분에서 서버 측이 TIME_WAIT로 인해 새로운 연결을 받아들이지 못한다는 했지만, 이는 웹서비스와 같은 특수한 케이스에서 주로 나타난다. 지속적인 연결을 사용하는 시스템에서는 발생하지 않을수도 있다. 만일 리눅스 시스템을 사용하고 있고 time wait 때문에 문제가 발생한다면 tcp_max_tw_buckets, tcp_tw_reuse, tcp_tw_recycle, ip_local_port_range를 튜닝하는 것도 방법일 수 있다. 그러나 이런 경우라고 할지라도 서버측의 가용 포트가 줄어드는 것은 아니다.(클라이언트측의 경우에는 가용 포트가 줄어들 수 있다.) 다른 블로그나 KLDP에서도 TIME_WAIT로 인해 가용 할 수 있는 port가 줄어들어 서버에 문제가 생긴다는 글이 꽤 많은데, 가용 port는 줄어드는 것이 없다. 왜냐하면 서버측은 listen port만 사용하기 때문이다. 소켓의 주소는 local과 foreign address가 페어(pair)로 되어있는데 서버측은 listen port로 고정되고 클라이언트 주소만 달라진다. 예를 들어 ssh 서버에 접속한 클라이언트가 3개가 있는 그림을 보면 쉽게 이해가 갈 것이다. netstat 화면 위 그림을 보면 서버 측의 local address는 모두 22번 포트를 사용하는 것을 볼 수 있다. 이와 반대로 클라이언트측 주소인 foreign address는 모두 포트 번호가 달라진다. 결국 서버 측은 1개의 포트만 사용하므로 TIME_WAIT로 가용 포트가 줄어든다는 것은 사실과 다르다. 즉 가용 port 개수가 문제가 아니라 time_wait 버킷의 제한값에 도달하거나 오픈된 파일 개수 제한에 걸려서 문제가 발생하는 경우가 대다수이다.
원래 TIME_WAIT가 문제되는 경우는 클라이언트측이 빠르게 접속, 종료를 반복할 때 클라이언트측의 가용 port가 소진되는 것을 의미한다. 주로 네트워크 서버의 스트레스 테스트 클라이언트에서도 이런 문제가 보고된다.(서버측이 active close를 빠르게 반복하는 경우에는 조금 다른 양상의 문제가 발생한다.) 본인도 회사에서 스트레스 테스트를 위한 더미 웹 브라우저 클라이언트를 개발할 때 이런 문제를 겪었었다. 물론 교과서에서 배운대로 SO_LINGER로 간단히 처리했다. (SO_LINGER가 싫다면 SO_REUSEADDR을 이용하여 TIME_WAIT 상태의 주소를 재사용하는 방법도 있다.)
또한 서버측 로그에서는 TIME_WAIT로 인해 가용할 수 있는 포트가 없어서 socket을 bind하는데 실패하면 EADDRNOTAVAIL 에러가 발생하므로 쉽게 인지할 수 있다.
TIME_WAIT로 인해 가용할 포트가 줄어서 EADDRNOTAVAIL이 발생한다면 해결 방법은 2가지가 있는데, 코드를 수정하거나 리눅스 시스템 설정을 바꾸거나 둘 중에 하나로 해결할 수 있다.(둘 다 해도 된다.) 첫째로 프로그램 코드를 수정할 수 있는 경우라면 바로 앞에서 설명한 SO_LINGER나 SO_REUSEADDR을 사용하는 방법을 쓰거나, 코드를 건드리지 못한다면 Linux의 경우 net.ipv4.tcp_tw_reuse의 값을 1로 변경하면 된다. root 권한으로 sysctl net.ipv4.tcp_tw_reuse=1을 실행하면 된다. 보통은 요새 tuned를 사용하므로 tuned profile을 만드는게 좋다.
참고로 별다른 조치를 취하지 않을 경우, 클라이언트측이 초당 몇 개의 커넥션을 열면 모든 포트를 TIME_WAIT로 소진하는지 계산한 문제가 있다. 바로 TCP/IP Illustrated 연습 문제로서 다음과 같이 적혀있다.