특집기사:Node JS 멀티코어 CPU 지원하기

위클립스
이동: 둘러보기, 찾기
Article.png 특집기사 정보
Jeeeyul.jpg
저자 이지율

목차

[편집] 개요

Node.js는 일반적으로 싱글 프로세스로 작동한다. 다른 WAS들이 스레드 풀을 이용하여, 접수된 각 요청에 대해 개별 스레드를 할당하는 대신, Node.js는 싱글 프로세스가 모든 요청을 직접 처리한다. 만약 요청을 처리하는데 시간 비용이 높은 작업이 존재한다면(DB 조회등), 메인 프로세스가 블록되어, 다른 요청을 받아들이지 못하게 될 것이다.

이 문제를 해결하기 위해 무거운 작업은 개별 프로세스에서 수행하고, 작업이 종료되면, 작업 결과가 메인 프로세스에 이벤트로 전달되어, 메인 프로세스는 이 결과만 처리하게 하는 콜백 패턴을 주로 사용한다. 이 과정은 잘 모듈화된 라이브러리(몽고 DB 드라이버등)를 통해 이뤄지기 때문에, 보통 개발자가 병행 프로세스를 고려할 일은 거의 발생하지 않는다.

이런 방식은 특별한 환경을 제외하고는 다른 WAS처럼 역동적으로 스레드를 만들거나 관리하지 않기 때문에, 이로 인한 오버로드가 감소하여, 상대적으로 높은 성능을 보여줄 수 있다.

[편집] 멀티 코어

하지만 멀티 코어 CPU환경에서는 이야기가 약간 달라진다. 예를 들어 8코어 CPU에서 어떤 Node.js 애플리케이션을 실행한다고 가정하자.

이 애플리케이션은 http 요청을 받아들이는 메인 프로세스와 DB 조회를 담당하는 몽고DB용 워커 프로세스, 즉 두개의 프로세스만 존재한다고 가정해 보자. 프로세스가 2개 뿐이므로, 나머지 6개의 코어는 놀게 된다.

물론 DB모듈이 여러 프로세스를 사용하도록 잘 설계된 경우는 예외로 한다. 그런데 그런게 잘 있지도 않다.

물론 나는 고생하는데 CPU가 노는 엿같은 상황을 방임하는 것은 바람직한 개발자의 태도가 아니다. 다같이 죽자

[편집] 클러스터

클러스터(cluster)는 전술한 문제점을 해결하기 위해 Node.js가 제시한 해결책으로 공급되는 빌트인 모듈이다. 이는 아직 실험적인 기술임을 염두에 두자.

클러스터 모듈은 서버 포트를 공유하는 자식 프로세스들을 쉽게 만들 수 있게 해 준다.

백문이 불여일코딩 이므로 코드를 보면서 설명한다.

  1. var cluster = require('cluster');
  2. var http = require('http');
  3. var numCPUs = require('os').cpus().length;
  4.  
  5. if (cluster.isMaster) {
  6.     // 클러스터 워커 프로세스 포크
  7.     for (var i = 0; i < numCPUs; i++) {
  8.         cluster.fork();
  9.     }
  10.  
  11.     cluster.on('exit', function(worker, code, signal) {
  12.         console.log('worker ' + worker.process.pid + ' died');
  13.     });
  14. } else {
  15.     http.createServer(function(req, res) {
  16.         var str = "";
  17.         for (var i = 0; i < 1000000; i++) {
  18.             str += i;
  19.         }
  20.         res.writeHead(200);
  21.         res.end("hello world " + process.pid + " : " + str);
  22.     }).listen(8000);
  23.     console.log(process.pid);
  24. }

1 require를 이용하여 cluster모듈을 로드한다. 5 에서 현재 프로세스가 마스터인지 아닌지 여부를 판단한다.

마스터인 경우에는 8 cluster.fork를 이용하여 CPU의 갯수만큼 프로세스를 분할한다.

포크된 각 프로세스는 워커라고 불리며, 현재 모듈을 동일하게 다시 실행하지만, cluster.isMaster의 값만 false가 된다. 이들이 실행될 때에는 15로 분기된다.

쿼드 코어라고 가정하면, 1개의 마스터 클러스터 프로세스와 4개의 워커 프로세스가 만들어진다.

16 ~ 19는 로드 밸런싱을 테스트하기 위해, 일부러 집어넣은 시간 낭비 코드이다.

이를 실행하고 브라우저 여러개를 열어 http://localhost:8000 에 접속해 보면, 21 각기 다른 pid가 출력됨을 알 수 있다. 혹시나 여러분 PC가 성능이 너무 뛰어나다면, 같은 것만 나올수도 있으니, 17에서 뻘짓의 수준을 조절해 주자.

[편집] 어떻게 작동하나

워커 프로세스가 server.listen(...)을 호출하면, 인자가 직렬화 된 뒤, 이 요청을 마스터 프로세스로 전달한다. 마스터 프로세스가 이미 워커의 요청사항에 해당하는 포트를 리스닝하고 있는 경우라면, 리스닝 핸들을 워커에게 전달한다. 리스닝하고 있지 않은 경우라면, 새로운 리스닝 핸들을 만들고 워커에제 전달한다.

이러한 특징은 다음 3가지 경우에서, 다소 당황스러운 동작으로 이어질 수 있다:


server.listen({fd: 7})

인자가 직렬화 되어 마스터로 전달되므로, 워커 프로세스내의 파일기술자 7이 아닌, 마스터 프로세스에서의 파일 기술자 7을 리스닝하게 된다.


server.listen(handle) 핸들을 리스닝하는 것은 마스터에게 이를 알리지 않고, 워커가 주어진 핸들을 명시적으로 리스닝하게 한다. 워커가 이미 핸들을 가지고 있다면, 뭔짓을 하고 있는 중인지 개발자가 알고 있다는 것을 당연하다고 본다.

글쎄, 그럴까...


server.listen(0)

일반적으로 0번 포트를 리스닝하는 것은 랜덤한 포트를 리스닝하는 것이다. 하지만 클러스터에서는 각 워커는 동일한 랜덤 포트를 받게 된다. 첫번째 워커만 랜덤 포트를 부여 받고, 나머지는 동일한 포트를 부여 받는다. 고유의 포트를 리스닝하고 싶다면, 클러스터 워커 아이디를 시드로 하는 포트 넘버를 생성하는등의 방법을 쓰면 된다.


여러 프로세스가 동시에 기반 자원을 접근할 때, OS가 로드 밸런싱을 수행한다. Node.js자체에 라우팅 로직은 존재 하지 않으며, 클러스터의 워커들은 개별 프로세스로 어떠한 상태(메모리)도 공유하지 않는다.

따라서, 애플리케이션이 세션이나 로그인같은 데이터를 메모리를 통해 운용하지 않도록 설계하는 것이 매우 중요하다. 레디스나 몽고등 스토리지를 이용하여 세션을 관리해야 한다.

워커 프로세스는 모두 독립적이므로, 요구사항에 따라 리스폰되거나 킬 될 수 있다. 하나라도 워커가 살아남아 있다면, 서비스는 계속 작동한다. Node.js는 이를 관리해 주지 않으며, 이러한 처리는 여러분이 직접해야 한다. (프로세스 장애 로그나, 복구 뭐 그런 것들...)

[편집] 결론

Node.js의 클러스터는 굉장히 단순하다. OS의 로드 밸런싱을 이용하며, 독립된 프로세스들이 포트를 공유하는 것을 허용하도록 한다. 워커들이 각기 독립된 프로세스에서 구동되므로, 메모리 상태 공유가 불가능하다는 것만 염두에 두면, 놀고 있는 CPU 코어들을 충분히 부려 먹을 수 있을 것이다.

[편집] 참조

이 기사에 대한 의견은 토론 페이지를 통해 나눌 수 있습니다.

개인 도구
이름공간
변수
행위
포탈
탐색
도움
도구모음