애플리케이션의 성능을 최적화하여 많은 양의 수신 요청을 처리하고 원활한 사용자 경험을 제공하는 것은 중요한 작업이다. 이를 위한 한 가지 방법은 여러 코어에 걸쳐 애플리케이션을 확장하고 여러 하위 프로세스 간에 워크로드를 분산할 수 있는 클러스터 모듈을 사용하는 것이다. 이 글에서는 클러스터 모듈 사용의 이점과 클러스터 모듈 사용 모범 사례, 애플리케이션의 성능을 측정하기 위한 부하 테스트의 중요성에 대해 언급하고자 한다.
클러스터 모듈 개요
클러스터 모듈은 서버 포트를 공유할 수 있는 여러 작업자 프로세스를 생성할 수 있는 Node.js의 기본 제공 모듈이다. 각 작업자 프로세스는 별도의 코어에서 실행되므로 애플리케이션이 초당 더 많은 요청을 처리하고 요청에 더 빠르게 응답할 수 있다. 클러스터 모듈은 작업자 프로세스를 관리하고 들어오는 요청을 이들 간에 분배하는 마스터 프로세스를 생성하는 방식으로 작동한다.
const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
console.log(`Master ${process.pid} is running`);
// Fork workers
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.log(`worker ${worker.process.pid} died`);
});
} else {
// Workers can share any TCP connection
// In this case it is an HTTP server
http.createServer((req, res) => {
res.writeHead(200);
res.end('Hello World\n');
}).listen(8000);
console.log(`Worker ${process.pid} started`);
}
클러스터 모듈 사용의 이점
Node.js 애플리케이션에서 클러스터 모듈을 사용하면 몇 가지 이점이 있다.
- 성능 향상: 워크로드를 여러 코어에 분산하여 애플리케이션이 초당 더 많은 요청을 처리하고 요청에 더 빠르게 응답할 수 있다.
- 안정성 향상: 애플리케이션의 단일 인스턴스에 문제가 발생해도 다른 인스턴스가 중단 없이 수신 요청을 계속 처리할 수 있다.
- 다운타임(downtime) 감소: 다른 인스턴스가 요청을 계속 처리하는 동안 애플리케이션의 한 인스턴스를 중단할 수 있으므로 유지 관리 또는 업데이트 중 다운타임을 줄일 수 있다.
부하 테스트 도구 사용
애플리케이션의 성능을 측정하고 수신 요청의 과부하를 처리할 수 있는지 확인하려면 Artillery와 같은 부하 테스트 도구를 사용할 수 있다. Artillery를 사용하면 수신 요청의 과부하를 시뮬레이션하고 애플리케이션의 응답 시간과 용량을 측정할 수 있다. 다음은 간단한 API 엔드포인트를 테스트하기 위해 Artillery를 사용하는 방법의 예시이다.
# Install Artillery
npm install -g artillery
# Create a test script
echo "{
\"config\": {
\"target\": \"http://localhost:8000\",
\"phases\": [
{
\"duration\": 10,
\"arrivalRate\": 10
}
]
},
\"scenarios\": [
{
\"name\": \"Test API endpoint\",
\"flow\": [
{
\"get\": {
\"url\": \"/api/data\",
\"headers\": {
\"Authorization\": \"Bearer 1234\"
}
}
}
]
}
]
}" > test.json
# Run the test
artillery run test.json
Artillery를 전역에 설치한 후 테스트 스크립트를 생성하여 test.json이라는 파일에 저장한다. 이 스크립트는 테스트할 대상 URL, 테스트 기간, 테스트 중 요청의 도착 속도를 설정한다.
스크립트의 scenario 섹션에서는 테스트 중에 시뮬레이션할 요청의 흐름을 정의한다. Authorization 헤더를 사용하여 /api/data 엔드포인트에 GET 요청을 하도록 한다.
마지막으로, test.json을 실행하여 테스트를 실행하고 응답 시간, 오류, 처리량 등의 메트릭을 포함한 결과 보고서를 생성한다. 이를 통해 병목 현상을 파악하고 애플리케이션의 성능을 최적화할 수 있다.
클러스터 모듈을 사용하기에 좋은 사례
클러스터 모듈을 사용할 때 애플리케이션이 원활하게 실행되도록 하기 위해 고려해야 할 몇 가지 경우가 있다.
하위 프로세스 모니터링
자식 프로세스가 원활하게 실행되고 문제를 일으키지 않는지 확인하기 위해 자식 프로세스의 상태를 모니터링하는 것이 첫번째 사례이다. 클러스터 모듈은 이를 위해 자식 프로세스가 예기치 않게 종료될 때 발생하는 종료 이벤트와 같은 이벤트를 발생시킨다. 이러한 이벤트를 파일이나 적절한 모니터링 도구에 기록할 수 있다.
const cluster = require('cluster');
const os = require('os');
if (cluster.isMaster) {
console.log(`Master process is running with PID ${process.pid}`);
for (let i = 0; i < os.cpus().length; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.log(`Worker ${worker.process.pid} died with code ${code} and signal ${signal}`);
cluster.fork();
});
} else {
console.log(`Worker process ${process.pid} is running`);
// ...
}
오류 또는 장애 처리
자식 프로세스에 오류나 장애가 발생하면 전체 애플리케이션이 멈추지 않도록 정상적으로 처리하는 것은 중요하다. 자식 프로세스가 오류 등으로 인해 종료가 될 때 PM2와 같은 프로세스 관리자를 사용해서 자동으로 다시 시작할 수 있다.
const cluster = require('cluster');
const os = require('os');
const pm2 = require('pm2');
if (cluster.isMaster) {
console.log(`Master process is running with PID ${process.pid}`);
pm2.connect((err) => {
if (err) {
console.error(err);
process.exit(1);
}
for (let i = 0; i < os.cpus().length; i++) {
cluster.fork();
}
});
} else {
console.log(`Worker process ${process.pid} is running`);
process.on('uncaughtException', (err) => {
console.error(`Worker ${process.pid} encountered an unhandled exception: ${err}`);
pm2.restart(process.env.pm_id);
});
// ...
}
작업 부하를 균등하게 분산
작업 부하가 하위 프로세스 간에 균등하게 분산되도록 라운드 로빈 알고리즘 또는 해싱 알고리즘을 사용하여 특정 요청을 처리할 하위 프로세스를 결정할 수 있다.
const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
console.log(`Master process is running with PID ${process.pid}`);
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
} else {
console.log(`Worker process ${process.pid} is running`);
http.createServer((req, res) => {
const worker = cluster.worker.id;
const hash = req.url.split('/')[1]; // assuming URLs are in the format "/hash"
if (hash % numCPUs === worker) {
// this worker should handle this request
res.writeHead(200);
res.end(`Hello from worker ${worker}`);
} else {
// forward the request to another worker
const targetWorker = Object.values(cluster.workers).find(w => hash % numCPUs === w.id);
targetWorker.send(req.url);
}
}).listen(8000);
}
마무리
트래픽이 많을 때 원활하고 반응이 빠른 사용자 경험을 제공하려면 Node.js 애플리케이션의 성능을 최적화하는 것이 중요하다. 클러스터 모듈은 여러 코어에 작업 부하를 분산하고 애플리케이션의 성능과 안정성을 개선할 수 있는 강력한 도구이다. 클러스터 모듈 사용에 대한 모범 사례를 따르고 Artillery와 같은 부하 테스트 도구를 사용하여 성능을 측정하면 성능 문제를 식별 및 수정하고 애플리케이션이 많은 양의 수신 요청을 처리할 수 있는지 확인할 수 있다.