Node.js cluster 모듈 멀티 프로세스 처리
Node.js의 cluster 모듈을 사용하여 멀티 프로세스 환경에서 애플리케이션을 구동하는 방법을 다룹니다.
Introduction
Node.js는 단일 스레드 이벤트 루프를 기반으로 동작하지만, cluster 모듈을 사용하면 여러 CPU 코어를 활용할 수 있습니다. 이번 포스트에서는 cluster 모듈을 사용한 멀티 프로세스 처리 방법을 살펴보겠습니다.
Environment
node --version
# v20.11.0
# CPU 코어 수 확인
node -e "console.log(require('os').cpus().length)"
# 8Problem
단일 프로세스에서 처리할 수 있는 요청 한도에 도달했습니다:
const express = require('express');
const app = express();
app.get('/api/heavy', async (req, res) => {
// CPU 집약적인 작업
const result = await heavyComputation();
res.json(result);
});
app.listen(3000);부하 테스트 결과
# Apache Bench 결과
$ ab -n 10000 -c 100 http://localhost:3000/api/heavy
Requests per second: 45.23 [#/sec] (mean)
Time per request: 2211.032 [ms] (mean)
Failed requests: 1247 (12.47%)
# CPU 사용률: 100% (1코어)
# 메모리: 150MBAnalysis
cluster 동작 원리
Master Process
│
┌──────────┼──────────┐
│ │ │
Worker 1 Worker 2 Worker 3
(CPU 1) (CPU 2) (CPU 3)단일 프로세스 vs 멀티 프로세스
| 항목 | 단일 프로세스 | 멀티 프로세스 |
|---|---|---|
| CPU 활용 | 1코어 | 전체 코어 |
| 메모리 | 적음 | 각 프로세스별 |
| 안정성 | 프로세스 죽음 시 전체 중지 | 부분적 영향 |
| 구현 복잡도 | 낮음 | 중간 |
Solution
1. 기본 cluster 설정
const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;
if (cluster.isPrimary) {
console.log(`Primary ${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. Starting a new one...`);
cluster.fork();
});
} else {
// 워커 프로세스
http.createServer((req, res) => {
res.writeHead(200);
res.end('Hello from worker ' + process.pid);
}).listen(8000);
console.log(`Worker ${process.pid} started`);
}2. Express와 cluster 통합
const cluster = require('cluster');
const express = require('express');
const os = require('os');
const numCPUs = os.cpus().length;
if (cluster.isPrimary) {
console.log(`Primary process ${process.pid} is running`);
console.log(`Forking ${numCPUs} workers...`);
// 워커 생성
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
// 워커 상태 모니터링
cluster.on('online', (worker) => {
console.log(`Worker ${worker.process.pid} is online`);
});
cluster.on('exit', (worker, code, signal) => {
console.log(`Worker ${worker.process.pid} exited with code ${code}`);
console.log('Starting a new worker...');
cluster.fork();
});
// Graceful shutdown
process.on('SIGTERM', () => {
console.log('SIGTERM received. Shutting down gracefully...');
for (const id in cluster.workers) {
cluster.workers[id].process.kill('SIGTERM');
}
});
} else {
const app = express();
// 라우트 정의
app.get('/', (req, res) => {
res.json({
message: 'Hello from worker',
pid: process.pid,
});
});
app.get('/api/heavy', async (req, res) => {
// CPU 집약적인 작업 시뮬레이션
const result = await performHeavyComputation();
res.json({ result, pid: process.pid });
});
app.listen(3000, () => {
console.log(`Worker ${process.pid} started on port 3000`);
});
}3. 로드 밸런싱 구현
const cluster = require('cluster');
const net = require('net');
const os = require('os');
const numCPUs = os.cpus().length;
const PORT = 3000;
if (cluster.isPrimary) {
// 로드 밸런서
const workers = [];
let currentWorker = 0;
for (let i = 0; i < numCPUs; i++) {
const worker = cluster.fork();
workers.push(worker);
}
// 라운드 로빈 방식으로 연결 분배
const server = net.createServer({ pauseOnConnect: true }, (socket) => {
const worker = workers[currentWorker];
currentWorker = (currentWorker + 1) % workers.length;
worker.send('connection', socket);
});
server.listen(PORT);
console.log(`Load balancer running on port ${PORT}`);
} else {
process.on('message', (msg, socket) => {
if (msg === 'connection') {
// 워커가 소켓을 처리
const server = net.createServer();
server.emit('connection', socket);
}
});
const app = require('./app');
app.listen(0); // 임의 포트
}4. 모니터링 대시보드
const cluster = require('cluster');
const express = require('express');
const os = require('os');
if (cluster.isPrimary) {
const workers = new Map();
for (let i = 0; i < os.cpus().length; i++) {
const worker = cluster.fork();
workers.set(worker.id, {
pid: worker.process.pid,
startTime: Date.now(),
requests: 0,
});
worker.on('message', (msg) => {
if (msg.type === 'request') {
const info = workers.get(worker.id);
info.requests++;
}
});
}
// 모니터링 엔드포인트
const monitorApp = express();
monitorApp.get('/status', (req, res) => {
const status = {
master: process.pid,
workers: Array.from(workers.values()),
memory: process.memoryUsage(),
uptime: process.uptime(),
};
res.json(status);
});
monitorApp.listen(3001);
console.log('Monitor running on port 3001');
} else {
const app = express();
app.use((req, res, next) => {
process.send({ type: 'request' });
next();
});
// 기존 라우트
app.get('/', (req, res) => {
res.json({ pid: process.pid });
});
app.listen(0);
}5. Graceful Shutdown
const cluster = require('cluster');
const express = require('express');
if (cluster.isPrimary) {
const workers = new Map();
for (let i = 0; i < require('os').cpus().length; i++) {
const worker = cluster.fork();
workers.set(worker.id, worker);
}
const gracefulShutdown = () => {
console.log('Graceful shutdown initiated...');
for (const [id, worker] of workers) {
console.log(`Sending SIGTERM to worker ${id}`);
worker.send('shutdown');
worker.disconnect();
// 강제 종료 타임아웃
setTimeout(() => {
if (!worker.isDead()) {
console.log(`Force killing worker ${id}`);
worker.kill();
}
}, 5000);
}
};
process.on('SIGTERM', gracefulShutdown);
process.on('SIGINT', gracefulShutdown);
} else {
const app = express();
process.on('message', (msg) => {
if (msg === 'shutdown') {
console.log(`Worker ${process.pid} shutting down...`);
// 리소스 정리
server.close(() => {
console.log(`Worker ${process.pid} closed`);
process.exit(0);
});
// 강제 종료 타임아웃
setTimeout(() => {
process.exit(1);
}, 5000);
}
});
const server = app.listen(3000);
}Lessons Learned
- 워커 재시작: 워커가 종료되면 자동으로 재시작하는 로직을 구현하세요
- 메모리 관리: 각 워커는 독립된 메모리를 가지므로 메모리 사용량을 모니터링하세요
- Graceful Shutdown: 프로세스 종료 시 현재 요청을 완료한 후 종료하세요
- 로드 밸런싱: 여러 워커에게 요청을 균등하게 분배하세요
- 환경 변수:
process.env는 모든 워커에서 공유되지만, 워커별 상태는 독립적입니다
This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.