Linux Programmer

오류를 잡자 : TCP에는 우아한 종료라는 것은 없다. 본문

컴퓨터 관련/프로그래밍 일반

오류를 잡자 : TCP에는 우아한 종료라는 것은 없다.

sunyzero 2020. 2. 2. 05:53

google에서 "TCP 우아한 종료"라고 검색해보자. 상당히 많은 내용이 나온다. 그러나 틀린 이야기들이 대부분이다. 문제는 이 틀린 이야기가 상당히 오랫동안 인터넷을 어지럽히고 많은 학생이나 개발자들에게 혼동을 준다는 점이다.

본인은 부업으로 그룹의 IT계열 회사나 금융권에 특강을 다닌다. 대상은 적어도 시니어급 이상의 개발자, 서버 관리자들이 대부분이다. 그런데 강의를 다니다보면 시니어급 개발자(프로그래머)들의 레벨에서도 잘못 알고 있는 지식이 많음을 느낄 수 있었다. 특히 잘못된 교재로 공부한 경우나 잘못 번역된 책으로 공부한 경우에는 특히 심했다.

사실 이 글을 쓰게 된 계기는 어제(20/02/01) IT관련 교육자들이 모이는 meetup으로 잠실의 '우아한 형제들 작은 집'에 갔다가 "(어떤 책이나) 강의자료나 강의내용의 오류는 왜 교정되지 않고 계속 반복되는 것일까" 토론중에 갑자기 생각이 났다. (그러고보니 우아한 형제들은 graceful brothers인가?)

 

1. TCP의 우아한 종료?

많은 블로그나 책을 보면 TCP에는 우아한 종료가 있다고 어쩌고 저쩌고 이야기를 한다. 이건 번역의 오류가 검증없이 퍼져나가서 생긴 문제다.

TCP 소켓을 닫을 때 일반적으로 2가지 방법이 있다. 각각 일반적인 닫기(normal close), 강제적인 닫기(abortive close)라고 한다. 그런데 여기서 일반적인 닫기를 예전에는 graceful close 혹은 graceful shutdown이라고도 불렀는데 이것이 오류의 시발점이 되었다.

 

(참고) close와 shutdown 용어는 모두 버클리 소켓 함수에서 유래한 기능으로 소켓 파일 디스크립터를 닫는 역할을 한다. 세부적으로는 약간 다르지만 일반적으로 파일 디스크립터가 복제되지 않는한 소켓 레이어에서 둘은 동작 결과는 같다. [1] [2]

다만 file descriptor자체는 언제나 닫아줘야 하기 때문에 shutdown을 하더라도 close는 해야 한다는 점에서 shutdown 자체는 복잡성을 증가시키는 면이 있다. 현대적인 network programming은 이런 연유로 되도록이면 shutdown을 쓰지 않는 추세다.

 

여기서 graceful close를 누군가 "우아한 종료"로 해석하기 시작하면서 오개념이 유행했던 것 같다. 그러면 어떻게 해야 우아한 종료일까? TCP packet을 우아한 몸짓으로 끊어주는 걸까? 아니면 컴퓨터들끼리 좀 더 부드러운 전기 신호를 쓰는 기능이 있는걸까? 정말이라면~ 에엑??? 부드러운 전기....

graceful의 의미는 사실상 우아하게라고 쓰이지만, 컴퓨터쪽에서는 의미가 다르게 쓰인다. 컴퓨터 쪽에서 graceful의 의미는 "(유예를 가지고 있는) 부드러운/깔끔한/적합한"의미를 가지고 있다.

그래서 graceful close는 "(유예를 두는) 깔끔한 정상 종료", 혹은 의역하면 "깔끔하게 정상적인 종료를 하기위해 시간을 기다려줄 수 있는 것"의 늬앙스를 담아서 해석하는게 맞다. 원래 grace period라는 말도 있는데 이는 유예기간이라는 뜻이다. 그러므로 graceful close, graceful shutdown은 패킷 전송에 있어서 유예 기간(다른 말로 타임아웃으로 이해해도 된다.)을 두고 앞서 전송된 패킷 모두가 정상적으로 전송된 뒤에 깔끔하게 끊는 것을 말한다. TCP/IP의 Bible이라고 불리는 Richard W. Stevens의 TCP/IP Illustrated Volume 1에 보면 다음과 같은 글이 있다.

 

The seven segments we have seen are baseline overheads for any TCP connection that is established and cleared "gracefully". (There are more abrupt ways to tear down a TCP connection using special reset segments, which we cover later)

- TCP/IP Illustrated, Volume 1 (2nd Edition) Kevin R. Fall, W.Richard Stevens, p598 TCP Connection Establishment and Termination 

 

위 인용된 페이지의 앞 부분에 보면 TCP 연결과 종료의 그림이 있고, 연결에서 TCP 쓰리웨이 핸드쉐이킹 하는 부분과 종료에서 TCP 포웨이 핸드쉐이킹하는 부분이 있다.[3] 이에 총 7개의 세그먼트를 주고받게 된다. 이런 기본적인 개념을 가지고 위 글을 부드럽게 의역하면 다음과 같다. "앞서 봤듯이 TCP의 연결 설정과 해제 과정(적합한 처리 과정에 유예 시간이 존재할 수 있음)에는 기본적으로 7개의 세그먼트 오버헤드를 가진다. (특별히 reset 세그먼트를 이용해서 TCP 연결을 갑작스럽게 끊는 방법도 있는데 뒤에서 다룰 것이다.)" 하지만 이를 잘못해석하여 gracefully를 우아하게라는 뜻으로 받아들이면 전혀 다른 의미가 된다.

 

사실 graceful close의 의미를 한국어로 표현하는 것은 매우 어려운 일이다. 영어 단어가 함축하고 있는 의미를 가진 단어가 한국어에는 없는 경우가 종종 있기 때문이다.(e.g. segment, fragment의 차이가 대표적이다. 둘다 조각으로 해석될 수 있지만 어떤 조각인지 설명하는 것이 다르다. 하나는 원형의 모양이 의미가 없거나 알 수 없지만 다른 하나는 원형의 어떤 부분을 의미한다.)

하지만 우아하게 뭔가를 진행하려면 빠르게 하는 것보다 느리고 천천히 하면서 기다려야 하기 때문에 우아한~의미로 해석해도 맞다고 하는 분들도 있다. 물론 그 말이 맞을 수도 있으나 한국어에서 통용되는 우아함이란 어떤 시간적 유예의 의미를 내포하는 용도로는 거의 쓰이지 않는다.

한국에서 어떤 트랜잭셔널한 일을 우아하게 처리하자고 하다간 거의 펀쿨섹 같은 소리 한다고 한소리를 들을 가능성이 높다고 생각된다.

 

이렇게 단어의 문맥적 의미를 잘못 이해한 누군가가 오역을 하고, 그 오역이 퍼져나가다가 심지어 왜곡까지 되었다. 그래서 몇몇 책이나 블로그에는 TCP의 half close가 우아한 종료라고 잘못 말하기도 한다. Half close는 TCP 소켓의 송신,수신 채널에 대한 것이므로 graceful close 개념이 아니다. 그럼에도 불구하고 많은 사람이 우아한 종료라고 착각하고 오염된 지식을 재생산하고 있다.

 

** 어떤 이들은 이런 개념을 잘못 아는게 무슨 큰일이냐고 되물을 수도 있다. 그런데 사실상 큰일 맞다. Abortive close는 나쁜게 아님에도 불구하고 우아하지 못하다는 오해를 받아서 사람들이 쓰면 큰일 나는 비정상적인 기능으로 오해를 받는다.(물론 비정상적인 상황에서도 발생하지만...) 하지만 application protocol을 설계할 때 7계층에서 명시적으로 끊는 메시지를 받았다면 즉시 RST를 전송하고 소켓을 파괴하도록 설계하는 경우도 종종 있다.(장비 특성상 이건 잘못된 것이 아니라 꼭 필요한 경우가 있다. 예를들어 오버헤드를 줄이거나 성능을 높이기 위해서 이런 기법을 사용하는 경우가 있다.)

반대로 graceful close에서 linger 기능을 사용할 경우 close단계에서 grace period 동안 wait의 주체를 커널이 할것인지, 아니면 프로세스(유저레벨)에서 할 것인지 결정하는 것은 tcp의 올바른 이해에서 비롯된다. 이를 잘못 이해하면 시스템에 성능 이슈를 가져올 수 있다. 

 

이에 대한 확실한 예로 graceful close를 하는 경우에 linger 옵션을 이용해서 다음과 같이 옵션을 주면 8초의 grace period를 주고, 사용자 프로그램이 직접 기다릴 수 있다. (예제의 행동은 서버형 프로그램에서는 작성하면 안되는 코드이다. 특수한 경우를 제외하고는 서버형 프로그램에서 blocking되는 코드를 작성하는 것은 잘못 배운 것이다. 그럼에도 불구하고 linger에 대해 잘못 이해해서 linger timeout을 positive value를 줘서 코딩한 경우가 있었는데, 다음날 오전에 오픈했을 때는 괜찮았다가 오후부터 시스템이 접속을 받지 못해서 난리난 적이 있었다. 왜 그런 일이 발생했는지는 linger가 하는 일을 제대로 이해했다면 쉽게 유추가 가능할 것이다.)

 

struct linger solinger = { 1, 8 };
if (setsockopt(cfd, SOL_SOCKET, SO_LINGER, &solinger, sizeof(struct linger))
        == -1) {
    perror("setsockopt(SO_LINGER)");
}

 

이 외에 "Graceful"을 사용하는 용어를 해석하다보면 다음과 같은 문장이 나올 수도 있다. 예를 들어 "By default, all transactions are graceful within 60 seconds." 라고 적으면 "기본적으로 모든 트랜잭션은 보통 60초내에 처리된다.(처리되는데 있어 최대 60초의 대기 시간이 있을 수 있다)"로 해석된다. 즉 보통은 처리를 위해 유예 혹은 대기 시간이 60초이므로 해당 트랜잭션은 약간 딜레이가 발생하면 60초까지도 대기할 수 있고, 이를 넘기면 어떤 액션(에러 처리 or retry, queueing?)을 취할 것이라는 늬앙스를 준다. 그리고 해당 메시지 뒤에 timeout설정이나 혹은 timeout시 어떤 액션이 취해질 것인지 적혀있는 경우가 대부분이다. 실제로 금융권에서 처리되는 몇몇 느린 트랜잭션은 저런 식으로 매뉴얼이 적혀있기도 하다. 하지만 이를 우아한 뜻으로 해석하면 도저히 의미가 통하지 않는다.

 

1.1. graceful, gracefully의 또다른 예

다른 예로 어떤 장치는 Start/Stop와 Power off 버튼이 따로 존재하는 경우도 있는데, Start/stop에는 gracefully하게 작동한다고 써있기도 한다.(반대로 Power off는 abortive하게 작동한다고 쓰여있을 수도 있다) 이런 경우에는 작동하는 내부 서비스 프로세스를 정상적으로 종료하고, 데이터는 저장장치에 기록하고 순서대로 종료한다. 그러나 Power off를 누르면 즉시 전원을 차단해서 강제로 꺼진다. 그래서 매뉴얼에는 stop을 누르고 완전히 정지된 상태에서 power off를 하라고 제안한다. 하지만 응답이 없으면 그냥 power off를 누르라고 한다. 이렇게 설계하는 이유는 정상적으로 종료하려고 했는데 응답이 없는 서비스나 프로세스가 있을 땐 강제로 꺼야 하기 때문이다.

PC의 경우에도 Alt-F4 눌러서 종료 버튼을 누르면 열려있는 프로세스나 창을 다 닫고 종료한다. 몇몇 프로세스가 시간이 오래걸리면 대기를 해주기도 하는데 이는 운영체제가 graceful shutdown하는 것으로 볼 수 있다. 그러나 파워 버튼을 4~5초정도 누르고 있으면 강제로 전원이 나가면서 종료되기도 한다. 이건 운영체제, 컴퓨터를 만들때 2가지 기능, 즉 graceful shutdown과 abortive shutdown 기능을 넣어두었기 때문이다. 시스템에 따라서 2가지 기능 중에 한가지만 구현해두는 경우도 있다. (참고: 스마트폰은 abortive shutdown기능을 제공할까? 스마트폰의 graceul하게 끄는 방법과 강제로 끄는 방법이 따로 있다면 어떻게 표현될까?)

따라서 graceful OOOO(혹은 OOOO gracefully)은 TCP에서만 사용되는 용어가 아니고 대부분의 IT 기기나 소프트웨어에서 다양하게 사용되는 용어이고 "우아한"의미로 해석하지 말고 정상적으로 어떤 액션을 수행하는데 있어서 깔끔하게, 혹은 부드럽게 처리하기 위해 순서를 기다리는 유예시간이 있을 수 있음으로 번역하고 이해하는게 맞다.

 

또다른 예로 apache web server의 매뉴얼에서 graceful restart 혹은 graceful stop 명령에 대한 설명을 볼 수 있다. 아래는 아파치 웹서버 매뉴얼에서 Stop Now, Graceful Restart 부분을 인용한 부분이다.

 

https://httpd.apache.org/docs/2.4/en/stopping.html

 

Stopping and Restarting Apache HTTP Server - Apache HTTP Server Version 2.4

In order to stop or restart the Apache HTTP Server, you must send a signal to the running httpd processes. There are two ways to send the signals. First, you can use the unix kill command to directly send signals to the processes. You will notice many http

httpd.apache.org

Stop Now
  Signal: TERM
apachectl -k stop

Sending the TERM or stop signal to the parent causes it to immediately attempt to kill off all of its children. It may take it several seconds to complete killing off its children. Then the parent itself exits. Any requests in progress are terminated, and no further requests are served.


Graceful Restart
  Signal: USR1
apachectl -k graceful

The USR1 or graceful signal causes the parent process to advise the children to exit after their current request (or to exit immediately if they're not serving anything). The parent re-reads its configuration files and re-opens its log files. As each child dies off the parent replaces it with a child from the new generation of the configuration, which begins serving new requests immediately.

위 인용된 매뉴얼을 매끄럽게 번역해보았다. 특히 graceful의 의미를 살리기 위해 2가지를 염두에 두고 해석하였다. 첫째로 읽는 사람들이 UNIX signal이나 웹 서버의 구조에 대해 잘 모를 수 있기 때문에 번역의 이해를 돕기 위한 내용을 { } 부분으로 표시하여 추가하였다. 둘째로 매뉴얼 윗 부분에 apachectl의 옵션 및 아규먼트(인수)에 대한 부분이 있으므로 이를 포함하였다.

 

그리고 용어의 이해를 돕기 위해 약간의 배경지식을 이야기 하도록 하겠다. 아파치 웹서버는 구동되면 fork를 통해 복제된 자식 프로세스들을 만들게 된다. 이 과정에서 아파치 웹서버는 부모 프로세스와 자식 프로세스로 나뉘는데, 부모 프로세스는 자식 프로세스들의 갯수 pool을 관리하는 일을 주로 하고 실제 웹 서비스 처리는 자식 프로세스들이 한다.

Stop Now {즉시 정지}
시그널 : TERM
apachectl -k stop

TERM 유닉스 시그널 혹은 {apachectl 명령행의 -k} stop 인수를 통해 시그널을 받은 부모 프로세스는 즉시 모든 {웹 서비스를 처리하는} 자식 프로세스를 죽이려고 시도한다. 자식 프로세스들을 모두 죽이는 데는 약간의 시간이 소요될 수 있다. 그러고 {자식 프로세스들이 모두 죽은 뒤에는} 부모 프로세스 자신도 종료한다. 이 과정에서 처리 중인 모든 리퀘스트는 중단되고, 더 이상 처리되지 않는다.

Graceful Restart {매끄러운 재시작}
시그널 : USR1
apachectl -k graceful

USR1 유닉스 시그널 혹은 {apachectl 명령행의 -k} graceful 인수를 통해 시그널을 받은 부모 프로세스는 자식 프로세스에게 현재 {처리하고 있는} 리퀘스트를 처리한 뒤에 종료하라고 지시한다. (혹은 자식 프로세스들이 아무것도 하지 않고 있는 상태라면 즉시 종료한다) 그리고나서 부모 프로세스는 설정 파일을 다시 읽고 로그 파일을 재오픈한다. 그 후 {처리 중인 리퀘스트가 있다면 종료에 지연이 있을 수 있는} 자식 프로세스가 종료될 때마다 부모 프로세스는 새로운 설정을 적용한 자식 프로세스를 새로 생성하여 종료된 자식 프로세스를 대체하여 새로운 리퀘스트를 처리하도록 한다.

 

위에서 { } 괄호 안의 부분은 graceful의 의미를 이해하기 쉽게 넣은 부분이다. 첫번째 부분인 Stop Now 부분은 즉시 프로세스를 죽이기 때문에 graceful하지 않은 방법이다. 따라서 처리 중인 모든 리퀘스트는 제대로 끝마쳐지지 못하고 취소될 수 있다. 만일 Graceful Stop이라면 반대로 최소한 처리중인 리퀘스트는 처리해주고, 종료할 것이다.

그에 비해 Graceful Restart 방법은 재시작을 지시하는 시점에서 자식 프로세스가 처리 중인 리퀘스트는 정상적으로 처리되고, 그 다음에 자식 프로세스가 종료되고, 새로운 자식 프로세스를 만들어서 옛날 자식 프로세스가 하던 일을 대체하게 된다. 즉 graceful하다는 의미에서 restart 자체가 유예될 수 있다는 것을 알 수 있다. 여기서 그 유예의 의미는 delay가 아니다. 매끄럽게 처리하기 위해 restart 자체가 유예될 수도 있다는 의미다. (약간 선택적인 의미라고 생각해야 할 듯 싶다)

여기서는 Apache 웹서버를 예로 들었지만 이 외에도 많은 IT 문서에서 graceful, gracefully용어가 쓰인다. 따라서 늬앙스를 알아두면 매뉴얼을 직독하면서 모호하게 해석되는 부분을 부드럽게 이해하는데 도움이 된다.

 

IT업계에는 이 외에도 deprecated라든지, kernel, shell, thread, signal, critical section, sliding window 라든지 일상적인 의미와 좀 다르게 쓰이는 단어들이 꽤 많다. 이걸 영어사전 의미 그대로 해석하면 문서 내용이 안드로메다로 가게 된다. 실제로 옛날에 김치하 교수와 같은 분들이 직독 번역을 좋아하셔서 kernel을 알맹이, shell을 껍데기, network를 그물, pipe를 대롱으로 거의 영어사전을 그대로 번역한 것 같이 쓴 적이 있다. 그런데 이렇게 영어 사전적 용어로 해석하면 "껍데기를 통해 OOO 명령을 내리면 알맹이가 보쌈을 만들어 처리한다" 뭐 이런 식으로 알수 없는 번역이 될 수 있다.

 

이 외에도 TCP/IP를 직접 핸들링해서 프로그래밍해야 하는 경우에는 복제된 FD의 linked channel 이라든지 SIGPIPE default behavior라든지 신경써야 하는 것들이 꽤 있는데, 매뉴얼에 생소한 용어들이 나올 수 있어서 용어 정립을 잘 해야 오해가 없어진다.[6] 

 

 

2. 문제의 근본 원인은 표절이다

이렇게 잘못된 용어의 번역과 개념이 끊임없이 번져나가는데는 표절이 하나의 원인이 아닐까 생각된다. 우리나라에서는 블로그를 쓰든 아니면 책을 쓰든지 인용을 적는 경우가 굉장히 드물다. 위의 "우아한 종료" 같은 실수는 수 많은 네트워크 서적이나 국내서에 단골 손님으로 등장한다.

그 이면에는 많은 책의 저자들이 인터넷을 검색하면서 원고를 작성하기 때문이기도 하다. 어떤 이는 KLDP의 내용을 통째로 긁어다가 책에 넣은 사람도 봤다.(더 슬픈건 긁어간 내용이 맞았다면 다행이겠지만 틀린 내용이었다.) 그런데 웃긴건 이렇게 잘못 쓰여진 책이 출판되어 많이 팔리면 그 책을 보고 어떤 이는 블로그에 정리하거나 게시판에 올려두고, 또 초보자인 누군가는 블로그 글을 읽고 다른 초보자에게 틀린 답변을 알려준다. 그 틀린 답변으로 공부한 초보는 시간이 지나 시니어가 되면 또 틀린 내용으로 책을 쓰고... 무한 반복이 된다.[4]

저널에 실리는 논문처럼 peer review가 없기 때문에 표절이 되면 오류가 정정될 가능성은 굉장히 적어진다.

하지만 처음에 글을 잘못 썼다고 해도 인용 레퍼런스를 제대로 넣었다면 중간에 누군가에게 검증받을 기회가 있었을 것이다. 허나 무분별하게 표절을 하면서 인용을 생략하면 원문을 알도리가 없다.

 

3. 내용 검증을 위해 꼭 인용을 표시하자

요새는 정치인을 하려고 해도 논문 표절, 심지어 자기 논문을 스스로 표절하는 자기표절도 걸리는 세상이다. 따라서 어떤 글을 적을 때 인용을 한다면 꼭 표시하자. 그래야 스스로도 올바른 내용을 썼는지 검증이 가능해진다.

하다못해 짧은 글을 쓰더라도 인용은 꼭 찾아서 표시해두자. 그래야 스스로 1차적으로 검증이 이뤄지고, 그 다음에 잘못 쓰였으면 원문까지 같이 검증받을 수 있게 된다. 꼼꼼하게 못하더라도 괜찮다. 처음부터 잘할 수는 없을테니... 그래도 인용을 찾아서 적어두는 연습을 하자.

 

 

[1] IEEE std. 1003.1 Functions "shutdown" https://pubs.opengroup.org/onlinepubs/9699919799/functions/shutdown.html

[2] IEEE std. 1003.1 Functions "close" 

https://pubs.opengroup.org/onlinepubs/9699919799/functions/close.html

[3] Kevin R. Fall, W.Richard Stevens. (1999). TCP/IP Illustrated, Volume 1 : Protocols (2nd Edition). p596

[4] 김선영, TCP의 TIME_WAIT를 없애는 법, https://sunyzero.tistory.com/198 

 

TCP의 TIME_WAIT를 없애는 법

* TCP의 TIME_WAIT는 없애는 방법은? TCP 소켓 네트워크 프로그래밍을 하다 보면 TIME_WAIT 상황에 대한 고민을 하는 시점이 오게 된다. 학부 시절 네트워크 프로그래밍 수업을 듣고 실습실에서 열심히 프로그래밍..

sunyzero.tistory.com

[5] Graceful shutdown, linger option, socket closure. https://docs.microsoft.com/ko-kr/windows/win32/winsock/graceful-shutdown-linger-options-and-socket-closure-2 

 

Graceful Shutdown, Linger Options, and Socket Closure - Win32 apps

Graceful Shutdown, Linger Options, and Socket Closure In this article --> The following material is provided as clarification for the subject of shutting down socket connections closing the sockets. It is important to distinguish the difference between shu

docs.microsoft.com

[6] TCP/IP 소켓 프로그래밍 주의할 점

 

* 히스토리

2020.08.16 아파치 웹서버의 graceful restart 예시 추가

2020.02.02 처음 씀

반응형
Comments