상세 컨텐츠

본문 제목

[Node.js] cluster 클러스터의 이해

Node.js

by 메타샤워 2023. 7. 19. 11:31

본문

개  요
 

Node.js는 일반적으로 싱글 프로세스에서 작동한다. 

다른 WAS들이 스레드 풀을 이용하여, 접수된 각 요청에 대해 개별 스레드를 할당하는 대신, Node.js는 싱글 프로세서가 모든 요청을 직접 처리한다. 만약 요청을 처리하는데 시간 비용이 높은 작업이 존대 한다면 ( ex. DB접근 ) 메인 프로세스가 블록되어 다른 요청을 받아들이지 못할 것이다.

 

이 문제를 해결하기 위해 무거운 작업은 개별 프로세스에서 수행하고, 작업이 종료되면, 작업 결과가 메인 프로세스에 이벤트로 전달되어, 메인 프로세스는 이 결과만 처리하게 하는 콜백 패턴을 주고 사용한다. 이 과정은 잘 모듈화된 라이브러리 ( Mongo DB Driver등)를 통해 이뤄지기 때문에, 보통 개발자가 병행 프로세스를 고려할 일을 거의 발생하지 않는다.

 

이런 방식은 특별한 환경을 제외하고는 다른 WAS처럼 역동적으로 스레드를 만들거나 관리하지 않기 때문에, 이로 인한 오버헤드가 감소하여, 상대적으로 높은 성능을 보여 줄수 있다.

 

* WAS(Web Application Server) : 인터넷 상에서 HTTP를 통해 사용자 컴퓨터나 장치에 애플리케이션을 수행해주는 미들웨어. WAS는 동적 서버 콘텐츠를 수행하는 것으로 일반적인  웹 서버와 구별되며, 주로 데이터베이스 서버와 같이 수행이 된다. 

(ex  Tomcat, tMax jeus, BEA Web Logic, IBM Webspere, JBOSS, Bluestone, Gemston, inprise, Oracle, PowerTier, ....)

 

멀티 코어

 

하지만 멀티모어 CPU환경에서는 이야기가 약간 달라진다. 예를 들어 8코어 CPU에서 어떤 node.js 애플리케이션을 실행한다고 가정하다. 이 애플리케이션은 http 요청을 받아드리는 메인프로세스와 DB 조회를 담당하는 몽고DB용 워커 프로세스 , 즉 2개의 프로세스만 존재한다고 가정해보자. 프로세스가 2개 뿐이므로 , 나머지 2개의 코어는 놀게 된다.

cf) 물론 DB 모듈이 여러 프로세스를 사용하도록 잘 설계된 경우는 예외이다. 그런데 그런게 잘 있지도 않더구나.

물론 본인은 고생하는데 정작 CPU는 놀고 먹고 있는 상황을 방관하는 것은 개발자의 태도가 아니다.

 

클러스터

 

클러스터 ( cluster )는 위에서 말한 cpu의 문제점을 해결하기 위해 Node.js가 제시한 해결책으로 공급되는 Built-in 모듈이다. 

이는 아직 실험적인 기술임을 염두해 두자.

클러스터 모듈은 서버 포트를 공유하는 작식 프로세스를 쉽게 만들 수 있도록 해준다.

var cluster = require('cluster');
var http = require('http');
var numCPUs = require('os').cpus().length;
 
if (cluster.isMaster) {
    // 클러스터 워커 프로세스 포크
    for (var i = 0; i < numCPUs; i++) {
        cluster.fork();
    }
 
    cluster.on('exit', function(worker, code, signal) {
        console.log('worker ' + worker.process.pid + ' died');
    });
} else {
    http.createServer(function(req, res) {
        var str = "";
        for (var i = 0; i < 1000000; i++) {
            str += i;
        }
        res.writeHead(200);
        res.end("hello world " + process.pid + " : " + str);
    }).listen(8000);
    console.log(process.pid);
}

1/. require 를 이용해 cluster 모율을 로드한다.

5/. 현재 프로세스가 마스터 인지 아닌지 여부를 판단단다.

8/. 마스터인경우 cluster.fork()를 이용하여 cpu 갯수만큼 프로세스를 분할한다. 포크된 각 프로세스는 워커라고 불리며, 현재 모듈을 동일하게 다시 실행하지만, cluster.isMaster 의 값만 false가 된다. 그래서 워커들은 else로 분기된다.

쿼드코어라고 가정하면 1개의 마스터 클러스터 프로세스와 4개의 워커 프로세스가 만들어진다.

16~19/. 는 로드 밸런싱을 테스트하기 위해, 일부러 집어넣은 시간 낭비 코드이다. 이를 실행하고 브러우저를 여러개 열어서 localhost:8000 에 접속해면 21/. 각기 다른 pid가 출력됨을 알 수 있다. 혹시나 여러분 PC가 성능이 너무 뛰어나다면, 같은 것만 나올수 있으니 17/. 에서 더욱 뻘짓 수준의 코드로 조절해보자 

 

 

어떻게 작동하는가?

 

워커 프로세스가 server.listen(..)을 호출하면, 인자가 직렬화 된 뒤, 이 요청을 마스터 프로세스로 전달한다. 마스터 프로세스가 이미 워커의 요청사항에 해당하는 포트를 리스닝 하고 있는 경우라면, 리스닝 핸들은 워커에게 전달한다. 리스닝 하고 있지 않은 경우라면, 새로운 리스닝이 핸들을 만들고 워커에게 전달한다.

이러한 특징은 다음 3가지 경우에서, 다소 당황스러운 동작으로 이어질 수 있다.

 

server.listen( { fd : 7 } )

인자가 직렬화 되어 마스터로 전달되므로. 워커 프로세스내의 파일 기술자 7이 아닌, 마스터 프로세스에서 파일 기술자 7을 리스닝하게 된다.

 

server.listen( handle )

핸들을 리스닝하는 것은 마스터에게 이를 알리지 않고, 워커가 주어진 핸들을 명시적으로 리스닝하게 한다. 워커가 이미 핸들을 가지고 있다면, 뭔 짓을 하고 있는 중인지 개발자가 알고 있다는 것을 당연하다고 본다더라... ( ....?? )

 

server.listen( 0 )

일반적으로 0번 포트를 리스닝하는 것은 랜덤한 포트를 리스닝 하는것이다. 하지만 클러스터에서는 각 워커는 동일한 랜덤 포트를 받게 된다. 첫번째 워커만 랜덤포트를 부여받고, 나머지는 동일한 포트를 부여받는다. 고유의 포트를 리스닝 하고 싶다면, 클러슽 워커 아이디를 시드로 하는 포트넘버를 생상하는 등의 방법을 쓰면 된다.

 

여러 프로세스가 동시에 기반 자원을 접근할 때, OS가 로듣 밸런싱을 수행한다. Node.js자체에 라우팅 로직은 존재 하지 않으며, 클러스터의 워커들은 개별프로세스로 어떠한 상태 ( 메모리 ) 도 공유하지 않는다. 따라서, 애플리케이션이 세션이나 로그인 같은 데이터를 메모리를 통해 운용하지 않도록 설계하는것이 매우 중요하다. Redis나 Mongo등의  스토리지를 통해 세션을 관리해야 한다. 워커 프로세스는 모두 독립적이므로, 요구사항에 따라 respawn 되거나 Killed 될 수 있다. 하나라도 워커가 살아남아 있다면, 서비스는 계속 작동한다. Node.js는 이를 관리 해 주지 않으며, 이러한 처리는 개발자가 직접 해야한다. ( 프로세스 장애로그나, 복구,... 이런거?)

 

마무으리

 

 Node.js의 클러스터는 굉장히 단순하다. OS의 로드 밸런싱을 이용하여, 독립된 프로세스들이 포트를 공유하는 것을 허용하도록 한다. 워커들이 각기 독립된 프로세스에서 구동되므로, 메모리 상태 공유가 불가능 하다는것만 염두에 두면, 놀고 있는 CPU 코어들을 충분히 부려 먹을 수 있을 것이다.

관련글 더보기