TIL/Node.js

Cluster

Art Rudy
반응형

Cluster?

Cluster는 단일 쓰레드 환경인 Node.js 환경을 멀티쓰레드 처럼 동작할 수 있도록 만든 모듈이다.

 

기본적으로 하나의 프로세스가 32bit에서는 512MB의 메모리, 64bit에서는 1.5GB메모리를 사용하도록 제한되어 있다.

 

V8엔진의 제한을 그대로 반영한 것인데, 설정을 더 늘릴 수는 있지만 그렇게 하기보다는 worker를 늘리는 것을 권장한다. 여러 개의 워커들이 병렬로 동작하며 효율을 극대화하는 것을 바람직한 방향으로 권하는 것.

 

모든 이벤트를 이벤트 큐로 넘기기 때문에 쓰레드가 하는 일은 그렇게 많지 않다. 대신 이벤트가 큐로 등록되기에 실행 시간이 예측하기 힘들다는 단점이 있다.

 

그래서 사용하는 것이 Cluster라는 기술이다.

 

worker를 생성하는 두가지 방법

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

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

 

클러스터의 스케쥴링 방식

클러스터는 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);
}
반응형

'TIL > Node.js' 카테고리의 다른 글

Redis  (0) 2021.07.21
Sass  (0) 2021.07.21
Babel  (0) 2021.07.21
Tedious  (0) 2021.07.21
Express  (0) 2021.07.21