Node.js 인터뷰 질문 22

질문: Node.js의 클러스터(Cluster) 모듈이 무엇이며 어떻게 활용할 수 있나요?

답변:

Node.js의 클러스터(Cluster) 모듈은 단일 스레드로 동작하는 Node.js 애플리케이션의 한계를 극복하고 멀티 코어 시스템의 성능을 최대한 활용하기 위한 모듈입니다. 이 모듈을 사용하면 여러 개의 워커 프로세스를 생성하여 부하를 분산시키고 애플리케이션의 가용성과 확장성을 향상시킬 수 있습니다.

클러스터 모듈의 기본 원리

Node.js는 기본적으로 단일 스레드로 실행됩니다. 이는 다중 코어 CPU 환경에서 하나의 코어만 사용하게 되어 자원을 효율적으로 활용하지 못하는 문제가 있습니다. 클러스터 모듈은 이러한 제한을 해결하기 위해 여러 개의 자식 프로세스(워커)를 생성하고, 이들 간에 부하를 분산시키는 기능을 제공합니다.

기본 사용법

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++) {
    cluster.fork();
  }

  // 워커 종료 이벤트 처리
  cluster.on("exit", (worker, code, signal) => {
    console.log(`워커 ${worker.process.pid} 종료됨`);
    // 워커가 종료되면 새로운 워커 생성 (선택사항)
    console.log("새 워커 생성 중...");
    cluster.fork();
  });
} else {
  // 워커들은 HTTP 서버를 생성하고 동일한 포트를 공유합니다
  http
    .createServer((req, res) => {
      res.writeHead(200);
      res.end(`워커 ${process.pid}가 응답합니다\n`);
    })
    .listen(8000);

  console.log(`워커 ${process.pid} 시작됨`);
}

위 코드에서는 시스템의 CPU 코어 수만큼 워커 프로세스를 생성하고, 각 워커는 동일한 포트(8000)에서 HTTP 서버를 실행합니다. 들어오는 요청은 워커들 간에 자동으로 분산됩니다.

클러스터 모듈의 주요 기능과 속성

1. cluster.isMaster와 cluster.isWorker

프로세스가 마스터인지 워커인지 식별할 수 있습니다.

if (cluster.isMaster) {
  // 마스터 프로세스 코드
} else {
  // 워커 프로세스 코드
}

2. cluster.fork()

새로운 워커 프로세스를 생성합니다.

const worker = cluster.fork();

환경 변수를 전달할 수도 있습니다:

const worker = cluster.fork({ WORKER_ID: 1 });

3. worker.id와 worker.process

워커의 고유 ID와 프로세스 객체에 접근할 수 있습니다.

console.log(`워커 ID: ${worker.id}, 프로세스 ID: ${worker.process.pid}`);

4. 이벤트 처리

클러스터 모듈은 여러 이벤트를 통해 워커의 생명주기를 관리할 수 있습니다.

// 워커 온라인 이벤트
cluster.on("online", (worker) => {
  console.log(`워커 ${worker.process.pid} 온라인`);
});

// 워커 종료 이벤트
cluster.on("exit", (worker, code, signal) => {
  console.log(
    `워커 ${worker.process.pid} 종료: 코드 ${code}, 시그널 ${signal}`
  );

  if (signal) {
    console.log(`워커가 시그널 ${signal}에 의해 종료됨`);
  } else if (code !== 0) {
    console.log(`워커가 오류로 종료됨 (코드: ${code})`);
    // 워커 재시작
    cluster.fork();
  } else {
    console.log("워커가 정상적으로 종료됨");
  }
});

// 워커 간 통신 이벤트
cluster.on("message", (worker, message, handle) => {
  console.log(`마스터가 워커 ${worker.id}로부터 메시지 수신:`, message);
});

워커 간 통신

마스터와 워커 간, 또는 워커 간 통신을 위해 메시지를 주고받을 수 있습니다.

마스터에서 워커로 메시지 보내기:

if (cluster.isMaster) {
  const worker = cluster.fork();
  worker.send({ msg: "워커에게 보내는 메시지" });
}

워커에서 마스터로 메시지 보내기:

if (cluster.isWorker) {
  process.send({ msg: "마스터에게 보내는 메시지" });
}

메시지 수신:

// 마스터에서 수신
cluster.on("message", (worker, message) => {
  console.log(`워커 ${worker.id}로부터 메시지:`, message);
});

// 워커에서 수신
process.on("message", (message) => {
  console.log("마스터로부터 메시지:", message);
});

로드 밸런싱 전략

클러스터 모듈은 기본적으로 라운드 로빈 알고리즘을 사용하여 요청을 워커들에게 분배합니다. 하지만 일부 플랫폼(특히 Windows)에서는 라운드 로빈이 아닌 다른 방식으로 동작할 수 있습니다.

Node.js 8.0.0부터는 schedulingPolicy 옵션을 사용하여 로드 밸런싱 방식을 선택할 수 있습니다:

const cluster = require("cluster");

cluster.schedulingPolicy = cluster.SCHED_RR; // 라운드 로빈
// 또는
cluster.schedulingPolicy = cluster.SCHED_NONE; // 운영체제에 위임

제로 다운타임 재시작 구현

클러스터를 활용하여 애플리케이션을 업데이트할 때 다운타임 없이 재시작하는 방법을 구현할 수 있습니다:

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++) {
    cluster.fork();
  }

  // 제로 다운타임 재시작 함수
  function restartWorkers() {
    const workers = Object.values(cluster.workers);

    let i = 0;
    const restartWorker = () => {
      if (i >= workers.length) return;

      const worker = workers[i++];
      console.log(`워커 ${worker.process.pid} 재시작 중...`);

      // 새 워커 생성
      const newWorker = cluster.fork();

      // 새 워커가 준비되면 이전 워커 종료
      newWorker.on("listening", () => {
        worker.disconnect();
      });

      // 이전 워커가 종료되면 다음 워커 재시작
      worker.on("exit", () => {
        console.log(`워커 ${worker.process.pid} 종료됨`);
        restartWorker();
      });
    };

    restartWorker();
  }

  // SIGUSR2 시그널 수신 시 워커들 재시작
  process.on("SIGUSR2", () => {
    console.log("SIGUSR2 수신, 워커들 재시작 중...");
    restartWorkers();
  });

  // 워커 종료 처리
  cluster.on("exit", (worker, code, signal) => {
    if (signal !== "SIGTERM" && signal !== "SIGINT") {
      console.log(`워커 ${worker.process.pid} 죽음, 새 워커 생성 중...`);
      cluster.fork();
    }
  });
} else {
  // 워커에서 HTTP 서버 생성
  http
    .createServer((req, res) => {
      res.writeHead(200);
      res.end(`워커 ${process.pid}가 응답\n`);
    })
    .listen(8000);

  console.log(`워커 ${process.pid} 시작됨`);
}

console.log(
  `프로세스 ${cluster.isMaster ? "master" : "worker"} ${process.pid} 시작됨`
);

이 구현에서는 SIGUSR2 시그널을 수신하면 순차적으로 워커를 재시작합니다. 각 워커가 준비되면 이전 워커를 종료하므로 서비스 중단 없이 업데이트가 가능합니다.

실제 사용 예시: Express 애플리케이션과 함께 클러스터 사용

const cluster = require("cluster");
const express = require("express");
const numCPUs = require("os").cpus().length;

if (cluster.isMaster) {
  console.log(`마스터 프로세스 ${process.pid} 실행 중`);

  // 워커 프로세스 포크
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  // 워커 종료 이벤트 처리
  cluster.on("exit", (worker, code, signal) => {
    console.log(`워커 ${worker.process.pid} 종료됨`);
    console.log("새 워커 생성 중...");
    cluster.fork();
  });
} else {
  // Express 앱 생성
  const app = express();

  app.get("/", (req, res) => {
    // CPU 부하 시뮬레이션
    let result = 0;
    for (let i = 0; i < 1e7; i++) {
      result += i;
    }

    res.send(`결과: ${result}, 프로세스 ID: ${process.pid}`);
  });

  const PORT = process.env.PORT || 3000;
  app.listen(PORT, () => {
    console.log(`워커 ${process.pid}가 포트 ${PORT}에서 실행 중`);
  });
}

이 예제에서는 Express 애플리케이션을 여러 워커에서 실행하여 CPU 사용량이 많은 작업을 분산시킵니다.

클러스터 사용 시 고려사항

  1. 공유 자원 관리: 워커 프로세스는 개별 메모리 공간을 가지므로, 세션 저장소와 같은 공유 상태를 관리하려면 Redis와 같은 외부 저장소를 사용해야 합니다.

  2. 로그 관리: 여러 워커가 동시에 로그를 출력하면 로그가 섞일 수 있으므로, 로그 수집 및 관리 전략을 신중히 계획해야 합니다.

  3. Worker IPC 한계: 프로세스 간 통신(IPC)은 성능 오버헤드가 있으므로, 워커 간에 대량의 데이터를 전송하는 것은 피해야 합니다.

  4. CPU 바운드 vs I/O 바운드: 클러스터는 주로 CPU 바운드 작업의 성능을 향상시키는 데 효과적입니다. I/O 바운드 애플리케이션의 경우, 성능 향상이 제한적일 수 있습니다.

  5. 클러스터 대안: PM2와 같은 프로세스 관리자는 더 많은 기능과 더 나은 관리 도구를 제공하므로, 프로덕션 환경에서 클러스터 모듈 대신 사용하는 것을 고려할 수 있습니다.

PM2를 활용한 클러스터링

PM2는 Node.js 애플리케이션을 위한 프로덕션 프로세스 관리자로, 클러스터 모듈을 더 쉽게 활용할 수 있게 해줍니다:

# 설치
npm install pm2 -g

# 클러스터 모드로 앱 실행 (max: 모든 CPU 코어 사용)
pm2 start app.js -i max

# 제로 다운타임 재시작
pm2 reload app

클러스터 모듈은 Node.js 애플리케이션의 확장성을 높이고 다중 코어 시스템의 성능을 최대한 활용할 수 있는 강력한 도구입니다. 하지만 공유 상태 관리나 로깅과 같은 측면에서 추가적인 설계 고려사항이 필요합니다.

results matching ""

    No results matching ""