Node.js Cluster (클러스터) 완벽 해부: 핵심 개념 파헤쳐보기 - Node.js (노드js) 성능 향상의 비밀, 알아볼까요?
혹시 PM2 (PM2)로 Node.js (노드js) 프로세스를 관리해 보신 적 있으신가요?
PM2 (PM2)에는 클러스터 모드라는 기능이 있는데요.
이 모드를 사용하면 Node.js (노드js)가 여러 개의 프로세스를 만들 수 있다는 사실, 알고 계셨나요?
클러스터 모드에서 인스턴스 수를 max
로 설정하면 PM2 (PM2)가 서버의 CPU (중앙처리장치) 코어 수에 맞춰서 Node (노드) 프로세스를 자동으로 생성해줍니다.
정말 편리하죠?
PM2 (PM2)가 이런 기능을 제공할 수 있는 건 바로 Node.js (노드js)의 Cluster (클러스터) 모듈 덕분인데요.
Cluster (클러스터) 모듈은 Node.js (노드js)가 원래 싱글 스레드 기반이라서 CPU (중앙처리장치) 코어를 다 활용하기 어렵다는 단점을 해결해줍니다.
그런데 Cluster (클러스터) 모듈이 속으로는 어떻게 작동하는 걸까요?
프로세스들은 서로 어떻게 통신할까요?
여러 프로세스가 어떻게 하나의 포트를 같이 사용할 수 있을까요?
그리고 Node.js (노드js)는 요청을 받으면 어떤 프로세스에게 전달해줄까요?
이런 궁금증들이 마구 샘솟는다면, 지금부터 함께 자세히 알아보도록 하겠습니다.
핵심 원리
Node.js (노드js) 워커 프로세스는 child_process.fork()
메서드를 사용해서 만들어집니다.
그래서 Cluster (클러스터) 모듈에서는 하나의 부모 프로세스와 여러 개의 자식 프로세스가 생기는 구조인데요.
코드를 보면 보통 이렇게 생겼습니다.
const cluster = require('cluster');
const os = require('os');
if (cluster.isMaster) {
for (let i = 0, n = os.cpus().length; i < n; i++) {
cluster.fork();
}
} else {
// 애플리케이션 시작
}
운영체제를 공부해보신 분이라면 fork()
시스템 호출이 익숙하실 텐데요.
fork()
를 호출한 프로세스가 부모 프로세스가 되고, 새로 만들어진 프로세스들이 자식 프로세스가 됩니다.
자식 프로세스들은 부모 프로세스와 같은 데이터 영역과 스택을 공유하지만, 물리적인 메모리 공간까지 공유하는 건 아닙니다.
Node.js (노드js) Cluster (클러스터)에서 마스터 프로세스는 포트를 열고 요청을 받아서 워커 프로세스들에게 나눠주는 역할을 합니다.
여기서 중요한 핵심 주제 3가지가 있는데요.
바로 프로세스 간 통신 (IPC), 로드 밸런싱 전략, 멀티 프로세스 포트 공유입니다.
하나씩 자세히 알아볼까요?
프로세스 간 통신 (IPC)
마스터 프로세스는 process.fork()
를 사용해서 자식 프로세스를 만듭니다.
이 프로세스들끼리 통신은 IPC (Inter-Process Communication) 채널을 통해서 이루어지는데요.
운영체제는 프로세스 간 통신을 위해 여러 가지 방법을 제공합니다.
- 공유 메모리 (Shared Memory): 여러 프로세스가 하나의 메모리 공간을 공유하는 방식입니다. 주로 세마포어 (Semaphore)를 사용해서 동기화 및 상호 배제를 관리합니다.
- 메시지 전달 (Message Passing): 프로세스끼리 메시지를 주고받으면서 데이터를 교환하는 방식입니다.
- 세마포어 (Semaphore): 시스템이 할당하는 상태 값입니다. 제어 권한이 없는 프로세스는 특정 시점에서 멈춰서서 신호를 기다리게 됩니다. 이 값이 0 또는 1로 제한되면 "뮤텍스 (Mutex)" (상호 배제 락)라고 부릅니다.
- 파이프 (Pipes): 두 프로세스를 연결해서 한 프로세스의 출력이 다른 프로세스의 입력으로 들어가도록 하는 방식입니다.
pipe
시스템 호출을 사용해서 만들 수 있습니다. 쉘 스크립트에서|
명령어가 바로 이 방식을 사용하는 대표적인 예시입니다.
Node.js (노드js)는 부모 프로세스와 자식 프로세스 간 통신에 이벤트 기반 메커니즘을 사용합니다.
다음은 부모 프로세스가 TCP (Transmission Control Protocol) 서버 핸들을 자식 프로세스에게 보내는 예시 코드입니다.
const subprocess = require('child_process').fork('subprocess.js');
// 서버를 만들고 핸들을 보냅니다.
const server = require('net').createServer();
server.on('connection', (socket) => {
socket.end('부모 프로세스에서 처리함');
});
server.listen(1337, () => {
subprocess.send('server', server);
});
process.on('message', (m, server) => {
if (m === 'server') {
server.on('connection', (socket) => {
socket.end('자식 프로세스에서 처리함');
});
}
});
로드 밸런싱 전략
앞서 말씀드렸듯이 모든 요청은 마스터 프로세스가 받아서 나눠주는데요.
서버 부하가 워커 프로세스들에게 골고루 분산되도록 하려면 로드 밸런싱 전략이 필요합니다.
Node.js (노드js)는 기본적으로 라운드 로빈 (Round-Robin) 알고리즘을 사용합니다.
라운드 로빈 (Round-Robin)
라운드 로빈 (Round-Robin) 방식은 Nginx (엔진엑스)에서도 사용하는 일반적인 로드 밸런싱 알고리즘인데요.
들어오는 요청을 첫 번째 프로세스부터 시작해서 순서대로 워커 프로세스들에게 돌아가면서 분배하는 방식입니다.
마지막 프로세스까지 요청을 분배한 후에는 다시 첫 번째 프로세스로 돌아가서 순환하는 방식이죠.
하지만 이 방식은 모든 프로세스의 처리 능력이 똑같다고 가정하고 있기 때문에, 요청 처리 시간이 들쭉날쭉한 경우에는 로드 불균형이 발생할 수 있습니다.
이런 문제를 해결하기 위해 Nginx (엔진엑스)에서는 가중 라운드 로빈 (Weighted Round-Robin, WRR) 방식을 많이 사용하는데요.
서버마다 가중치를 다르게 줘서, 가중치가 높은 서버를 먼저 선택하고, 가중치가 0이 되면 다시 새로운 가중치 순서에 따라 순환을 시작하는 방식입니다.
Node.js (노드js)에서는 NODE_CLUSTER_SCHED_POLICY
환경 변수를 설정하거나 cluster.setupMaster(options)
를 통해서 로드 밸런싱 전략을 조절할 수 있습니다.
여러 대의 서버를 사용하는 클러스터 환경에서는 Nginx (엔진엑스)를 사용하고, 한 대의 서버에서 멀티 프로세스 로드 밸런싱을 할 때는 Node.js (노드js) Cluster (클러스터)를 함께 사용하는 것이 일반적인 방법입니다.
멀티 프로세스 포트 공유
Node.js (노드js) 초기 버전에서는 여러 프로세스가 같은 포트를 공유하려고 하면 서로 접속을 경쟁하는 문제가 있어서 로드 밸런싱이 제대로 되지 않았습니다.
하지만 라운드 로빈 (Round-Robin) 전략이 도입되면서 이 문제가 해결되었는데요.
현재는 다음과 같은 방식으로 작동합니다.
- 마스터 프로세스가 소켓을 만들고, 주소를 바인딩하고, 리스닝을 시작합니다.
- 소켓의 파일 디스크립터 (fd)는 워커 프로세스들에게 전달되지 않습니다.
- 마스터 프로세스가 새로운 연결을 수락하면, 어떤 워커 프로세스가 연결을 처리해야 하는지 결정하고 해당 워커 프로세스에게 연결을 전달합니다.
결국 마스터 프로세스가 포트를 열고 리스닝하면서 정의된 전략 (예: 라운드 로빈)에 따라 워커 프로세스들에게 연결을 분배하는 구조입니다.
이렇게 하면 워커 프로세스 간의 경쟁은 없어지지만, 마스터 프로세스가 매우 안정적으로 작동해야 한다는 단점이 있습니다.
결론
PM2 (PM2) 클러스터 모드를 시작점으로 해서, Node.js (노드js) Cluster (클러스터) 모듈이 멀티 프로세스 애플리케이션을 구현하는 데 사용하는 핵심 원리들을 알아보았습니다.
프로세스 간 통신, 로드 밸런싱, 멀티 프로세스 포트 공유 이 세 가지 핵심 주제에 집중해서 설명했는데요.
Cluster (클러스터) 모듈을 자세히 살펴보니 운영체제, 서버 로드 밸런싱 등 다양한 분야에서 사용하는 기본적인 원리와 알고리즘들이 정말 널리 쓰인다는 것을 다시 한번 확인할 수 있었습니다.
예를 들어 라운드 로빈 (Round-Robin) 알고리즘은 운영체제 프로세스 스케줄링, 서버 로드 밸런싱 모두에서 사용되고, 마스터-워커 구조는 Nginx (엔진엑스) 멀티 프로세스 설계와 비슷합니다.
세마포어 (Semaphore), 파이프 (Pipes) 같은 메커니즘도 여러 프로그래밍 패러다임에서 흔하게 볼 수 있고요.
새로운 기술들이 계속 쏟아져 나오지만, 결국 기본은 변하지 않는 것 같습니다.
이런 핵심 개념들을 제대로 이해하고 있으면 어떤 새로운 기술이 나와도 자신감을 가지고 응용하고 적응할 수 있을 거라고 생각합니다.
'Javascript' 카테고리의 다른 글
대용량 파일 업로드 최적화 방법: 초보자도 쉽게 따라 하는 6가지 전략 (0) | 2025.03.24 |
---|---|
AbortController, 아직 제대로 모르세요? - 숨겨진 기능부터 활용 꿀팁까지! (0) | 2025.03.22 |
JavaScript 디버깅의 숨겨진 보석: error.cause, 에러의 근본 원인을 쉽게 찾아볼까요? (0) | 2025.03.22 |
Node.js Event Loop 속속들이 파헤쳐보기: 싱글 스레드 모델의 비밀, 알아볼까요? (0) | 2025.03.22 |
TypeScript 컴파일러, Go 언어로 갈아탄다고요?! 속사정 한번 알아볼까요? (0) | 2025.03.22 |