Recommanded Free YOUTUBE Lecture: <% selectedImage[1] %>

Contents

  • 이 문서는 일부 2.6의 내용을 포함하고 있지만 전체적으로 커널 2.4를 기준으로 작성되었다. 최신 커널의 변경사항을 조사해서 수정할 필요가 있다.

소개

몇번에 걸쳐서 RTS를 다루었는데 너무 피상적인 내용만 다룬것 같다. 아무래도 제대로 사용하기 위해서는 실제로 서비스 가능한 간단한 인터넷 서비스 프로그램이라도 개발해야 할것 같다.

그러기 위해서는 몇가지 해결해야될 문제들이 있으며, 다른 이벤트 전달 방법인 select(), /dev/poll등과 비교해서 어떤 잇점을 가지고 있는지도 확실히 짚고 넘어가야 한다.

이와 관련된 문서를 찾던중 HP 연구소의 훌륭한 문서를 찾게 되었고 이 문서를 이용해서 연구를 하고 결과를 이용한 응용 애플리케이션을 작성하기로 결정했다.

개요

인테네트 서비스들의 문제점은 무엇일까 ?. 아마도 많은 양의 데이터를 처리때문에 골치아플 거라고 생각될 수 있게지만, 실제 겪는 진정한 골치거리는 다수의 연결을 처리해야 한다는 점이다. 보통 이러한 연결에 있어서 통신에 사용되는 데이터 자체의 크기는 그리 많지 않은 반면 요청과 응답이 매우 빈번하게 이루어 지는데, 서버측에서 보자면 이러한 요청을 빠른시간에 받을 수 있어야 하며, 요청을 누가했는지 최대한 효율적으로 판단해서 요청에 대한 응답을 보내줄 수 있어야 한다.

이 글에서 우리는 인터네트 서버에서 다수의 요청에 의한 네트워크 I/O 이벤트를 효과적으로 처리하기 위한 몇가지 방법에 대해서 알아볼 것이다. 아마도 이 사이트에서 몇번 다루어본적이 있는 (비교적 최근의 이벤트 처리기술인)RTS를 위주로 설명할 것이며, RTS가 정말로 효율적으로 이벤트를 처리할 수 있는지를 확인하기 위해서 고전적인 이벤트 처리 방식인 select()와 /dev/poll과의 성능 테스트도 함께 하게 될 것이다.

또한 RTS와 같은 레벨에서 /dev/epoll도 다루고 서로 비교할 것이다. /dev/epoll은 최근에 나온 이벤트 처리기술로 매우 좋은 성능을 보여준다고 알려져 있다. /dev/epoll에 대한 내용은 [gururang]님의 [epoll]위키를 많이 참고하게 될것이다.

RTS를 다룸에 있어서는 중복된 내용은 가급적 피하고, 몇번 언급되었지만 깊이 다루지 않은 signal-per-fd에 대한 내용을 꽤 심도 있게 다루고 실제 적용후 예제코드를 만들어 보도록 할것이다. signal-per-fd를 사용할경우 시그널을 제어하기 위한 여러가지 복잡한 내용들에 대해 신경쓰지 않게 되므로 좀더 코드에 집중할 수 있으며, 좀더 안전한 프로그램을 만들 수 있게 된다.

소개

이 문서의 제목이 네트워크 I/O이벤트 처리에 관한 연구이다. 왜 이러한 주제를 다루게 되었는지 인터네트와 인터네트에 관련된 기술동향들에 대해서 알아보도록 하겠다.

웹과 e-commerce가 빠른 속도로 발전하고 있으며, 이로인해 인터네트의 트래픽 역시 가파르게 증가하고 있다. 웹서비스와 관련된 네트워크 애플리케이션과 프록시(proxy)들은 전세계에서 들어오는 클라이언트의 요청을 처리할 수 있어야만 한다. 거기에다 가 서버들은 거의 동시에 발생하는 수많은 연결을 가능한 빠른시간에 처리할 수 있어야 한다. 만약 연결처리가 늦어지게 된다면 클라이언트는 서비스의 이용을 위해서 많은 시간을 기다려야 할 것이고 고객의 인내심을 벗어나게 될경우 분명히 고객을 잃어 버리게 될것이다. 특히 다른 것들(단지 데이터를 통신하는것)에 비해 연결은 매우 많은 시간을 소비한다. 고로 가능하면 빠르게 클라이언트의 연결을 감지하고 이를 처리해 낼 수 있어야 한다.

가장 인기 있는 인터네트 서비스인 웹서비스를 예로 들어보자. 웹서비스의 경우 동시에 여러개의 네트워크 I/O를 처리하기 위해서 운영체제에서 제공하는 이벤트 전달(devent-dispatch) 방식을 사용한다. 아직까지는 많은 서버들이 고전적인 이벤트 전달 방식들을 사용하는데 이들 이벤트 전달방식은 오래된 만큼 그리 효율적이지 못하다는 단점을 가진다. 아파치 웹서버의 경우 일반적인 서비스를 운영하는데에는 문제가 없지만 조금더 규모가 커지면 연결증가에 따른 많은 문제가 발생한다. 연결이 느려지거나 아예 연결시도 자체가 실패하는 경우가 대표적인 경우인데, 이러한 문제의 해결을 위해서는 웹서버 튜닝과 하드웨어 증설등에 많은 시간고 비용을 투자하게 된다.

이러한 문제를 해결하기 위해서 TUX와 같이 아예 커널 공간으로 서비스 기능을 올려서 성능을 극적으로 향상시켜 버리는 제품들도 있다. 그러나 여기에서는 이러한 것들은 다루지 않을 것이다. 이벤트 전달방식을 사용해서 이러한 문제를 해결하는데 집중하도록 하겠다.

앞으로의 장에서 우리는 select()시스템콜과 /dev/poll 인터페이스 그리고 POSIx.4 Real Time Signal(이하 RTS)의 이벤트 전달 기법들에 대해서 다룰 것이다.

이벤트 전달 방식

이번장에서 우리는 다중연결을 처리하는 서버들의 두가지 유형에 대해서 알아볼 것이다. 그리고 리눅스 커널에서 지원하는 여러가지 이벤트 전달방식들을 알아볼 것이다. 이러한 내용들은 이미 이사이트에서 여러번에 걸쳐서 다루어 졌으므로 주로 각각의 방식의 효율성과 단점, 어떤 것을 선택해야 하는지 등에 촛점을 맞출 것이다.

다중 연결의 처리

네트워크상에서 다중 연결을 처리하는 데는 크게 2가지 방법이 존재한다.
  • 쓰레드 기반 : 다중연결을 처리하기 위한 방법중 하나로 메인 쓰레드에서 accept를 이용해서 연결을 기다리고 있다가 새로운 연결소켓이 만들어지면 연결소켓만을 처리하는 쓰레드를 생성해서 쓰레드당 하나의 클라이언트를 처리하도록 하는 방식이다. 여기에는 쓰레드의 생성시간에 따라서 두가지 다른 방식이 존재한다.
    • 요청이 있을 때마다(on-demand)) : accept에 의해서 새로운 연결이 들어올 때마다 연결을 처리할 쓰레드를 생성하는 방식이다. 매우 직관적이고 코딩이 편하긴 하지만 쓰레드를 생성할 때 많은 비용을 지불한다는 단점을 가진다. 연결이 빈번하게 이루어지는 서버일 경우 문제가 될 수 있다.
    • 쓰레드 풀 방식 : on-demand방식의 단점을 극복하기 위해서 사용되는 방법으로 미리 연결을 처리할 쓰레드를 일정 갯수 생성시켜 놓는 방식이다. 메인 쓰레드는 새로운 연결이 만들어 졌을 때 연결 처리를 위한 새로운 쓰레드를 생성시키는 대신 이미 만들어진 쓰레드에 연결을 위임하는 방식을 사용한다. 이 방법을 이용하면 쓰레드 생성시 발생하는 오버헤드를 상당히 해소할 수 있다. on-demand방식에 비해서 구현이 좀더 복잡하고 연결이 적은 서버의 경우 오히려 자원을 낭비할 수 있다는 단점을 가진다.
  • 이벤트 기반 : 이벤트 기반 어플리케이션은 하나의 쓰레드에서 비봉쇄(non-blocking) I/O방식으로 여러개의 연결을 처리한다. 운영체제는 하나 혹은 여러개의 연결로 부터 이벤트가 발생하면 이를 어플리케이션으로 통보하고 어플리케이션은 통보된 정보를 바탕으로 이벤트 발생한 파일(소켓)을 이용해서 통신을 한다. 어플리케이션에 이벤트를 통보하기 위해서 운영체제는 만들어진 연결에 대한 파일지정자에 대한 리스트를 유지하고 있어야 한다. 운영체제는 이 파일지정자 리스트의 각 지정자에 이벤트가 발생하는지를 확인을 해서 이벤트가 발생하면 애플리케이션으로 이벤트를 전달한다.
일반적으로 연결당 쓰레드(thread-per-connection)를 생성하는 서버는 많은 쓰레드가 생성될 경우 문백교환(context switching)을 위한 상당한 오버헤드가 발생할 수 있다는 단점을 가진다. 또한 각각의 쓰레드는 자신만의 스택공간과 연결처리를 위한 별도의 루틴을 가져야 하기 때문에 메모리 공간역시 많이 차지한다는 문제점도 가지게 된다. 만약 운영체제가 커널레벨 쓰레드를 지원하지 않는다면 이러한 단점은 더욱 크게 부각될 것이다. 리눅스가 이러한 경우에 해당되는데 clone()를 통한 프로세스 방식의 쓰레드를 사용하게 되므로 다른 운영체제 보다 더욱 과도한 오버헤드와 메모리 소비를 가지게 된다. 어쨋든 간에 쓰레드(혹은 프로세스)기반의 서버는 많은 연결을 처리하는데 있어서 근본적인 문제를 가지게 된다.

위의 이유로 많은 운영체제들이 이벤트 기반처리 방식을 제공하고 있다. 물론 이벤트 기반 처리를 도입하려는 서버는 대부분 매우 바쁘게 작동할 것이라고 예상하고 작성되므로 이벤트 처리방식과 함께 다중 쓰레드 기법까지 함께 사용하게 된다. 이러한 서버들에서 어떠한 이벤트 처리방식을 사용할 것인가 하는 것은 매우 중요한 문제다. 여러 종류의 이벤트 처리방식이 존재하는데 각각 용도와 성능에 있어서 차이가 있기 때문이다. 다음장에서는 여기에 대해서 집중적으로 알아보도록 하겠다.

리눅스 커널 매커니즘

위에서 이벤트 기반 서버는 네트워크상의 입출력(I/O)의 처리를 위해서 이벤트 전달기법을 사용하고 있다. 이번 장에서는 리눅스 커널에서 애플리케이션에 이번트를 통지하기 위해서 사용되는 몇가지 방법들에 대해서 알아보도록 하겠다. 다음은 리눅스 커널에서 지원하는 이벤트 전달 방법들이다.

select() 시스템 콜

select()는 단일 쓰레드나 프로세스에서 열려있는 여러개의 연결을 다중화 시켜서 처리할수 있도록 허용한다. select()는 fdset을 가진다. 이 fdset은 1024크기의 bit테이블로써 여기에 관심있어하는 파일의 목록을 등록시킨다. 만약 등록시킨 파일에서 입출력이 발생하면 select()는 리턴하며 이때 fdset의 bit테이블을 설정한다. 만약 4번 파일지정자에 읽기 이벤트가 발생했다면 fdset[3]의 값을 1로 해서 넘겨주는 방식이다.

다음은 select()의 특징적인 작동이다.
  1. 애플리케이션은 관심있어하는 파일지정자의 목록(fdset)을 커널에 전달한다.
  2. 관심있는 파일지정자에 대한 정보는 fdset에 드문드문 설정될 것이다. 이 정보는 유저레벨에서 커널레벨로 복사된다.
  3. 커널은 fdset을 모두 뒤지면서 관심있는 파일지정자를 찾아내고 이 파일 지정자에 이벤트(읽기/쓰기 데이터가 있는지)가 발생했는지를 검사한다. 눈치챘겠지만 이것은 꽤나 비용이 드는 작업이다.
  4. 이 fdset은 커널 모드에서 다시 유저모드로 복사되고 select()는 리턴된다.

poll() 시스템 콜

poll()은 내부적으로 select()를 사용한다. 어찌보면 select()와 전혀 동일하다고 생각할 수도 있다. 그렇지만 인터페이스에 있어서 약간의 차이점을 보인다. poll()은 관심있는 파일지정자를 유지하기 위해서 fdset을 이용하는 대신에 pollfd 구조체를 사용한다. 만약 pollfd구조체에서 유지하고 있는 파일에 데이터가 준비되어 있다면 이 구조체를 리턴하게 된다.

poll()이 내부적으로 select()를 사용하고 있다는 것 때문에 언뜻 생각하기에 언제나 select()가 더 효율적일 거라고 생각하지만 그렇지않다. select()는 poll()에 비해서 매우 넓은 fdset범위에서 파일지정자들을 검색해야 하기 때문이다.

POSIX. 4 Real Time Signals

RTS는 Unix에서 이벤트 통지를 위해 사용하는 시그널을 확장시킨 형태로 시그널을 객체로 다루며 동시에 시그널의 대기열(queue)를 유지한다. 기존의 시그널은 시그널을 비트를 세팅하는 것으로 신고하가 전달 되었는지의 유무를 관리한다. 그러므로 동일한 시그널이 빠른 시간에(핸들러가 종료하기 전에)여러번 발생하게 된다면 하나를 제외하고 모든 시그널을 잃어 버리게 된다.

더불어 시그널에 대해서 비트 설정만이 아닌 sigino를 전달하는데, 이를 통해서 여러가지 정보들까지 함께 전달 가능하다.

다음은 RTS를 이용해서 파일로 부터 이벤트를 통지 받기 위한 일반적인 코드이다. RTS응용에 대해서는 이미 여러번 다룬적이 있으므로 설명은 하지 않도록 하겠다.

이벤트 통지에서의 효율성

이번장에서는 각각의 이벤트 통지방법이 어느정도의 효율성을 가지고 있는지 알아보도록 하겠다. 참고로 이 테스트의 결과는 필자의 테스트 결과가 아닌 HP 연구소에의 Scalability of Linux Event-Dispatch Mechanisms문서의 내용을 발췌한 것임을 박힌다.

이 테스트를 위해서 (HPL)은 uservers라는 조그마한 웹서버를 만들어서 테스트 했다. 물론 테스트를 위해서 각 이벤트 통지방식을 적용한 여러개의 웹서버가 만들어 졌다. 테스트는 2개의 400MHz 팬티엄III CPU를 장착한 시스템에서 이루어졌다. 커널은 2.4.0-test7버젼이다. 현재 2.4.23안정버젼까지 나오고 2.6.0-13테스트 버젼까지 나온 상태에서 현실에 약간 동떨어진 구식의 커널을 사용하긴 했지만 각 이벤트 통지 방식간 성능 차이를 비교하는데는 별 문제가 없을 것이다. - 시간이 된다면 직접 최신의 환경을 만들어서 테스트하도록 하겠다 -

테스트 서버가 준비되었으니 테스트 클라이언트도 준비되어야 할것이다. 역시 전용의 테스트용 클라이언트가 준비되었다. 테스트용 클라이언트는 HP-UX10.20을 탑제한 B180 RA-RISC기계에서 작동을 한다. 서버와 클라이언트는 100Mbps 패스트 이더넷 스위치로 연결된다. 테스트는 많은 수의 연결을 만들었을 때 각각의 서버가 얼마나 빠르게 반응하는지 어느정도의 CPU자원을 사용하는지를 확인하는 방식으로 이루어진다.

다음은 테스트 결과다.

  • 동시 연결요청에 대한 응답율
  • 동시 연결요청이 이루어졌을 때의 CPU사용율
  • 동시 연결요청에 대한 응답반응 시간
  • 256idle 연결에 대한 성능비교
  • 6000 idle 연결에 대한 성능비교
  • 로드증가에 대한 응답시간
성능의 차이를 한번에 알아 볼수 있을 것이다.

RTS의 단점/해결방법

RTS가 매우 효율적이긴 하지만 몇 가지 단점들을 가지고 있다. 이번 장에서는 이러한 RTS의 단점과 이에 대한 해결방법에 대해서 알아보도록 하겠다.

Linux에서의 Signal Queue 크기

시그널 큐의 크기는 프로세스당 제한되어 있으며, 크기는 /proc/sys/kernel/rtsig-max에 정의되어 있다. 아마 1024로 설정되어 있을 것이다. 물론 필요에 따라서 간단하게 변경 가능하다.
# echo 2048 > rtsig-max 
서버의 용도에 따라서 적당한 값을 이용하도록 하자.

Siganl queue Overflow 문제

이러한 단점은 시그널 대기열의 크기가 제한되어 있기 때문에 발생하는 문제들이다. 소켓에서 발생한 이벤트는 시그널의 대기열에 쌓인다. 쌓인 이벤트들은 sigwaitinfo()를 통해서 가져옮으로써 대기열에서 지워지게 된다. 그런데 특정 시간대에 서버가 매우 바뻐져서 시그널 대기열을 비우는 속도를 훨씬 초과해서 이벤트가 쌓이고 결국 시그널 대기열이 모두 차버리는 문제가 발생할 수도 있을 것이다.

시그널 큐 오버플로는 데드락(deadlock -교착상태)상태를 만들 수 있다. 또한 대기열이 꽉차게 될경우 당연히 이후에 발생하는 어떠한 시그널도 대기열에 쌓이지 못하고 버려지게 된다.

이러한 문제를 피하기 위해서 리눅스 커널은 시그널 큐 오버플로어가 발생하면 애플리케이션으로 SIGIO를 발생시킨다. 만약 RTS를 사용중 SIGIO를 통지 받았다면 애플리케이션에서 시그널 큐 오버플로어 문제를 해결해야 한다. 불행하게도 RTS에서의 시그널큐 오버플로어의 처리는 애플리케이션을 꽤나 복잡하게 만든다.

Signal-per-fd의 사용

위에서 RTS의 가장 큰 단점인 시그널 큐 오버플로어에 대해서 알아보았다. 이것은 애플리케이션의 작성을 매우 복잡하게 만든다. 그렇다면 가장 바람직한 방법은 시그널 큐 오버플로어 상황이 아예 발생하지 않도록 하는 것이다.

시그널 큐 오버플로어가 발생하게 되는 이유는 각각의 연결당 여러개의 이벤트를 받아들일 수 있다는 데에서 발생한다. 지금 4,5,6,7,...,100 의 연결이 만들어져 있다고 가정을 해보자. 이때 각각의 소켓은 제한 없이 이벤트를 받아들이게 되고 그러다 보니 이벤트의 총합이 시그널 대기열의 크기를 벗어나는 문제가 발생한다. 그렇다면 각각의 연결에 대해서 단지 하나의 시그널만 유지하도록 만든다면 시그널 큐 오버플로어 문제를 간단하게 회피할 수 있을 것이다. 그렇다면 1000개의 여결이 있다고 하더라도 최대 1000개의 이벤트만이 시그널 큐에 쌓일 수 있기 때문에 절대 시그널 큐 오버플로어가 발생할 수 없을 것이다. - 참고로 시그널 큐의 크기는 열수 있는 파일의 크기와 같다. -

이렇게 파일지정자당 하나의 시그널만 사용할 수 있게 하면 시그널 큐 오버플로어 문제를 해결가능하긴 하는데, 이렇게 될경우 성능에 있어서 희생을 가져오지 않는가 하는 의문이 발생할 수 있을 것이다. 하나의 소켓이 동시에 여러개의 이벤트를 처리할 수 없다는 것을 의미하기 때문이다. 그러나 이 문제는 그리 걱정할 상황이 되지는 않는다. 보통 하나의 소켓은 하나의 클라이언트와의 연결인데, 하나의 클라이언트에서 동시에(매우 짧은시간에) 여러개의 이벤트가 발생하는 경우는 필요하지 않기 때문이다. 일반적으로 클라이언트와 서버간에 일대일 연결이 맺어졌다면 클라이언트에서 요청을 보내고 서버가 응답을 하면 다시 클라이언트가 요청을 하는 방식이기 때문이다. 매우 바쁜 웹서버라고 할지라도 단일 클라이언트와 서버의 관점(소켓관점)에서 본다면 하나의 소켓에 대해서는 한번에 단지 하나의 이벤트만 발생한다.

결론적으로 Signal-per-fd를 사용하게 될경우 서버성능에 큰 희생 없이 시그널 큐 오버플로어를 피해나갈 수 있다.

다음은 signal-per-fd를 사용할 때 얻을 수 있는 장점들이다.
  1. 시그널 큐 오버플로어를 회피할 수 있으므로 프로그램이 구조적으로 단순해 진다.
  2. 또한 시그널 큐 자원을 안전하게 사용할 수 있도록 보장한다.
  3. 하나의 사건에 대해서 여러개의 이벤트가 발생할 수도 있는데, signal-per-fd를 이용하므로써 애플리케이션에게 잘정의된 하나의 이벤트만 받을 수 있도록 한다.

2.4.x에서의 signal-per-fd 커널 패치

이제 실제 2.4.x커널에 signal-per-fd 패치를 하고, 이를 통해서 간단한 응용 프로그램을 만들어서 테스트 해 보도록 하겠다.

kernel 다운로드 및 패치 하기

signal-per-fd가 비교적 최근에 제공된 기술인 관계로 모든 리눅스 커널에 대한 패치가 존재하지 않는다. 설사 존재한다고 하더라도 찾기 어려운 경우가 많다. 그래서 가장 얻기 쉬운 패치를 기준으로 커널을 다운로드 받아서 패치후 컴파일 하기로 했다.

google에서 signal-per-fd patch로 찾은 결과 커널 2.4.13에 대한 signal-per-fd커널 패치를 쉽게 찾을 수 있었다. 커널 2.4.13는 http://www.kernel.org에서 받으면된다. signal-per-signal패치는 one-sig-perfd-2.4.13.pat를 다운로드 받으면 된다.

다운로드 받은 커널소스는 /usr/src/linux-2.4.13에 푼다. 그후 패치파일을 linux-2.4.13디렉토리로 이동한후 다음과 같이 패치를 하도록한다. {{{ # patch -p 1 < one-sig-perfd-241.pat ... }}} 위의 패치파일을 보면 다음과 같은 내용을 발견할 수 있다. 위의 코드를 보면 f_auxflags의 설정 여부에 따라서 signal-pre-fd의 적용여부가 결정됨을 확인 할 수 있다. 실제 코드 상에서는 아래처럼 fcntl을 이용해서 해서 signal-per-fd를 적용시킨다.
fcntl(sockfd, F_SETAUXFL, O_ONESIGFD);

패치를 끝냈다면 이제 커널컴파일을 하도록 한다. 커널 컴파일 방법은 여기에서 언급하지 않도록 하겠다.

2.6에서의 signal-per-fd

2.6.x커널에서 signal-pre-fd의 지원을 확인 하기 위해서 kernel/signal.c파일을 확인해 보았다. 정확한 커널버젼은 2.6.0-test11이였으며, 다음과 같은 내용을 확인 할 수 있었다.

  • 아래 코드에 대해선 해석이 필요함..

간단한 셈플 프로그램

  1. signal-per-fd를 적용한 간단한 프로그램을 만들고 이에 대한 테스트를 실시한다.

프로젝트 진행

  1. signal-per-fd 커널 패치가 적용된 리눅스에서의 RTS웹서버 제작 프로젝트를 수행한다. 실질적인 프로젝트로 발전시키도록 한다.

참고 문헌

  1. Real Time Signal - 1
  2. Real Time Signal -2
  3. RTS와 쓰레드풀과의 조합
  4. Scalability of Linux Event-Dispatch Mechanisms
  5. Scalability of Linux Event-Dispatch Mechanisms (ps)
  6. 구루랑님의 [epoll] 연구 위키
  7. http://www.kegel.com/c10k.html