node.js로 javascript 기반 cpu-intensive 작업 가능할까

node.js (이하 node)는 공식적으로 event loop 이 수행되는 main thread 에서만 v8엔진 기반의 javascript 실행환경을 제공한다. node 는 자신의 프로세스 내/외부에서 발생하는 모든 request를 main thread를 통해 처리한다. 이 때문에 main thread 를 잠깐 멈추게 하는 cpu-intensive javascript 코드를 수행해야 하는 usecase는 node에서 사용하기엔 무리가 있다. 이것이 node의 제약이기도 하다. 그러나 node로 꼭 그러한 작업를 수행해야 한다면 어떻게 해야 할까. 언제든 제약 또는 단점으로 여겨지는 문제를 푸는 일은 재미있는 일이다. node에서 cpu intensive 작업을 수행하면서도 node가 멈추지 않고 동작하게끔 할 수 있는 몇가지 방법을 정리해보려 한다. 

* 이글에서 말하는 cpu intensive 작업은 cpu를 연속적으로 사용하는 javascript 코드로써 코드 수행이 끝날때까지 자신을 호출한 caller에게 결과를 리턴하지 않는 작업을 의미한다. 

1. child_process 모듈

node 프로세스를 fork하여 새로운 node 프로세스를 생성하는 것이다. 즉, 자식 node 프로세스를 만드는 것이다. 아래의 코드처럼 child_process를 fork하면서 child process가 수행해야 할 javascript 파일을 지정할 수 있다.
[gist 981c240f2fa24b7d9dfc]

 child process는 지정된 javascript 코드를 수행하면서 수행한 결과를 메시지를 통해 parent process에게 전달해줄 수 있다. 물론 parent process가 이 메시지를 받기 위해서 listen하고 있어야 한다. child process를 fork해서 cpu-intensive 작업을 수행하도록 하면 원래 node 프로세스는 계속해서 event loop을 수행하며 다른 요청들을 처리할 수 있다. 그러나 child_process 를 사용시 고려해야 할 것이 있다. 그것은 child 프로세스가 또 하나의 node 프로세스라는 것이다. 이것은 child 프로세스가 부모처럼 node를 위한 각종 초기화 작업을 수행한 후 지정된 javascript 코드를 수행시킨다는 의미이다. node 초기화 작업은 크게 다음과 같다.

 – v8 초기화 및 Isolate, Context 생성
 – uv event loop 생성 및 non-blocking 작업을 위한 thread pool 생성 (현재 4개)
 – ‘process’와 ‘module’ js object 생성, 각종 builtin 모듈 로딩

음, cpu-intensive 작업을 위해 이런 비용을 들이면서 프로세스를 생성하는 것은 그렇게 나이스해 보이지 않는다.  child_process 모듈은 쉘 명령어를 수행해서 표준출력으로 결과를 parent process에게 보내는 용도가 더 적합하지 않을까 싶다. 그러나 node 프로세스 생성 비용이 큰 문제가 되지 않는 시나리오라면 사용해도 괜찮을 것 같다. 

2. cluster 모듈

cluster 모듈은 내부적으로는 child_process 모듈의 fork 메소드를 사용해서 구현하기 때문에 위에서 언급한 node 프로세스 비용이 고스란히 발생한다. 무엇보다 cluster 모듈은 cpu core를 모두 활용함으로써 동시 발생하는 외부 요청들을 더욱 많이 처리하여 scalability를 높이기 위해 만들어졌다. 그러므로 cpu intensive 작업을 처리하는데 있어서는 cluster 모듈 사용은 적절하지 않은 것 같다. 
[gist df60c43becedbfe94b51]

3. threads-a-gogo 모듈

cpu intensive 작업을 위해서 별도의 프로세스를 생성하는 것보다는 이미 실행중인 node 프로세스안에 새로운 thread를 생성하여 javascript 실행환경을 만들어주는 것이 비용과 반응성 측면에서 더 나아보인다. v8의 javascript 실행환경을 위해서는 반드시 v8::Isolate과 v8::Context가 필요하다. 새롭게 생성된 thread는 이미 만들어진 v8::Isolate 를 쓸 수도 있고, 아니면 새롭게 만들어서 쓸 수도 있다. main thread가 다른 thread와 javascript 오브젝트들을 공유하려면 javascript 실행환경이 같아야 한다. 이말은 같은 v8::Isolate를 사용해야 한다는 것인데, 이렇게 되면 동시에 두 thread가 javascript 코드를 수행할 수 없다. 왜냐하면 v8이 하나의 v8::Isolate은 자신을 lock을 하고 있는 하나의 thread에 의해서만 사용되도록 제한하고 있기 때문이다. main thread의 javascript 수행이 멈추지 않도록 하려면 새로운 thread에 새로운 v8::Isolate과 v8::Context를 생성해줘야 한다. main thread는 콜백이나 메시지 전달을 통해 결과를 받을 수 있어야 할 것이다. 찾아보니 방금 말한 것을 구현한 node 모듈이 있었다. 
threads-a-gogo 모듈이다. (이름이 아주 독특한데 왜 gogo라고 했을까 a는 뭐지-_-;ㅋ). threads-a-gogo의 목적자체가 cpu-intensive 작업을 main thread가 아닌 별도의 thread에서 처리하도록 하는 것이다. 별도로 생성된 thread는 작업이 완료되면 main thread에게 콜백 또는 메시지로 결과를 알려줄 수 있다. 아래는 별도의 thread를 생성하고 계산작업을 시킨후 그 결과를 main thread 내 콜백으로 호출하는 예이다. 
[gist 2d12240c4fd64d8c2eab]

threads-a-gogo가 생성하는 thread는 pthread이며 main thread의 v8::Isolate를 사용하지 않고 별도의 v8::Isolate과 v8::Context을 생성하여 사용한다. 즉, main thread가 생성한 어떤 v8 object도 접근할 수 없을뿐만 아니라 다른 thread과도 어떤 v8 Object도 공유할 수 없다는 의미이다. 생성된 thread는 독립적으로 javascript 코드 또는 파일을 수행하고 main thread에게 결과만 전달한다. 아래 코드는 thread-a-gogo의 c++ addon에서pthread_create를 호출할때 start_routine 인자로써 전달하는 함수이다.  
[gist b14b66d0cd11ba6ce64c]

4. webworker-threads 모듈

HTML5의 Web Worker 스펙을 구현한 node 모듈이다. 서버 사이드에서 web worker를 쓸 수 있게 했다는 것만으로도 좋은 시도라 생각한다. 바로 위에서 소개한 threads-a-gogo 모듈을 이용해서 thread 처리를 하고 있다. 아래 소스는 threads-a-gogo의 예제를 web worker버전으로 바꿔본 것이다.
[gist 12008430e591d3209699] 

목적 자체가 threads-a-gogo와 같고 thread 내부 동작도 동일하나 개발자들에게 노출된 javascript API만 다르다. 둘 중 맘에 드는 걸 선택해서 쓰면 되겠으나, Web Worker 스펙은 W3C에 의해 만들어졌고 클라이언트 사이드에서 많이 사용되고 있어 레퍼런스도 많으니 이왕이면 webworker-threads 모듈을 사용하는게 좋을 듯하다.

5. JXCore (forked from node.js)

JXCore는 node.js 소스코드를 fork하여 multithreading 기능을 추가한 프로젝트이다. 프로젝트가 시작된 이유는 node가 제 아무리 I/O중심의 요청들을 받아서 빠르게 처리한다 하더라도 동시에 수많은 요청이 한꺼번에 들어오면 반응성이 떨어지는 현상이 있는데 이를 해결하기 위해서라고 한다. node.js는 cluster 모듈을 이용해서 node 프로세스를 여러개 생성해서 반응성을 높이려고 하는데 JXCore는 하나의 node 프로세스 안에 추가적인 thread들을 생성하여 각각 v8::Isolate, v8::Context를 만들고 각각 uv기반의 event loop을 갖도록 하여 반응성을 높인다. 일반적으로 thread가 process에 비해 cpu나 메모리 측면에서 생성비용이 적게 때문에 JXCore가 cluster에 비해 효과를 보는 부분이 있을 거라고 예상할 수 있다. 실제로 벤치마크 결과(레퍼런스 참고)를 보면 node.js의 cluster보다 좋은 성능을 내고 있음을 알수 있다. JXCore가 node.js에 multithreading 기능을 넣은 건 기존 cluster의 반응성 문제를 해결하기 위해서였으나, 이 multihreading 기능을 이용해서 cpu-intensive 작업도 할 수 있도록 지원한다. 아래의 코드에서 보듯이 생성된 JXCore의 thread pool에 task를 add하면 JXCore가 내부적으로 thread를 선택하여 해당 task를 수행시킨다. main thread는 등록한 callback 또는 메시지를 통해 결과를 받을 수 있다. 
[gist 63d008f5b38d9020a5fa]

thread를 이용해서 동시 요청에 대한 반응성을 끌어올린 부분과 cpu intensive 작업을 병렬로 처리하게 한 부분은 주목할만하지만, 굳이 node.js를 fork해서 독립적으로 개발할 필요가 있었을까 싶다. 물론 node.js의 core를 수정해야 했기에 그렇다고는 하지만 node.js가 앞으로 계속 버전업이 될텐데 그에 따라 계속 호환성 유지를 해줄지 모르겠다. 현재까지는 node.js의 버전 (0.11)과 100% 호환되기 때문에 기존 node module과 app이 동작하는데 문제가 없다. JXCore는 현재 오픈 소스도 아니고 중간에 독자적인 길을 걷게 될 수도 있기 때문에 JXCore 기반으로 node 코드를 짜는건 node 개발자들에겐 약간 risk가 있게 느껴지지 않을까 싶다. 

마무리하며..

node에서 제약으로 여겨지는 cpu intensive 작업을 병렬적으로 수행하여 해결하는 몇가지 방법을 간단히 살펴보았다. cpu intensive 작업을 수행하는 상황과 시나리오가 다양하고 다를 것이기 때문에 어떤 방법이 좋다라고 결론 짓기는 어렵다. 상황에 맞게 선택해서 쓰도록 하자. 

 

< refereces >

– http://nodejs.org/api/child_process.html

– http://nodejs.org/api/cluster.html

– https://github.com/xk/node-threads-a-gogo

– https://github.com/audreyt/node-webworker-threads

– http://jxcore.com/docs/jxcore-feature-multithreading.html

– http://flippinawesome.org/2014/03/03/jxcore-a-node-js-distribution-with-multi-threading/

– http://jxcore.com/nodejx-vs-vert-x-vs-node-js-cluster/

My name is Yunchan Cho. I love web and its technology. I hope that my life makes peoples to be inspired and encouraged. twitter: @yunchancho

Posted in Technical Note

Leave a comment