클러스터 모듈 및 부하 테스트로 Node.js 성능 개선하기

이상문
9 min readMay 8, 2023

--

애플리케이션의 성능을 최적화하여 많은 양의 수신 요청을 처리하고 원활한 사용자 경험을 제공하는 것은 중요한 작업이다. 이를 위한 한 가지 방법은 여러 코어에 걸쳐 애플리케이션을 확장하고 여러 하위 프로세스 간에 워크로드를 분산할 수 있는 클러스터 모듈을 사용하는 것이다. 이 글에서는 클러스터 모듈 사용의 이점과 클러스터 모듈 사용 모범 사례, 애플리케이션의 성능을 측정하기 위한 부하 테스트의 중요성에 대해 언급하고자 한다.

클러스터 모듈 개요

클러스터 모듈은 서버 포트를 공유할 수 있는 여러 작업자 프로세스를 생성할 수 있는 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와 같은 부하 테스트 도구를 사용하여 성능을 측정하면 성능 문제를 식별 및 수정하고 애플리케이션이 많은 양의 수신 요청을 처리할 수 있는지 확인할 수 있다.

--

--

이상문
이상문

Written by 이상문

software developer working mainly in field of streaming, using C++, javascript

No responses yet