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

Contents

EventMachine

EventMachine는 Ruby, C++, Java에서 사용할 수 있는 네트워크 프로그래밍 라이브러리다. Reactor pattern을 이용 이벤트 드리븐 방식의 I/O를 지원한다. Eventmachine의 핵심 키워드는 다음과 같이 정리할 수 있다.
  • 높은 확장성과 성능 안정성을 가진 네트워크 애플리케이션 개발을 지원
  • 멀티 스레드 방식의 네트워크 프로그램은 복잡하다. EM을 이용하면 단일 스레드로 고성능 네트워크 프로그램을 개발할 수 있다. 개발방식이 단순해지는 덕분에, 개발자는 로직의 완성도를 높이는데 집중 할 수 있다.

왜 EM을 하려고 하는가 ?

이유는 이러하다.
  • 빠르게 네트워크 프로그램을 개발하고 싶다.
  • 이제 머리 아픈거 싫다. 단순하게 쉽게 쉽게 개발하고 싶다. 요즘은 event loop, 논 블러킹(이하 비봉쇄) I/O 기술을 활용 단일 스레드 기반으로 개발하는게 유행이기도 하고.
  • 예전 C로 개발을 할적에는 epoll로 한땀 한땀 삽질해가면서 프로그램을 만들었는데, 지금은 EM이라는 잘 만들어진 바퀴가 있다. 바퀴를 새로 만들 필요는 없지.
  • Ruby 언어를 사용하고 있는데, Ruby EM도 있더라.
  • 웹 애플리케이션 개발 환경으로 thin 웹서버와 sinatra를 사용하고 있다. thin 웹서버는 성능 때문에 사용 하고 있는데, 이게 EM 기반이다. 그래 예전부터 EM을 좀 살펴보고 싶었다.

비봉쇄 I/O 와 Event Loop

EM의 핵심 키워드인 비봉쇄 I/O, Event Loop에 대해서 정리해야 겠다.

요즘 유행

비봉쇄 I/O와 Event Loop(혹은 이와 유사한 다른 구조들)를 이용해서 단일 스레드로 작동하는 네트워크 프로그래밍 개발이 유행이다. "새로운 기술"이 아닌 "유행"이라고 한 이유는 실제 "새로운 기술"이 아니기 때문이다. 유닉스 시절로 거슬러 올라가자면 select 기반으로 수십년 전부터 사용해왔던 프로그래밍 개발 방식이고, 고성능 네트워크 프로그램 개발을 위한 epoll, /dev/poll, kqueue 등의 기술이 지원된 것도 10년이 넘어서고 있기 때문이다.

새삼 스럽게 유행이 된건, 아마도 node.js 때문인 걸로 보인다.

비봉쇄 I/O 정리

비봉쇄 I/O는 입력과 출력 함수를 호출하는 지점에서 "블럭 하지 않는"것을 의미한다. 예컨데 데이터를 읽기 위해서 read() 함수를 비봉쇄 방식으로 호출하면, 읽을 데이터가 없더라도 바로 리턴한다. 반대로 read() 함수를 블럭킹 방식으로 호출하면, 읽을 데이터가 있을 때까지 해당 영역에서 머무른다.

관련 내용은 비동기 입출력을 참고하자.

장점

하나의 스레드에서 여러 개의 입출력을 처리할 수 있다. 블럭킹 방식에서 두 개이상의 입출력을 동시에 처리하기 위해서는 멀티 스레드방식으로 코드를 만들어야 한다. 그런데 멀티 스레드 방식으로 제대로된(그리고 효율적인) 프로그램을 만드는게 쉬운 일이 아니다. 일단 사람의 인지 구조자체가 멀티 스레드 방식의 소프트웨어를 개발하는데 적합하지 않아서 코드의 흐름을 제대로 구현하기가 쉽지 않다. 경험있는 개발자라고 하더라도 공유자원 관리는 골머리 썩히는 작업이다.

비봉쇄 방식의 경우 입/출력에서 블럭되는 부분이 없기 때문에, 하나의 스레드로 두 개 이상의 입출력을 다룰 수 있다. 개발자는 하나의 스레드만 신경쓰면 되기 때문에, 데이터의 흐름을 쉽게 예측해서 코드를 개발할 수 있다. 그리고 멀티 스레드 프로그램에서 수반되는 컨텍스트 스위칭 과정이 없기 때문에, 그 만큼의 비용을 절약할 수 있다. 즉 같은 환경이라면 좀 더 안정적이고, 좀 더 효율적으로 작동하는 프로그램을 만들 수 있다.

주의할 점

비봉쇄 & 단일 스레드 방식의 코드를 만들 때 주의점을 살펴보려 한다.

단일 스레드는 하나의 흐름을 가진다. 따라서 I/O를 제외한 다른 모든 코드영역에서도
  1. 봉쇄 구간
  2. 봉쇄가 될만한 여지가 있는 구간
  3. 봉쇄는 아니지만 처리에 많은 시간이 걸리는 구간
이 있어서는 안된다. 1, 2의 경우라면 해당 봉쇄구간에서 프로그램이 멈춰버릴 테니, 서비스 중단 사태가 발생할 거고 3의 경우에는 서비스 응답시간이 길어질 거다.

예를들어 데이터 베이스에 접근하는 코드가 있다면, 이 구간에서 봉쇄 혹은 지연이 발생하지 않도록 조치를 취해야 한다. 이런 경우에 대비하기 위한 몇 가지 방법들이 있을 건데, 이들 방법들에 대해서 따로 살펴볼 생각이다.

Event Loop

Event Loop는 는 프로그램내에서 이벤트나 메시지를 전송하기 위해서 사용하는 프로그래밍 구조다. 외부 혹은 내부에 이벤트 공급자(event provider)가 있어서, 이벤트를 처리할 이벤트 핸들러(event handler)를 호출한다. 보통 이벤트가 외부에서 전달될 때까지, 특정 영역에서 대기한다. 만약 Event loop가 select나 poll을 이용해서 파일(소켓도 파일이다)을 다룬다면, 리액터(reactor)와 함께 사용할 수도 있다.

유닉스에서 파일을 다룰 경우의 event loop는 아래와 같이 구성할 수 있다.
main():     
    file_fd = open ("logfile")
    x_fd = open_display ()
    construct_interface ()
    while changed_fds = select ({file_fd, x_fd}):
        if file_fd in changed_fds:
            data = read_from (file_fd)
            append_to_display (data)
            send_repaint_message ()
        if x_fd in changed_fds:
            process_x_messages ()

Apache vs Nginx

Apache와 Nginx 웹서버를 예로 설명을 해보려한다.

아파치는 새로운 클라이언트 요청이 들어오면, 새로운 프로세스를 만든다. 이 프로세스는 클라이언트가 연결을 하고 있는 동안 계속 유지가 되는데, 따라서 네트워크 지연이나 클라이언트 프로그램의 처리 지연등이 발생할 경우, 서버는 오랜 시간동안 - 별로 할일이 없음에도 불구하고 - 프로세스를 실행하고 있어야 한다. 프로세스를 유지하는데에는 많은 RAM과 CPU 자원이 필요한데, 즉 제어할 수 없는 클라이언트가 전체 서비스 성능에 영향을 주는 문제가 발생할 수 있다. 실제 아파치 서버의 이런 특징은 denial-of-service 공격에 쉽게 노출되게 한다. 그냥 클라이언트가 요청을 하고 끊지 않은 정도로 아파치 프로세스를 수백개씩 띄워버릴 수 있다.

Nginx는 새로운 클라이언트가 들어오면, 새로운 소켓을 만드는 것 외에 다른 아무일도 하지 않는다. 하나의 프로세스로 모든 요청을 해결하는 방식이다. 하나의 프로세스로 여러 요청을 처리해야 하기 때문에, 프로세스는 봉쇄 영역을 가지지 않는다. 대신 소켓에 읽을 데이터나 쓸데이터가 있는지 확인해서 콜백 함수를 호출하는 방식으로 작동한다. 따라서 처리속도가 느린 클라이언트가 붙어도 서비스의 성능이 떨어지지 않는다. 또한 클라이언트의 수가 늘어나도 시스템 리소스 사용량에 큰 변화가 없다.

Ruby EventMachine 사용

설치

설치 환경은 아래와 같다.
  • 운영체제 : 우분투 리눅스 13.10 데스크탑 버전
  • Ruby 1.9.3p194
gem으로 설치 할 수 있다.
# gem install eventmachine
Building native extensions.  This could take a while...
Successfully installed eventmachine-1.0.3
1 gem installed
Installing ri documentation for eventmachine-1.0.3...
Installing RDoc documentation for eventmachine-1.0.3...

echo server

# coding : utf-8
require 'eventmachine'

module EchoServer
  def post_init
    port, ip = Socket.unpack_sockaddr_in(get_peername)
    puts "새 클라이언트 연결 :: #{ip}:#{port}"
  end

  # 읽은 데이터를 그대로 전송한다.
  # 클라이언트가 "quit"를 전송하면 연결을 끊는다.
  def receive_data data
    send_data "#{data}"
    close_connection if data =~ /quit/i
  end

  def unbind
    puts "클라이언트 연결 종료"
  end
end

EventMachine.run {
  EventMachine.start_server "0.0.0.0", 8080, EchoServer
}

http server