toggle menu

[NodeJS] 클러스터(cluster)에 대한 정리

2015. 11. 17. 23:13 Node.js

들어가며


node.js는 기본적으로 하나의 프로세스가 32bit에서는 512MB의 메모리, 64Bit에서는 1.5GB 메모리를 사용하도록 제한되어 있다. V8엔진의 제한을 그대로 반영한 것인데, 물론 설정으로 더 늘릴 수는 있지만 그렇게 하기 보다는 worker 를 늘리는 것을 권장하고 있다. 여러개의 워커들이 병렬로 동작하며 효율을 극대화하는 것을 바람직한 방향으로 권하고 있는 것이다.




worker를 생성하는 두가지 방법


node.js 에서 worker 를 생성하는 방법은, child_process와 cluster 정도로 요약할 수 있다.

cluster 는 node.js v0.8 부터 소개되었는데, 큰 부하를 노드 프로세스들의 클러스터를 통해 다루려는 목적으로 시작되었다. 추가적으로 이 프로세스들은 서버의 포트들을 공유할 수 있기 때문에 web application 에 매우 적합하다.


단, (이미 node.js v5.x가 나오고 있는 시점이지만) cluster의 경우 node.js v0.12 이전 버전에서 worker 들에게 균일하게 load balancing 이 안되는 문제가 있다는 점을 주의해야 한다. 다행히 0.12버전부터는 Round-Robin Load Balancing 이 적용되어 해당 이슈가 해결된 상태이다.


프로세스들을 단순하게 병렬로 실행하는 것은 child_process.fork() 로 가능하고, 여기에 로드밸런싱과 포트 공유 등이 필요하다면 클러스터로 접근하는 것이 좋다. 두 방식 모두 IPC(Inter-Process Communication)로 process 간에 통신이 가능하기 때문에 로드 밸런싱 등의 추가적인 기능이 필요한 경우 클러스터를 활용하고 워커를 직접적으로 컨트롤해야하는 경우 child_process 를 주로 활용하게 된다.


참고로 새로운 child process 는 모두 V8의 인스턴스이기때문에 30ms의 시작시간과 10MB 가량의 메모리를 소모한다는 것을 기억해야 한다.


이제 클러스터를 사용하는 방법에 대해 좀더 자세히 살펴보자.





클러스터의 스케쥴링 방식


클러스터는 cluster 모듈을 require 하는 것으로 시작된다.


var cluster = require('cluster');


cluster 모듈을 가져왔다면, 실제 클러스터를 생성하기 전에 스케쥴링 방식을 설정해줄 수 있는데, 아래와 같이 지정해 줄 수 있다.


//워커 스케쥴을 OS에 맡긴다.
cluster.schedulingPolicy = cluster.SCHED_NONE;

//워커 스케쥴을 Round Robin 방식으로 한다.
cluster.schedulingPolicy = cluster.SCHED_RR;


원래는 기본적으로 스케쥴을 OS에 맡기는 방식이었는데, 이 경우 특정 워커에 작업이 몰리는 경우가 많아서 차라리 순차적으로 하나씩 작업을 배분하는 Round Robin 방식이 node.js v0.12에서 추가되었다.




마스터와 워커


클러스터 모듈은 지금 실행된 인스턴스가 마스터인지 확인할 수 있다.


if (cluster.isMaster) {
    console.log('마스터');
}


물론 현재 클러스터가 워커인지도 알 수 있다.


if (cluster.isWorker) {
    console.log('워커');
}


워커 생성은 fork 를 수행한 만큼 생성된다.


var worker = cluster.fork();



동일한 JavaScript 파일을 실행하면서 처음 실행되면 기본적으로 마스터가 된다. 마스터에서는 cluster.fork() 메서드를 통해서 워커들을 생성하면 생성된 워커들도 마찬가지로 동일한 JavaScript 파일을 실행하게 되는데, 이때 이미 마스터가 있다면 새롭게 실행되는 프로세스는 워커가 된다.


마스터와 워커가 수행해야 할 각 작업은 isMaster, isWorker 메서드를 활용해서 마스터일 때와 워커일 때를 구분해서 규정해주면 된다. 마스터인 경우 되도록 워커들을 생성/관리하는 로직만 포함하고 그 외의 로직은 적게 가져가는 것이 좋다.




워커 생성/제거 이벤트


워커가 생성되면 online 이벤트가 발생한다. 워커가 죽으면 exit 이벤트가 발생한다. 이 이벤트들을 활용해서 워커가 생성되었을 때 필요한 로직들과 워커가 죽었을 때 복구를 위한 작업들을 설정해줄 수 있다.


cluster.on('online', function (worker) {
    console.log('생성된 워커의 아이디 : ' + worker.process.pid);
});

cluster.on('exit', function (worker, code, signal) {
    console.log('죽은 워커의 아이디 : ' + worker.process.pid);
    console.log('죽은 워커의 exit code : ' + code);
    console.log('죽은 워커의 signal : ' + signal);
});




마스터와 워커간 Communication


위에서 child_process 와 cluster 두 방식 모두 IPC(Inter-Process Communication)로 process 간에 통신이 가능하다는 것을 이야기했었다. 마스터와 워커간 통신을 사용하면, 워커의 재시작이 필요한 경우 워커들에게 종료할 예정이라고 메시지를 보내고 워커가 종료 준비를 마쳤을 때 마스터에게 다시 메시지를 보내서 안전하게 워커를 죽인 뒤 재생성을 수행하는 등의 작업을 해줄 수 있다.


if (cluster.isMaster) {
    //워커 생성
    var worker = cluster.fork();

    //생성한 워커가 보내는 메시지 처리
    worker.on('message', function (message) {
        console.log('마스터가 ' + worker.process.pid + ' 워커로부터 받은 메시지 : ' + message);
    });

    //생성한 워커에게 메시지 보내기
    worker.send('마스터가 보내는 메시지');
}

if (cluster.isWorker) {
    //마스터가 보낸 메시지 처리
    process.on('message', function(message) {
        console.log('워커가 마스터에게 받은 메시지 : ' + message);
    });

    //마스터에게 메시지 보내기
    process.send(process.pid + ' pid 를 가진 워커가 마스터에게 보내는 메시지');
}




Zero Down-time


node.js application에 어떤 변경을 반영하기 위해서 재시작이 필요하지만, cluster를 활용하면 무중단 서비스가 가능해진다. 물론 이를 위해서는 마스터는 항상 동작해야 하고, 마스터에는 워커들을 관리하기 위한 작고 짧은 로직만 있어야 한다.


안전하게 재시작을 하기 위해선, 먼저 마스터가 워커들에게 종료 예고를 해주는게 좋다.


//마스터가 워커들에게 종료 예고 메시지 전송
for (var id in cluster.workers) {
    cluster.workers[id].send({type: 'shutdown', from: 'master'});
}

//워커가 종료 예고를 받고 적절한 처리 후 스스로 종료
process.on('message', function(message) {
    if(message.type === 'shutdown') {

        //안전하게 종료할 수 있는 로직

        //워커 종료
        process.exit(0);
    }
});




CPU 갯수만큼 워커 생성하기


일반적으로 워커는 CPU의 갯수만큼 생성한다. 특별한 경우가 아닌이상 CPU 갯수만큼 있을 때 가장 나은 성능을 보여주기 때문이다. 앞서 살펴본 클러스터에 대한 내용을 바탕으로 간단하게 CPU 갯수만큼 워커를 생성하는 로직을 작성해봤다.


worker.js 파일에는 워커에만 필요한 로직이 들어있고 start 라는 메서드를 제공해줘서 start로 관련 로직을 실행한다고 가정했다.


var cluster = require('cluster');
var os = require('os');

if (cluster.isMaster) {
    //CPU의 갯수만큼 워커 생성
    os.cpus().forEach(function (cpu) {
        cluster.fork();
    });

    //워커가 죽으면,
    cluster.on('exit', function(worker, code, signal) {

        //종료된 클러스터 로그
        console.log('워커 종료 : ' + worker.id);

        if (code == 200) {
            //종료 코드가 200인 경우, 워커 재생성
            cluster.fork();
        }
    });
}
else {
    //워커 로직을 여기에 작성
    console.log('워커 생성 : ' + cluster.worker.id);
}




마치며


지금까지 클러스터 모듈이 등장하게 된 배경부터 클러스터를 사용하는 방법까지 살펴보았다. 앞서 이야기 했듯이 node.js는 V8 엔진이 가진 제한으로 한 프로세스당 512MB ~ 1.5GB의 메모리만 사용하도록 설정되어 있다. 하지만 클러스터를 활용하면 서버의 리소스를 최대한 낭비없이 활용 할 수 있을 것이다.


Node.js 관련 포스팅 더보기