Node.js 멀티코어 활용하기 - cluster 모듈 도입기
배경
회사 API 서버가 4코어 인스턴스에서 동작 중인데, CPU 사용률이 25% 근처에서 머물렀다. Node.js는 싱글 스레드라 하나의 코어만 사용하고 있었던 것.
트래픽이 증가하면서 응답 시간이 느려지기 시작했고, 스케일 아웃 전에 우선 현재 리소스부터 제대로 활용하기로 했다.
cluster 모듈 구현
PM2를 쓸 수도 있었지만, 불필요한 의존성을 줄이고 싶어서 네이티브 cluster 모듈로 직접 구현했다.
const cluster = require('cluster');
const os = require('os');
if (cluster.isMaster) {
const numCPUs = os.cpus().length;
console.log(`Master ${process.pid} is running`);
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.log(`Worker ${worker.process.pid} died`);
cluster.fork();
});
} else {
require('./app');
console.log(`Worker ${process.pid} started`);
}
주의사항
메모리 사용량
워커 프로세스마다 별도의 메모리 공간을 사용한다. 4코어면 메모리도 4배 가까이 증가했다. 모니터링 설정을 다시 조정해야 했다.
상태 공유 불가
프로세스 간 메모리 공유가 안 되므로, 인메모리 캐시는 Redis로 이전했다. 각 워커가 독립적으로 캐싱하면 비효율적이고 정합성 문제도 생긴다.
그레이스풀 셧다운
process.on('SIGTERM', () => {
server.close(() => {
console.log('Process terminated');
});
});
무중단 배포를 위해 SIGTERM 시그널 핸들링을 추가했다.
결과
CPU 사용률이 골고루 분산되고, 동일 트래픽 대비 응답 시간이 30% 가량 개선됐다. 다만 메모리 사용량이 증가한 만큼 인스턴스 스펙 조정이 필요할 수 있다.
당장은 cluster로 충분하지만, 나중에 워커 스레드(Worker Threads)도 검토해볼 예정이다.