2015년 6월 5일 금요일

TCP의 TIME_WAIT는 없애는 방법 - SO_LINGER

출처: http://sunyzero.tistory.com/198

* TCP의 TIME_WAIT는 없애는 방법은?

TCP 소켓 네트워크 프로그래밍을 하다 보면 TIME_WAIT 상황에 대한 고민을 하는 시점이 오게 된다. 학부 시절 네트워크 프로그래밍 수업을 듣고 실습실에서 열심히 프로그래밍 해봤다면 학부 때 맞닥뜨리게 되는 경우도 있다. 만일 학생 때 고민하지 않고 넘어갔다면 회사에서 주먹구구식으로 혼동을 일으키는 내용이기도 하다. 그래서 이에 대해 좀 정확한 정보를 전달하고자 이 글을 쓴다.

    아래 글은 각종 책과 표준안의 레퍼런스와 실제 코딩으로도 검증했지만, 그래도 혹시 틀린 점이 있다면 개의치 말고 지적해주면 감사하겠다. ^^



    1. TIME_WAIT란 무엇인가?
    2. 이 글을 쓰게 된 계기
    3. TIME_WAIT는 당신을 해치지 않는다. 그래도 없애고 싶다면?
    4. 결론


    TCP의 TIME_WAIT는 TCP 연결을 종료 할 때 신뢰성을 높이기 위해 존재하는 것으로 자연스럽게 발생하는 상태다. TCP/IP 네트워크 교과서인 TCP/IP Illustrated volume1에 보면 TCP 상태 전이도(TCP state transition diagram)에 표시되어있다.1 TCP 상태 전이도는 복잡할 수도 있으니 본인의 저서에 좀 쉽게 풀어서 그린 그림을 편집해서 붙여보겠다.2

    TCP state transitionTCP state transition (출처 : 내 책)
    위 그림을 보면 클라이언트측의 마지막 상태에 TIME_WAIT가 발생하는데, 이는 클라이언트측에서 active close를 했다고 가정했기 때문이다. 만일 서버측에서 먼저 active close를 했다면 아래 부분 그림의 좌우는 바뀌게 되어 , 서버측에 TIME_WAIT가 발생하게 된다. 아주 드물게 simultaneous close 경우에는 서버, 클라이언트 양측에 TIME_WAIT가 발생한다. 

    하지만 일반적으로 타임 아웃이나 비정상 종료를 제외하고는 클라이언트가 active close 하도록 설계하므로 TIME_WAIT는 클라이언트측에 발생한다. (참고로 리눅스의 TIME_WAIT는 60초 고정이다.)

    위의 그림으로 제대로 이해가 안간다면 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는 메모리가 지극히 부족한 경우를 제외하고는 서버측에 문제를 일으키지 않는다. 아마도 직접 목격한 사건은 다른 것이 원인이었으나 착각한 것이 아닐까 생각된다.(왜냐하면 timewait 버킷 개수는 간단한 커널 설정으로도 수정되기 때문이다.)

    그런데 다른 블로그나 KLDP에서도 TIME_WAIT로 인해 가용 할 수 있는 port가 줄어들어 서버에 문제가 생긴다는 글이 꽤 많다. 그들도 아마 클라이언트와 착각을 한 것이라고 생각된다. 왜냐하면 서버측은 listen port만 사용하기 때문이다. 소켓의 주소는 local과 foreign address가 페어(pair)로 되어있는데 서버측은 listen port로 고정되고 클라이언트 주소만 달라진다. 예를 들어 ssh 서버에 접속한 클라이언트가 3개가 있는 그림을 보면 쉽게 이해가 갈 것이다.

    netstat 화면netstat 화면

    위 그림을 보면 서버 측의 local address는 모두 22번 포트를 사용하는 것을 볼 수 있다. 이와 반대로 클라이언트측 주소인 foreign address는 모두 포트 번호가 달라진다. 결국 서버 측은 1개의 포트만 사용하므로 TIME_WAIT로 가용 포트가 줄어든다는 것은 사실과 다르다.

    TIME_WAIT가 문제가 되는 경우는 클라이언트측이 빠르게 접속, 종료를 반복할 때 클라이언트측의 port가 소진되는 것으로서, 주로 스트레스 테스트 클라이언트에서 이런 문제가 보고된다. 본인도 회사에서 스트레스 테스트를 위한 더미 웹 브라우저 클라이언트를 개발할 때 이런 문제를 겪었었다. 물론 교과서에서 배운대로 SO_LINGER로 간단히 처리했다.

    참고로 별다른 조치를 취하지 않을 경우, 클라이언트측이 초당 몇 개의 커넥션을 열면 모든 포트를 TIME_WAIT로 소진하는지 계산한 문제가 있다. 바로 TCP/IP Illustrated 연습 문제로서 다음과 같이 적혀있다.

    TCP/IP Illustrated Volume1. p262
    Q) Exercises 18.14 With an MSL of 120 seconds, what is the maximum at which a system can initiate new connections and then do an active close?
    A) The limit is about 268 connections per second: the maximum number of TCP port numbers (65536-1024 = 64512, ignoring the well-known ports) divided by the TIME_WAIT state of 2MSL.


    이 글을 포스팅하게 된 계기는 KLDP에서 오래전에 쉰 떡밥(2003년도 떡밥)인 TIME_WAIT 관련 내용이 10년 만에 새로 운 답글이 달렸기 때문이다. 도저히 지나칠 수 없는 강력한 떡밥이라서 몇 시간을 들여서 포스팅을 하게되었다. 물론 KLDP에는 답글을 달지 않았다. 이유는 나보다 더 까칠하지만 간략하게 오류를 지적한 다른 분이 있어서...


    그런데 답글을 읽다 보니 둘 중에 누가 맞는지 혼동을 줄 수 있는 내용들이 있어 혹시 검색하는 학생이나 신입 사원들에게 도움이 될까하여 좀 자세히 포스팅 했다. 사실 여기 있는 내용들은 TCP/IP Illustrated나 내 책에도 다 있는 내용이다. 책을 열심히 읽은 사람들은 대부분 알고 있는 내용일 것이다. 그러면 이제 논란이 된 부분을 정리하자. 

    1. TCP_NODELAY는 TIME_WAIT와 상관이 없다. TCP_NODELAY는 소켓에 Nagle's algorithm을 on/off 시키는 기능이다.
    2. 원 글을 쓴 사람은 웹 서버를 개발하는데 있어서 서버 측에서 active close하여 발생하는 TIME_WAIT를 문제 삼은 것이다. 그러나 앞서 이야기 한 것처럼 TIME_WAIT는 문제가 되지 않는다. Java 서버 개발자분이 TIME_WAIT 를 문제 삼은 것은 아마도 TCP/IP에 대해 잘 몰라서 그런 것이 아닐까 생각된다.

    KLDP의 떡밥글에 10년 만에 새롭게 달린 굉장히 공격적인 답글이 보이는데, 정답이 아니라서 상당히 유감이다. 해당 답글에서는 서버측에서는 shutdown을 하고, 클라이언트 측에서는 SO_LINGER를 사용하면 TIME_WAIT이 발생되지 않는다는 말이 나오는데 절반 정도만 맞은 사실상 오답이다. 정답은 클라이언트든 서버든 무조건 SO_LINGER로만 TIME_WAIT을 없앨 수 있다. 

    shutdown과 close의 차이는 linked channel이 있는 경우 무시하고 close를 할 것인지 말 것 인지의 차이만 있을 뿐 기능은 같다. 고로 정답을 말한 답글은 다음과 같다.

    KLDP 정답 답글KLDP 정답 답글

    참고로 글을 쓰기 전에 네이버나 구글로 TIME_WAIT를 검색해보니 다른 블로그들에도 비슷한 내용이 있지만 틀린 내용을 적은 블로그들이 절반이 넘었다. 그래서 교과서가 아닌 웹 검색으로 공부하는 학생들은 틀린 내용으로 알고 있는 경우도 많을 것 같다. 항상 정확한 검증을 위해 웹 검색보다는 교과서나 표준안, 매뉴얼을 먼저 보도록 하자.



    TIME_WAIT는 정상적인 상태이며 당신을 해치지 않지만, 그래도 꼭 없애고 싶다면 SO_LINGER를 사용하면 된다. 이것도 kuaaan님이 쓰신 블로그 글에 포스팅 되어있다. 인터넷엔 워낙 검증되지 않은 글들이 많으니 의심이 많은 사람은 이 글을 믿지 못할 수 있다. 

    그래서 공신력 있는 스티븐스의 TCP/IP Illustrated의 설명을 인용해 보겠다.

    W. Richard Stevens. TCP/IP Illustrated Volume 1, p247 Aborting a Connection
    Aborting a connection provides two features to the application: 
    (1) any queued data is thrown away and the reset is send immediately, and (2) the receiver of the RST can tell that the other end did an abort instead of a normal close. The API begin used by the application must provide a way to generate the abort instead of a normal close.
    We can watch this abort sequence happen using our sock program. The sockets API provides this capability by using the "linger on close" socket option(SO_LINGER).

    위 인용한 부분을 코드로 쓰면 다음과 같다. 아래 코드에서 cfd는 클라이언트와 연결된 소켓 파일 기술자다.
    1struct linger solinger = { 1, 0 };
    2if (setsockopt(cfd, SOL_SOCKET, SO_LINGER, &solinger, sizeof(struct linger))
    3        == -1) {
    4    perror("setsockopt(SO_LINGER)");
    5}

    위와 같이 SO_LINGER를 설정한 상태에서 active close를 하면 정말 스티븐스 아저씨 말대로 TIME_WAIT가 생기지 않는지 확인해봐야 한다. 확증을 위해 예제를 작성해서 살펴보면 된다. 

    패킷은 wireshark로 캡쳐했다. 그림이 크니 꼭 클릭해서 보자. 예제 코드는 너무 간단해서 첨부하지 않았는데, 혹시 필요한 사람이 있다면 나중에라도 첨부해 놓겠다.

    wireshark : TCP SO_LINGER optionwireshark : TCP SO_LINGER option 확인

    그림의 클라이언트측(192.168.0.10)과 서버측(192.168.0.100)은 연결 후 몇 초 뒤 서버측에서 active close하도록 프로그래밍 한 상황이다. 서버측(192.168.0.100)에서는 SO_LINGER 옵션에서 타임아웃을 0초로 설정하였다. 그 결과 10번 프레임을 보면 서버측이 FIN을 먼저 보내는 것을 볼 수 있다.

    그리고 11번 프레임에서 ACK를 수신 받고, 12번 프레임에서 서버측은 RST를 송신하여 연결을 취소하는 것을 볼 수 있다. 이 과정을 설명하면 다음과 같다.

    앞서 서버측은 FIN을 보낸 다음에 기다리지 않고 즉시 소켓 연결을 파괴해버렸다. 그러나 클라이언트는 FIN을 수신받고 잘 받았음을 의미하는 ACK를 발송하게 된다. 하지만 서버측에선 이미 파괴된 연결에 ACK가 수신되었으므로 "이미 없는 연결에 왜 패킷을 보내니?" 하면서 클라이언트에게 RST를 보낸 것이다. 이에 클라이언트는 RST를 수신하고 취소 작업에 들어가게 된다. 

    그 다음에 서버에 도착한 FIN(13번 프레임)은 클라이언트가 워낙 빠르게 반응해서, 첫 번째 RST가 수신되기 전에 close()를 호출한 결과이다. 따라서 두 번째 RST의 등장은 클라이언트의 반응 속도와 RTT에 따라 생길 수도 있고 아닐 수도 있다. 만일 위와 같이 로컬에서 테스트하면 거의 대부분 생긴다. 이에 서버는 두 번째 RST를 보내서 "뭔 소리야? 아까 내가 보낸 RST 몰라? 응? 응?" 하면서 다시 RST를 보내게 된 것이다. 

    이렇게 linger가 설정되면 서버측은 active close를 하고도 TIME_WAIT가 발생되지 않는다.3

    비교를 위해 SO_LINGER를 설정하지 않은 경우에는 어떻게 되는지도 살펴보자. 예상한 대로라면 분명 FIN을 서로 주고받고 끝날 것이다. 마찬가지로 wireshark로 캡쳐를 해봤다.

    wireshark : TCP normal closewireshark : TCP normal close

    위 그림을 보면 앞서 SO_LINGER를 설정한 경우와는 다르게 RST을 주고 받는 부분이 보이지 않는다. 이것이 TIME_WAIT를 발생시키는 정상적인 close의 패킷 흐름이다. 두 그림을 비교하면서 살펴보면 그 차이를 쉽게 알 수 있을 것이다.


    TIME_WAIT에 대해 인터넷에 돌아다니는 정보 중에는 틀린 내용도 많다. 교과서를 먼저 보자.

    그리고 TIME_WAIT는 당신의 서버를 해치지 않는다는 것을 명심하자. 간혹 서버측에서 TIME_WAIT로 인해 재시동시에 socket bind 실패로 에러가 발생하는데, 이것은 SO_REUSEADDR 옵션으로 간단하게 해결 된다.


    * 레퍼런스

    1. W. Richard Stevens. (1994). TCP/IP Illustrated Volume 1. p240 TCP state Transition Diagram. [본문으로]
    2. 김선영. (2012). Advanced 리눅스 시스템 네트워크 프로그래밍 2판. 가메출판사. p250 TCP 소켓 관련 함수와 TCP 상태 전이 [본문으로]
    3. Clark Satter, Reset은 어디서 오는 것일까? (황새가 물어다 주지 않습니다). http://blogs.technet.com/b/escape/archive/2010/01/06/reset.aspx [본문으로]

    댓글 없음:

    댓글 쓰기