반응형
본문은 Node.js Design Patterns - Mario Casciaro 를 번역/정리한 글입니다.

Blocking I/O

- 기존의 Blocking I/O를 웹서버에 사용하게 되면 동시에 여러 접속을 처리하기 위해 각각의 커넥션을 별도의 쓰레드로 분리를 하여야한다. 이렇게 각가의 쓰레드가 하나의 커넥션만을 처리하게 되면 실제로 일을 하는 시간보다 기다리는 시간이 훨씬 많아지게 된다. 그리고 쓰레드를 여러개 사용하는것은 메모리를 많이 잡아먹고 Context Switch를 발생시키므로 효율적이지 않다.

 

Non Blocking I/O

- Non-blocking I/O에서는 system call이 데이터를 읽고 쓰는것을 기다리지 않고 만약 그시점에 처리할 것이 없다면 바로 반환해버린다.

 

Busy Waiting

- Non-blocking I/O에서는 여러 커넥션을 병렬적으로 처리하기 위한 방법 중 가장 간단한 방법으로 리소스를 polling(수시로 체크)하는 busy-waiting이라는 방법이 있다. 아래의 pseudo 코드 방식으로 작동한다:

resources = [socketA, socketB, socketC];
while(!resources.isEmpty()) {
	for(i=0;i < resources.length; i++){
    	resource = resources[i]
        var data = resource.read();
        if (data == NO_DATA_AVAILABLE)
        	continue;
        if (data == RESOURCE_CLOSED)
        	resources.remove(i);
        else
        	consumeData(data)
    }
}

이런방식을 사용하면 동시에 여러개의 접속을 하나의 쓰레드에서 처리할 수 있다. 하지만 이러한 Polling 알고리즘들은 CPU를 너무 낭비한다.

 

Synchronous Event Demultiplexer

- 이를 효율적으로 해결한 방법이 Synchronous Event Demultiplexer 또는 Event Notification Interface이다.

socketA, pipeB;
watchedList.add(socketA, FOR_READ)
watchedList.add(pipeB, FOR_READ)
while( events = demultiplexer.watch(watchedList)){
	foreach(event in events){
		data = event.resource.read()
        if(data == RESOURCE_CLOSED)
        	demultiplexer.unwatch(event.resource)
        else
        	consume(data)
	}
}

1. 여기서 핵심은 demultiplexer.watch(watchedList)인데, demultiplexer가 watchedList를 계속 보면서 다음으로 넘어가지않고 Block하고 있다가 watchedList 중 하나라도 준비가 된다면 다음 foreach문으로 넘어간다.

2. 이 foreach문이 실행될때는 각각의 event들이 준비가 되었다는것이 보장이 되어 처리될때 block이 되지않는다.

3. 모든 event가 처리되고 나면 다시 demultiplexer가 block하며 새로운 event가 발생할때까지 기다린다.

이 일련의 과정을 event loop라고 한다.

 

따라서 정리하자면 각각의 쓰레드에서 커넥션을 1개씩 잡고 있는게 아니라 event demultiplexer가 여러개의 커넥션을 보고있다가 event가 발생하면 이를 모아서 전달해주는데, 이 모여진 event들은 blocking되지 않고 바로 처리할 수 있기 때문에 1개의 쓰레드로 처리가 가능하게 된다.

 

이 방식을 차용하면 쓰레드가 기다리는 idle time이 획기적으로 줄게되고 single thread로 운용이 가능해지기 때문에 race condition이나 multi-thread synchronization같은 문제를 겪지않고 더 직관적이게 병렬처리가 가능해진다.

 

The Reactor Pattern

Reactor 패턴은 앞에서 소개한 알고리즘의 특화된 버전이다. 핵심이 되는 아이디어는 각각의 I/O에 핸들러를 붙이는 것이다(Node.JS에서는 Callback). 그리고 이 핸들러들은 event가 발생하면 호출되어 event loop에 의해 처리된다.

 

Reactor패턴이 처리하는 방식은 다음과 같다:

1. Application은 Event demultiplexer에게 새로운 요청을 보냄으로서 새로운 I/O를 생성한다. Application은 요청을 보낼때 event가 발생하면 호출될 Handler를 설정하여 보낸다. 이 작업은 Non-Blocking이다.

2. I/O 작업이 끝나면 Event Demultiplexer는 새로운 event를 Event Queue에 삽입한다.

3. 이 때가 되면 Event Loop가 Event Queue를 순회한다.

4. 이벤트가 발생하면, 각각에 적용된 핸들러가 호출된다.

5. 어플리케이션 코드의 일부인 핸들러는 본인의 작업이 완료되면 Event Loop에게 control을 다시 넘겨준다. 

6. Event Queue에 있는 모든 작업이 완료되면 Event Demultiplexer가 다음 사이클을 시작할때까지 block 된다.

 

Node.js의 Non-Blocking Engine - libuv

I/O operation은 같은 OS안에서도 resource의 종류에 따라 상당히 다르게 처리될 수 있다. 예를 들자면 Unix의 regular filesystem은 non-blocking operation을 지원하지 않는다. 따라서 non-blocking처럼 행동하게 만들기 위해서는 event loop 밖에 별도의 thread를 사용하여 처리하게 된다. 이렇게 다양한 운영체제에서 Event Demultiplexer를 지원하기 위해서는 high-level abstraction이 필요했다. 그래서 메이저 OS들을 지원하기 위해 Node.js core team은 libuv라는 C 라이브러리를 만들게 되었다.

 

The Callback Pattern

Javascript는 callback을 나타내기 위한 최적의 언어이다. Function은 class object로서 쉽게 변수에 할당될 수 있고, argument로 전달되거나 return으로 반환될 수도 있다. 그리고 closure를 사용하면 function이 생성된 환경을 쉽게 접근할 수 있다. 즉, asynchronouse operation이 생성된 context를 언제 callback이 불러지던 접근할 수 있다.

 

Callback은 다른 함수에 인자로 전달되는 함수로서 함수의 작업이 끝나면 작업의 결과물과 함께 호출되는 함수이다.

이런 방식은 함수형 프로그래밍에서는 Continuation-Passing style(CPS)라고 불리며 Asynchronous operation에서만 사용되는 개념은 아니다. 단지 이 방식은 작업의 결과물을 caller에게 바로 반환하는 것이아니라 다른 함수에게 전달하는 방식을 뜻할 뿐이다.

 

반응형
블로그 이미지

개발자_무형

,