Pages

February 23, 2014

[Node.js] Node.js의 이벤트 루프 이해하기

아래 글은 이곳(http://blog.mixu.net/2011/02/01/understanding-the-node-js-event-loop/)의 글을 번역한 글임을 밝힙니다. 그리고 이 글의 내용은 node.js의 기본적인 내용을 어느 정도 숙지하고 있다는 전제하에 쓰여진 글입니다.


node.js에서 가장 기본이 되는 전제는 I/O에 드는 비용이 비싸다는 것이다. (아래의 표는 각 I/O를 처리하는데에 CPU에서 얼마만큼의 cycle이 소요되는지를 나타낸 표이다.)


현재의 프로그래밍 기술에서 가장 큰 손실은 I/O가 끝나기를 기다리는 데서 온다.  이렇게 퍼포먼스에 영향을 미치는 것을 처리 하기 위한 몇가지 방법이 있다.

  • 동기적 해결 : 하나의 요청을 한번에 처리하기. 장점: 단순하다. 단점: 어떤 요청이 끝나기를 다른 요청은 기다려야만 한다.
  • 새로운 프로세스를 만들기 : 각 요청을 처리하기 위해 매번 새로운 프로세스를 만들어 해결한다. 장점: 쉽다 단점: 확장성이 떨어진다. 만약 수백개의 연결이 만들어 진다면 수백개의 프로세스가 만들어 지는 것을 의미한다. 이것은 문제를 해결하지만 과도한 방법이다.
  • 스레드: 각 요청을 처리하기위해 스레드를 만든다. 장점:쉽다, 프로세스를 만드는 것보다는 OS커널에 친절하고 비용이 적게 드는 방법이다.  단점: 스레드 프로그래밍은 아주 복잡해지기 쉽다. 특히 공유되는 자원에 접근하게 되는 경우에 특히 그렇다.
각 연결당 하나의 스레드를 만드는 것은 메모리를 과도하게 소비하게 된다. 
아파치는 멀티스레드 프로그램이다. 각 요청당 스레드를 하나씩 생성한다. (설정에 따라 프로세스를 fork하기도 한다.) 동시 접속이 늘어 남에 따라 생성되는 스레드가 얼마나 많은 메모리를 소비하는지 볼 수 있다.  이러한 단점을 극복하기 위해 Nginx와 Node.js는 멀티 스레드 프로그램으로 설계되지 않았다. 이것들은 싱글 스레드 프로그램이지만 이벤트에 기반한 프로그램이다. 싱글스레드 설계로 수천개의 스레드 혹은 프로세스가 생성될때 생기는 과부하를 제거하였다.  

Node.js는 당신의 코드를 싱글 스레드로 유지한다.


Node.js는 정말로 싱글스레드로 동작한다. 동시에 실행되는 코드를 만들 수 없다. 1초 동안의 "sleep" 코드는 정말로 서버를 1초동안 중단시킨다.

while(new Date().getTime() < now + 1000) {
   // do nothing
}


node.js에서는 당신의 코드를 실행하는 스레드가 정말로 하나이기 때문에 따라서 위의 코드가 동작하는 동안은 다른 어떠한 요청에도 node.js는 응답하지 않는다. 혹은 CPU자원을 많이 쓰는 코드(예를 들어 이미 리사이징같은..)를 동작하는 동안에도 해당 코드가 실행이 끝날때까지 다른 요청을 처리하지 못하게 된다.

그러나 당신의 코드만 빼고 다른 모든 것은 병렬적으로 실행된다.


하나의 요청을 처리하는 코드내에서 어떤 코드도 다른 요청에 대한 처리와 함께 병렬적으로 실행되도록 하는 방법은 없다. 그러나 모든 I/O 작업은 이벤트지향적이며 비동기적으로 동작한다.  따라서 다음의 코드는 서버의 동작을 멈추지 않는다.


c.query(
   'SELECT SLEEP(20);',
   function (err, results, fields) {
     if (err) {
       throw err;
     }
     res.writeHead(200, {'Content-Type': 'text/html'});
     res.end('Hello

Return from async DB query

'); c.end(); } );


당신이 위의 코드를 어떤 하나의 요청처리 중에 포함시킨다면 해당 코드의 데이터베이스 작업 (즉 20초간 sleep하기)이 끝날때까지 다른 요청 들이 정상적으로 처리가 된다.

왜 이것이 좋은가? 그리고 언제 우리의 코드는 비동기적/병렬 처리를 하게 되는가?

동기적 처리도 코드의 단순성에서 보았을 때 나쁘지 않다. (스레드를 사용하는 코드에 비교할때, 가끔은 스레드의 동시성은 우리를 미치게 만든다.)

Node.js를 사용할때 당신은 뒷단에서 무슨 일이 일어나는지 걱정할 필요없이 단지 I/O작업이 필요할때 I/O작업이 끝난 후 처리될 callback만 넘겨 주면 된다. 그러면 당신의 I/O작업이 포함된 코드는 새로운 스레드나 프로세스의 생성할 필요 없이 다른 요청이 처리되는 것을 막지도 않으면서 처리됨을 보장받을 수 있다. 

I/O작업은 보통 대부분의 다른 어떤 코드보다 비용이 비싼 작업으로 I/O작업이 끝나기를 기다리기 보다는 무엇인가 의미있는 일을 수행해야만 한다. 따라서 이런 경우 비동기적 I/O처리는 매우 유용하다.


이벤트 루프란 코드 외부의 이벤트들을 처리하고 그것의 결과를 callback으로 전달하는 객체로 정의할 수 있다. 어떤 한 요청 처리 도중 I/O콜이 일어나서 기다려야 하는 순간 node.js는 다른 요청을 처리하게 된다. 이 I/O콜이 일어날 때 코드에서는 callback을 등록하고 컨트롤을 node.js 의 runtime environment로 넘기게 된다. 그리고 callback은 I/O콜이 완료된 순간 그 결과를 전달 받아 호출되게 된다.

따라서 당연히도 뒷단에서는 스레드들과 프로세스들이 DB 접속과 프로세스 실행을 위해 준비되어 있다. 그러한 스레드와 프로세스는 당신의 코드와는 관계 없이 실행되고 있고 당신의 코드를 만들면서 신경쓸 필요가 없다. 단지 한 요청내에서의 I/O작업들은 비동기적으로 수행된다는 것만 생각하면 된다. node.js의 뒷단에서 여러 스레드와 프로세스들이 I/O처리를 완료후 이벤트 루프를 통해 당신의 코드로 결과를 전달 받게 된다. 정말 병렬 작업이 필요한 경우에만 스레드 혹은 프로세스가 생성이 되고 그것 조차도 node.js가 관리를 하게 되므로 아파치 서버 모델과 비교할 떄 훨씬 적은 수의 스레드와 프로세스만이 동작하게 된다.

I/O콜을 제외하고 node.js는 모든 요청이 굉장히 빨리 처리 될 것을 가정하고 만들어졌다. 따라서 CPU를 많이 사용하게 되어서 느리게 처리되는 작업이 있다면 WebWorkers를 이용하거나 이벤트를 통해 실행과 결과를 주고 받을 수 있는 다른 프로세스로 분리가 되어야 한다. 이는 당신의 코드는 이벤트롤 통해 상호작용할 수 있는 스레드가 백그라운드에서 돌지 않으면 절대로 병렬화 될 수 없음을 의미한다. 기본적으로 모든 이벤트를 방출하는 오브젝트는 비동기적 이벤트 상호작용을 지원하며 당신은 블록된 코드와 이러한 방식으로 상호작용 할 수 있다. 예를 들어 파일을 사용하거나 소켓, 자식 프로세스 이런 것들은 모두 node.js에서 EventEmitters이다. 멀티코어의 사용도 이러한 방식으로 접근할 수 있다. node-http-proxy도 한번 보길 바란다.

내부 구현


내부적으로는 node.js는 libev를 통해 이벤트 루프를 구현한다. 그리고 libeio를 통해 스레드 풀을 통해 비동기적 I/O를 달성할 수 있도록 보조하고 있다. 더 알고 싶으면 libev documentation을 참고하기를 바란다.

그래서 Node.js에서 어떻게 async를 구현하지?

Tim Caswell의 멋진 프리젠테이션을 통해 패턴을 알아 보자.
  • First-class functions : function이 데이터 처럼 전달될 수 있으며 필요한 경우 실행될 수 도 있다.
  • Function composition :  I/O작업중 어떤 이벤트가 발생했을 때 실행될 수 있는 이름이 없는 함수들과 클로져를 사용한다.
  • Callback counters : I/O 이벤트가 어떤 순서로 발생할 지 알 수 없다. 따라서 만일 여러개의 쿼리가 완료되어야 한다면 일반적으로 동시에 실행되는 I/O 작업의 숫자를 세고 필요한 모든 I/O작업이 완료되었는지 기다린다. 즉 예를 들어 DB쿼리가 몇개나 리턴이 되었는지를 이벤트 콜백에서 세어서 모든 데이터를 가지게 된 순간 더 진행하도록 한다. I/O라리브러리가 지원하기만 한다면 여러개의 쿼리는 동시에 실행되게 된다. (Connection pooling등을 이용하여.)
  • 이벤트 루프 : 앞에서 언급했듯이 blocking 코드를 예를 들어 자식 프로세스를 실행하여 처리후 결과를 받는 등의 이벤트 추상화로 감쌀 수 있다. 
정말로 쉽죠?




No comments:

Post a Comment