1. cluster 모듈을 이용한 멀티 프로세스 방식
- 멀티 프로세스 방식이란?
- 하나의 애플리케이션이 여러 개의 프로세스를 생성하여 동시에 작업을 수행하는 방식
- 각 프로세스는 독립적인 실행환경을 가지고, 별도의 메모리 공간을 할당 받아 작동
- 즉, 여러 프로세스가 독립적으로 애플리케이션의 일부를 병렬로 처리한다.
- Node.js는 기본적으로 싱글 스레드 기반이지만, 멀티 프로세스 방식을 사용하여 다중 코어 CPU에서 병렬 처리를 지원할 수 있다.
- 싱글 프로세스 : 하나의 프로세스가 모든 요청을 처리. CPU 코어가 여러개 있어도 한 코어만 사용
- 멀티 프로세스 : 여러 프로세스가 각각의 요청 처리. CPU 코어를 여러 개 활용하여 병렬 처리
1. cluster 모듈의 장단점
- 장점
- 성능 향상 : 멀티 코어 CPU의 모든 코어를 사용하여 싱글 스레드에 비해 훨신 많은 요청을 동시에 처리
- 프로세스 격리 : 하나의 워커가 다운되도 다른 워커에 영향이 없다. 마스트 프로세스가 워커를 감시하고, 필요 시 새로운 워커를 생성하여 안정성을 높인다.
- 확장성 : 워커 수를 늘려서 애플리케이션 성늘을 쉽게 확장가능. 서버 환경에 맞춰 CPU 코어에 대응하는 워커 수를 늘림으로써, 서버 자원을 효율적으로 사용 가능
- 단점
- 메모리 오버 헤드 : 각 워커 프로세스가 독립적인 메모리를 사용하여, 메모리 사용량이 증가할 수 있다. 워커가 많아질수록 자원 관리가 중요해 진다.
- 복잡성 증가 : 워커 간의 데이터 공유나 통신이 필요한 경우, 이를 처리하기 위한 추가적인 코드 필요, 쉽게 말해 워커간의 파라미터 및 변수 공유가 안되어서 redis 등의 별도의 대책이 필요하다.
- 로드 밸런싱 한계 : 기본적으로 cluster 모듈은 라운드 로빈 방식으로 요청을 분배하지만, 각 워커가 동일한 부하를 처리할 수 없는 경우 비효율이 발생할 수 있다.
- 라운드 로빈 : 공평하게 작업에 대한 일정 시간 할당량을 주고 순서대로 처리하는 작업 방식, FIFO와 유사하지만 작업이 완료되지 않더라도 다른 대기 작업에게 차례를 넘기는 특징이 있다.
2. Cluster 모듈 사용 예시
코드
const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
console.log(`마스터 프로세스 아이디: ${process.pid}`);
for (let i = 0; i < numCPUs; i += 1) {
cluster.fork();
}
// 워커가 종료되었을 떄
cluster.on('exit', (worker, code, signal) => {
console.log(`${worker.process.pid}번 워커가 종료되었습니다.`);
console.log('code', code, 'signal', signal);
});
} else {
http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.write('<h1>Hello Node!</h1>');
res.end('<p>Hello Cluster!</p>');
}).listen(8086);
console.log(`${process.pid}번 워커 실행`);
}
결과
마스터 프로세스 아이디: 7700
24540번 워커 실행
27316번 워커 실행
27396번 워커 실행
25808번 워커 실행
25836번 워커 실행
26228번 워커 실행
- 마스터 프로세스는 CPU 개수 만큼 워커 프로세스를 만들고, 8086번 포트에서 대기 한다. 요청이 들어오면 만들어진 워커 프로세스에 요청을 분배한다.
- 아래와 같이 8086 포트 접속 후 1초 후에 서버가 종료되도록 해본다.
const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;
let temp = 100; // 각 워커에서 독립적으로 초기화
if (cluster.isMaster) {
console.log(`마스터 프로세스 아이디: ${process.pid}`);
for (let i = 0; i < numCPUs; i += 1) {
cluster.fork();
}
// 워커가 종료되었을 때
cluster.on('exit', (worker, code, signal) => {
console.log(`${worker.process.pid}번 워커가 종료되었습니다.`);
});
} else {
http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.write('<h1>Hello Node!</h1>');
res.end('<p>Hello Cluster!</p>');
// 1초 후에 워커 강제 종료
setTimeout(() => {
// 워커가 종료되기 전에 temp를 출력
console.log(`워커 ${process.pid}에서 temp: ${temp++}`);
process.exit(1);
}, 1000);
}).listen(8086);
console.log(`${process.pid}번 워커 실행`);
}
결과
마스터 프로세스 아이디: 41124
10972번 워커 실행
8908번 워커 실행
40652번 워커 실행
7592번 워커 실행
42724번 워커 실행
17740번 워커 실행
워커 17740에서 temp: 100
17740번 워커가 종료되었습니다.
워커 42724에서 temp: 100
42724번 워커가 종료되었습니다.
워커 7592에서 temp: 100
7592번 워커가 종료되었습니다.
워커 40652에서 temp: 100
40652번 워커가 종료되었습니다.
워커 8908에서 temp: 100
8908번 워커가 종료되었습니다.
워커 10972에서 temp: 100
10972번 워커가 종료되었습니다.
- 8086번 포트에 접속할 때 마다 워커가 하나씩 종료되면서, 모두 종료됐을 때는 최종적으로 app.js까지 종료된다.
- temp++를 이용해 temp를 증가시켜도 각각의 프로세스에서 temp는 100으로 출력되고 있다.
3. 클러스터 모듈이 동작하는 방식
- 클러스터 모듈은 마스터 프로세스와 워커 프로세스라는 두 가지 유형의 프로세스 생성
- 마스터 프로세스
- 앱이 시작될 때 하나의 마스터 프로세스가 생성
- 마스터 프로세스는 각 코어에 대해 하나의 워커 프로세스를 생성
- 마스터 프로세스는 요청을 받아서 워커 프로세스들에 분배
- 워커들의 상태를 관리하고, 필요시 워커들을 재시작하거나 종료
- 워커 프로세스
- 각 워커는 마스터 프로세스에 의해 포크(fork)되어 독립적인 Node.js 프로세스로 동작
- 워커들은 동일한 서버 포트를 공유하며, 마스터로부터 전달받은 요청 처리
- 워커들은 서로 메모리를 공유하지 않고 독립적으로 작동
4. 주요 메서드와 속성
- cluster.isMaster
- 설명 : 현재 프로세스가 마스터 프로세스인지 확인
- 용도 : 마스터 프로세스에서 수행할 작업을 정의할 때 사용
- cluster.isWorker
- 설명 : 현재 프로세스가 워커 프로세스인지 확인
- 용도 : 워커 프로세스에서 요청을 처리하는 로직을 정의할 때 사용
- cluster.fork([env])
- 설명 : 워커 프로세스 생성, env를 통해 생성될 워커 프로세스에 환경 변수를 전달 가능
- 용도 : 마스터 프로세스에서 워커 프로세스를 추가로 생성할 때 사용
- cluster.on('event', callback)
- 설명 : 클러스터 이벤트를 처리하는 이벤트 리스너를 정의한다.
- 주요 이벤트
- 'exit' : 워커가 종료될때 발생
- 'fork' : 새로운 워커가 생성될 때 발생
- 'online' : 워커가 준비되었을 때 발생
- 'listening' : 워커가 서버의 연결을 수락할 준비가 되었을 때 발생
- cluster.workers
- 설명 : 현재 생성된 모든 워커 프로세스를 담고 있는 객체. 워커들의 ID로 접근 가능
- 용도 : 특정 워커에 접근하거나 상태를 조회할 때 사용
1. cluster.isMaster
- 설명: 현재 프로세스가 마스터 프로세스인지 여부를 나타냅니다.
- 용도: 마스터 프로세스에서 수행할 작업을 정의할 때 사용합니다.
2. cluster.isWorker
- 설명: 현재 프로세스가 워커 프로세스인지 여부를 나타냅니다.
- 용도: 워커 프로세스에서 요청을 처리하는 로직을 정의할 때 사용합니다.
3. cluster.fork([env])
- 설명: 워커 프로세스를 생성합니다. env를 통해 생성될 워커 프로세스에 환경 변수를 전달할 수 있습니다.
- 용도: 마스터 프로세스에서 워커 프로세스를 추가로 포크(생성)할 때 사용합니다.
4. cluster.on('event', callback)
- 설명: 클러스터 이벤트를 처리하는 이벤트 리스너를 정의합니다. 주요 이벤트로는 exit, fork, online, listening 등이 있습니다.
- 주요 이벤트:
- 'exit': 워커가 종료될 때 발생하는 이벤트.
- 'fork': 새로운 워커가 생성될 때 발생.
- 'online': 워커가 준비되었을 때 발생.
- 'listening': 워커가 서버의 연결을 수락할 준비가 되었을 때 발생.
5. cluster.workers
- 설명: 현재 생성된 모든 워커 프로세스를 담고 있는 객체. 워커들의 ID로 접근할 수 있습니다.
- 용도: 특정 워커에 접근하거나 상태를 조회할 때 사용합니다.
2. worker thread를 이용한 멀티 스레드 방식
- 멀티 스레드 방식이란?
- 하나의 프로세스를 다수의 실행 단위로 구분하여 자원을 공유하고, 자원의 생성과 관리의 중복을 최소화하여 수행하는 능력을 향상시키는 것
- 하나의 프로그램에서 동시에 여러개의 일을 수행할 수 있도록 해주는 것이다.
- 쓰레드 간의 통신이 필요한 경우 전역 변수의 공간 또는 동적으로 할당된 공간인 힙 영역을 이용하여 데이터를 주고 받 을 수 있다.
- 본래 싱글 스레드로 동작하는 비동기 I/O 모델을 사용하지만, 워커 스레드를 통해 여러 스레드를 생성하고 병렬로 실행 가능하다.
- 장점
- 프로세스 생성은 많은 시간과 자원을 소비하는데 쓰레드는 이런 단점을 최소화 한다
- 쓰레드간 스택 영역만 비공유하고 데이터 영역과 힙 영역을 공유한다
- 쓰레드의 생성 및 컨텍스트 스위칭은 프로세스의 생성 및 컨텍스트 스위칭 보다 빠르다.
- 쓰레드 사이에서 데이터 교환에서는 특별한 기법이 필요 없다.
- 단점
- 반대로 공유하는 자원에 접근하는 부분을 신경 써주어야 한다.
- 다른 쓰레드에서 사용중인 변수나 자료 구조에 접근하여 오류가 발생하거나 수정할 수 있다.
- 동기화를 통해 작업 순서를 컨트롤하고 공유 자원에 대한 접근을 컨트롤할 필요가 있다.
- 즉, 데드락 등의 문제가 발생할 가능성이 있다.
1) 워커쓰레드 사용 예시
- 4개의 워커 쓰레드를 생성하고
- 종료할 때 마다 공유되는 counter 변수를 출력하게 된다.
// main.js
const http = require('http');
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');
const numThreads = 4; // 사용할 워커 스레드 수
let counter = 0; // 공유할 변수
if (isMainThread) {
// 워커 스레드 생성
for (let i = 0; i < numThreads; i++) {
const worker = new Worker(__filename, { workerData: { id: i + 1 } });
console.log(`Worker ${i + 1} created.`);
// 워커 종료 시 카운터 증가
worker.on('exit', () => {
counter++;
console.log(`Worker ${i + 1} terminated. Counter: ${counter}`);
});
}
// HTTP 서버 생성
const server = http.createServer((req, res) => {
if (req.url === '/') {
res.writeHead(200);
res.end(`Counter: ${counter}`);
} else {
res.writeHead(404);
res.end('Not Found');
}
});
server.listen(3000, () => {
console.log('Server listening on http://localhost:3000');
});
} else {
// 워커 스레드 - 각 워커의 ID 출력
const threadId = workerData.id;
console.log(`Worker ${threadId} started.`);
// 3초 후 워커 종료
setTimeout(() => {
console.log(`Worker ${threadId} is terminating.`);
process.exit(); // 워커 종료
}, 3000);
}
결과
Worker 1 created.
Worker 2 created.
Worker 3 created.
Worker 4 created.
Server listening on http://localhost:3000
Worker 2 started.
Worker 4 started.
Worker 1 started.
Worker 3 started.
Worker 4 is terminating.
Worker 2 is terminating.
Worker 2 terminated. Counter: 1
Worker 4 terminated. Counter: 2
Worker 1 is terminating.
Worker 3 is terminating.
Worker 1 terminated. Counter: 3
Worker 3 terminated. Counter: 4
2) 주요 구성 요소 및 메서드
- Worker 클래스 : 새로운 워커 스레드를 생성하는 클래스. 새로운 스레드는 메인 스레드와는 독립적으로 실행되며, 메인 스레드와 메시지를 통해 통신한다.
- isMainThread 변수 : 현재 실행 중인 코드가 메인 쓰레드에서 실행되는지, 워커 스레드에서 실행되는 지 확인
- parentPort : 워커 스레드가 메인 스레드와 통신할 때 사용하는 메시지 포트. 워커 스레드에서 메인 스레드로 메시지를 보내거나 메인 스레드에서 워커 스레드로 메시지를 받는데 사용한다.
- workerData : 워커 스레드를 생성할 때 메인 스레드에서 워커 스레드로 데이터를 전달할 수 있는 속성
- postMessase 및 on('message') : 메시지 기반의 통신 방법,
- postMessage : 메인 스레드 또는 워커 스레드에서 다른 스레드로 메시지를 보낸다.
- on('message') : 스레드가 상대 스레드로 부터 메시지를 받을 때 호출
- terminate() : 워크 스레드를 강제로 종료시키는 메서드
- ShareArrayBuffer & Atomics : 기본적으로 워커 스레드는 서로 독립적인 메모리 공간을 가진다. 하지만 메서드를 이용하여 스레드 간에 메모리를 공유하고, 원자적으로 데이터를 수정할 수 있다.
- ShareArrayBuffer : 공유 메모리 버퍼를 생성
- Atomics : 공유 메모리 상에서 원자적 연산을 제공하여 충돌 없이 데이터를 수정
1. Worker 클래스
const { Worker } = require('worker_threads');
// 새로운 워커 스레드 생성
const worker = new Worker('./worker.js');
2. isMainThread 변수
const { isMainThread } = require('worker_threads');
if (isMainThread) {
// 메인 스레드에서 실행
} else {
// 워커 스레드에서 실행
}
3. parentPort
// 워커 스레드에서 메인 스레드로 메시지 전송
parentPort.postMessage('Hello from worker');
// 메인 스레드에서 워커로부터 메시지 받기
worker.on('message', (msg) => {
console.log(msg);
});
4.workerData
// 메인 스레드에서 워커 스레드 생성 시 데이터 전달
const worker = new Worker('./worker.js', { workerData: { someData: 123 } });
// 워커 스레드 내에서 전달받은 데이터 사용
const { workerData } = require('worker_threads');
console.log(workerData.someData); // 123
5. postMessage 및 on('message')
// 메인 스레드
const worker = new Worker('./worker.js');
worker.postMessage('Start Work');
worker.on('message', (msg) => {
console.log(`Worker replied: ${msg}`);
});
// 워커 스레드
const { parentPort } = require('worker_threads');
parentPort.on('message', (msg) => {
console.log(`Received from main: ${msg}`);
parentPort.postMessage('Work done');
});
6. terminate()
worker.terminate().then(() => {
console.log('Worker terminated');
});
7.SharedArrayBuffer & Atomics
const sharedBuffer = new SharedArrayBuffer(1024);
const sharedArray = new Int32Array(sharedBuffer);
// Atomics를 사용하여 안전하게 값을 변경
Atomics.store(sharedArray, 0, 123);
Atomics.add(sharedArray, 0, 1);
3. 참조
1) 클러스터 모듈 : https://lgphone.tistory.com/67
2)멀티 스레딩 및 멀티프로세스와의 차이점 : https://goodgid.github.io/What-is-Multi-Thread/