NodeJS에서 멀티 코어로 작업하기
컴퓨터의 구성요소 중 CPU는 우리의 뇌와 같다. CPU 코어가 여러 개 있다는 것은 여러 개의 뇌로 연산을 할 수 있다는 것과 동일하다. 예를 들어 24158325497 같은 수가 소수인지 아닌지를 하나의 코어가 있을 때보다 여러 개의 CPU를 활용할 때 훨씬 빠르게 계산할 수 있다.
NodeJS의 가장 큰 장점은 싱글 스레드(개발자가 다룰 수 있는 부분)이기 때문에 멀티 스레드처럼 공유 자원의 경쟁 문제 등에서 자유롭고 좀 더 쉬운 프로그래밍이 가능하다는 것이다. 반면, 싱글 스레드이기 때문에 하나의 CPU만 활용할 수 있다는 단점이 있다. 따라서 CPU 집약적 작업을 처리하는 NodeJS 서버에 많은 요청이 동시에 들어왔을 때 서버 컴퓨터가 이들을 빠르게 연산할 수 있는 조건이 됨에도 불구하고 이를 활용하지 못해 응답을 지연시키는 일이 발생할 수 있다. NodeJS가 CPU 자원을 모두 사용할 수 있도록 하는 방법이 없을까?
NodeJS에서 여러 개의 CPU를 사용하기 위한 방법은 Worker Thread, Child Process, Cluster 3가지 방법이 있다. 이 3가지 방법이 어떤 것인지, 어떤 장단점이 있는지 살펴보자.
1. Worker Thread
Worker Thread는 NodeJS를 멀티 스레드로 구동되도록 한다. 하나의 메인스레드가 있고, 그 아래에 여러 개의 워커 스레드를 두 개 하여, 메인 스레드가 워커 스레드들에게 작업을 분배하는 방식이다. 각 Worker Thread마다 각각의 이벤트 루프와 V8 엔진이 존재한다. 따라서 각 Worker Thread는 힙을 공유하지 않고 격리된 코드를 실행할 수 있다. 이때 작업 분배를 어떻게 할 것인지는 메인 스레드에서 결정해주어야 한다.
예를 들어 자연수 n을 전송하면 0~n까지의 수에서 소수의 갯수를 찾아주는 서버가 있다고 생각해보자. 예를 들어 서버에 워커 스레드가 4이고, 10,000을 서버에 전송했다면, 메인 스레드는 각 워커 스레드에게 0~2500, 2501~5000, 5001~7500, 7501~10000의 범위에서 소수를 찾도록 작업을 분배할 수 있다. 즉, 작업을 워커 스레드 개수만큼 n등분하여 병렬 처리할 수 있는 것이다.
Worker Thread의 가장 적절한 수는 얼마일까? 보통 Worker Thread의 수는 코어 갯수와 동일하게 하는 것이 가장 좋다. 코어보다 더 적은 수라면 그만큼 코어를 다 활용하지 못하는 것이고, 코어보다 많은 수라면 하나의 코어에서 작업을 위해 문맥 교환이 일어나면서 비효율이 생길 수 있기 때문이다.
CPU활용을 위해 NodeJS에서 Worker Thread를 무조건 사용하는 것이 좋을까? 앞서 말했듯이 NodeJS의 가장 큰 장점은 싱글 스레드이므로 멀티 스레드에서 발생하는 여러 문제들을 개발자가 신경 쓰지 않고 개발할 수 있는 것이 큰 장점이다. NodeJS에서 Worker Thread를 반드시 써야 하는 상황이라면 서버를 NodeJS로 만들 이유가 없는 상황일 수도 있다. 또한 스레드를 생성하기 위한 작업 자체에도 자원이 소모되므로 CPU 집약적 작업이 아니라면 오히려 성능이 떨어질 수도 있는 위험이 있다.
2. Child Process
Child Process는 새로운 자식 프로세스를 생성한다. 즉, 새롭게 프로그램을 실행할 수 있다는 의미이다. 예를 들어 NodeJS 서버를 새롭게 띄우거나 다른 언어로된 프로그램을 실행할 수 있다.
3. Cluster
Cluster는 같은 NodeJS 서버를 여러 개 띄우는 것으로, Child Process를 기반으로 만들어진 기능이다. Child Process와 다른 점은 NodeJS 서버들이 같은 포트를 공유할 수 있다는 점과 받은 요청을 각 서버로 전달하는 스케쥴링 정책을 설정할 수 있다는 점이다.
Child Process와 Cluster를 이용하면 같은 코드에서 비롯된 NodeJS 서버를 여러개 띄우고, 요청이 들어오면 각 서버에 요청을 할당한다. 즉, 각각의 요청을 병렬 처리할 수 있게 되는 것이다. 이렇게 하면 CPU 부하가 큰 하나의 요청이 들어와도 다른 요청이 앞의 요청의 처리가 끝날 때까지 기다리지 않고, 다른 서버를 이용하여 응답을 받을 수 있다.
Cluster를 사용할 때 단점은 세션과 같은 데이터를 공유할 수 없다는 점이다. 이는 일반적으로 Redis 서버를 도입하여 해결한다. 그 외에 여러 개의 프로세스를 다루기 위한 추가적인 설계가 필요하다. 예를 들어 프로세스가 메모리 제한에 도달하거나, 예상치 못한 오류로 종료되었을 때 어떻게 종료 이벤트를 전달할지, 서버가 변경되었을 때 어떻게 재시작을 처리할지 등의 문제이다. 이러한 문제는 일반적으로 PM2를 활용하여 해결한다.
프로세스도 코어 갯수와 동일하게 하는 것이 가장 좋다. Worker Thread의 수를 코어의 개수와 동일하게 하는 것과 동일한 이유이다.