서버 작업을 하다보니 "node-schedule" 모듈을 사용할 일이 생겼었다.
서버는 node.js express 를 사용하면서 apollo server 를 사용했는데 API 와 스케쥴러를 동시에 사용하고 있고, 같은 리소스를 쓰는 다른 프로젝트 또한 구동되고 있어서 4코어를 가지고 있는 cpu 임에도 불구하고 요청과 응답이 너무 느리고 이벤트루프에 스택이 쌓이는게 느껴질 정도였다.
여러번 cluster 를 사용 해보려 했으나 제대로 찾아보고 하지 않은 탓에 무작정 cluster 모듈을 사용해서 서버를 구동하려 하니 제대로 동작하지 않는 느낌이었다.
그래서 찾아보는 도중 pm2 가 cluster 를 지원하며 pm2 의 강점이라기에 pm2 의 cluster 기능을 구현해 보았다.
원래 서버를 pm2 로 구현하고 있었으나 cluster 를 사용하고 있진 않았다.
우선 node.js 특징부터 알아보자면 node.js 는 싱글스레드이며, 그래서 많은 연산을 요구하는 작업에는 약하다고 한다.
node.js 를 공부하다보면 이벤트 루프라는 것이 등장하는데
node.js 는 이 이벤트 루프라는 구조를 가지고 있다.
client 에서 요청이 들어오면 우선 call stack 에 쌓이고 이를 queue 에서 처리하는 방식이다.
그래서 어쨋든 싱글이니 여럿이 처리하는 것보다 성능이 뒤처진다고 볼 수 있다.
이를 보완하는 것이 cluster 이며 쉽게 할 수 있게 해주는 모듈이 pm2 이다.
현재 상황은 node.js express 를 사용하며 모듈 형식을 따르고 있다.
pm2 를 통해서 server.js 를 실행시킨다면
pm2 start server.js
상기와 같이 실행할 것이다.
그러면 pm2 list 를 통해서 실행된 프로세스를 확인할 수 있다.
하지만 cluster 를 사용할 것이므로 root 디렉토리에
ecosystem.config.js 파일을 생성한다.
//ecosystem.config.js
module.exports = {
apps: [{
name: "server", //1
script: "./server.js", //2
instances: 0, //3
exec_mode: "cluster" //4
instance_var: "INSTANCE_ID", //5
}]
}
하지만 나의 경우 프로젝트가 전부 ESM 방식으로 작성되어 단순히 실행할 경우
"import" 를 사용할 수 없다는 에러가 발생한다.
이를 처리하기 위해 몇가지 선행작업이 필요한데,
총 세가지 모듈이 추가적으로 설치되어야 한다.
npm install --save-dev @babel/register
npm install --save-dev @babel/plugin-transform-runtime
npm install --save @babel/runtime
첫번째로 @babel/register 는 앱이 구동될 때 ESM 방식을 사용할 수 있도록 해준다.
이를 선행 처리해야 하므로 file 을 하나 생성해서 해당 모듈과 server.js 파일을 import 하도록 한다.
//server-register.js
require("@babel/register");
require("./server.js");
그리고 ecosystem.config.js 파일을 하기와 같이 변경한다.
//ecosystem.config.js
module.exports = {
apps: [{
name: "server", //1
script: "./server-register.js", //2
instances: 0, //3
exec_mode: "cluster" //4
instance_var: "INSTANCE_ID", //5
}]
}
이제 기본적인 준비는 완료되었다. 하기의 코드로 서버를 구동할 수 있다.
pm2 start ecosystem.config.js
하지만 실행 도중 다른 에러에 직면했다.
ReferenceError: regeneratorRuntime is not defined
프로젝트에 async/await 을 사용하는데 이를 번역하는 regeratorRuntime 가 없어서 발생하는 문제였다.
npm install --save-dev @babel/plugin-transform-runtime
npm install --save @babel/runtime
상기의 남은 두가지 모듈인데, 이 모듈들이 해당 에러를 해결할 것이다.
이미 ESM 방식으로 프로젝트를 작성했다면 babel.config.js 파일이 있을것이다.
해당 모듈을 설치 후 해당 파일에 하기와 같이 기입한다.
//babel.config.js
{
...
"plugins": ["@babel/plugin-transform-runtime"]
}
이후 다시 실행하니 문제없이 서버가 구동되었다.
프로세스를 나눠서 작업 할당하기
현재 프로젝트는 node-schedule 을 통해 1분 주기의 작업과 API 의 요청에 응답하는 두가지의 일을 하고 있다.
실행되고 있는 총 프로세스의 수가 4개이고 첫번째 프로세스만 sheduler 를 실행시키기 위해 작업을 나누려고 한다.
ecosystem.config.js 파일을 확인하면 instance_var 의 프로세스명을 지정할 수 있는데, 여기에서는 "INSTANCE_ID" 라고 지정했다.
기본값은 "NODE_APP_INSTANCE" 라고 한다.
이제 매 프로세스마다 "process.env.INSTANCE_ID" 를 통해 해당 프로세스의 id 를 가져올 수 있다.
process.env.INSTANCE_ID //0, 1, 2, 3
이를 통해 조건문으로 scheduler 와 API 의 작업을 분리할 수 있게되었다.
REFS
https://engineering.linecorp.com/ko/blog/pm2-nodejs
https://songjang.tistory.com/11
https://velog.io/@haebin/React-regeneratorRuntime-is-not-defined-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0
https://gareen.tistory.com/95