Node.js는 비동기 이벤트 주도 자바스크립트 런타임입니다. 논블로킹 I/O으로 이전 작업이 완료될 때까지 대기하지 않고 다음 작업을 수행할 수 있습니다. 또한 싱글 스레드입니다. 정확히 말하면 여러 스레드들이 생성되기는 하지만 프로그래머가 코드로 제어할 수 있는 스레드가 하나이기 때문에 싱글 스레드라고 이야기 합니다.
이러한 형태는 짧은 시간에 여러 작업을 처리할 수 있어서 효율적입니다. 또한 동시성 모델에서 발생하는 shared memory로 인하여 발생하는 문제들이 발생하지 않습니다. 하지만 그로 인하여 발생하는 단점 또한 존재합니다.
싱글 스레드 모델이기 때문에 하나의 작업이 cpu 집약적인 작업 즉, 시간이 많이 걸리는 작업이면 전체 시스템의 성능이 급격하게 떨어집니다.
const wait = (time) => {
const start = Date.now();
while(Date.now() - start < time){
}
};
const sayHello = () => {
wait(5000);
console.log('hello');
};
const setHello = () => {
sayHello();
sayHello();
}
setPromise();
위의 코드는 5초를 기다린 다음 hello를 출력하는 코드입니다. 싱글 스레드인 Node.js는 첫 번째 sayHello()를 처리하기 위해서 5초를 사용하게 되고, 두 번째 sayHello()를 처리하기 위해서 5초를 사용하게 됩니다. sayHello 함수를 사용자의 요청이라고 가정을 하면, 연속적으로 요청된 내용에 대해서 나중에 들어온 내용은 앞의 내용이 처리될 때까지 기다려야 한다는 것을 의미합니다. 따라서 서버의 성능이 급격하게 떨어지기 시작합니다.
다음과 같은 문제를 해결하기 위해서 multiprocessing 기법을 도입할 수 있습니다. 서버의 CPU가 2개 이상인 경우, Node.js 어플리케이션은 하나의 코어에서 실행되기 때문에 나머지 코어를 사용하지 못하는 상태입니다. 서버의 자원을 효율적으로 사용하여 응답 속도를 높이기 위해 cluster 모듈을 활용하여 나머지 코어들에도 Node.js 어플리케이션이 돌아가게 할 수 있습니다.
cluster 모듈은 하나의 마스터 프로세스가 생성되고, 마스터 프로세스가 워커 프로세스를 생성하고 관리하게 됩니다. 아래의 코드는 여러 프로세스를 통해서 http server를 생성하는 코드로 워커 프로세스에 http server를 생성하고, 마스터 프로세스는 워커 프로세스를 생성하는 코드입니다.
import cluster from 'cluster';
import http from 'http';
import { cpus } from 'os';
import process from 'process';
const numCPUs = cpus().length;
if (cluster.isPrimary) {
console.log(`Primary ${process.pid} is running`);
// Fork workers.
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('fork', (worker) => {
console.log('worker is dead:', worker.isDead());
});
cluster.on('exit', (worker, code, signal) => {
console.log('worker is dead:', worker.isDead());
});
} else {
// Workers can share any TCP connection. In this case, it is an HTTP server.
http.createServer((req, res) => {
res.writeHead(200);
res.end(`Current process\\n ${process.pid}`);
process.kill(process.pid);
}).listen(8000);
}
cluster 모듈은 round-robin 방식으로 워커 프로세스의 load를 조절합니다. 하나의 워커 프로세스에 너무 과도한 요청이 몰리지 않도록 조절하는 것입니다. 하지만 이는 운영체제의 스케줄링으로 인하여 온전하게 작동하지는 않습니다. 여러 프로세스가 존재해도 운영체제가 하나의 프로세스로 처리할 수 있다고 판단을 한다면, 하나의 프로세스만 작동을 하게 됩니다. 따라서 우리가 생각하는 밸런스 로더처럼 작동하지는 않지만 어느 정도 load를 분산시키는 역할을 수행할 수 있습니다.
마스터 프로세스를 작성하는 일은 상당히 시간이 걸리는 일입니다. 워커 프로세스에서 발생할 수 있는 에러에 대해서 핸들링을 해주어야 하고, 워커 프로세스가 죽게 된다면 일련의 과정을 통해서 프로세스를 새롭게 만들어야 합니다. 신경 쓸 부분이 많으므로 작성하는데 시간이 꽤 걸립니다.
따라서 마스터 프로세스의 역할을 pm2
라는 프로그램에 위임할 수 있습니다.
pm2에서 cluster mode는 node의 cluster 모듈을 활용하여 multiprocessing 환경을 제공합니다.
pm2 start app.js -i max
위의 명령어를 활용하면 app.js를 실행하는 프로세스를 cpu의 개수만큼 만들 수 있습니다. 다음과 같이 프로그램을 작성하는 경우, stateless한 어플리케이션을 만들어야합니다.
stateless한 어플리케이션은 process에 local data를 저장하지 않는 것을 의미합니다. local data는 session이나 socket instance 등 어플리케이션을 운영하면서 필연적으로 서버의 메모리에 저장을 해야 하는 내용들입니다. multiprocessing의 경우 메모리의 공유가 불가능합니다.
예를 들어 process1과 process2가 운영이 되고 있다고 하면, process1이 특정 사용자의 요청을 처리하고 session을 저장하게 되면 process1의 메모리에 저장이 되기 때문에 후에 사용자의 요청이 process2가 처리하게 될 때, 적절한 응답을 할 수 없게 됩니다. 따라서 공유가 필요한 자원을 다루기 위해서 데이터베이스를 사용하게 됩니다.