...
Node.js 싱글 스레드
Node.js는 Chrome의 V8 자바스크립트 엔진으로 빌드된 자바스크립트 런타임(runtime)으로 ‘Event Driven’, ‘Non-Blocking I/O’ 모델을 사용해 가볍고 성능이 뛰어나 높은 평가를 받고 있다.
Node.js는 기본적으로 싱글 스레드(thread)로 돌아간다.
Node.js 애플리케이션은 단일 CPU 코어에서 실행되기 때문에 CPU의 멀티코어 시스템은 사용할 수 없다.
그래서 만약 서버의 사양이 8코어이며 16쓰레드면, 프로그램을 돌리는데 최대 16개 코어를 사용 할 수 있지만, 노드는 싱글 스레드 이기 때문에 모든 코어를 사용하지 못해 최대 성능을 내지 못하는, 즉 자원을 제대로 활용하지 못한다.
그래서 Node.js는 이런 문제를 해결하기 위해 클러스터(Cluster) 모듈을 통해 단일 프로세스를 멀티 프로세스(Worker)로 늘릴 수 있는 방법을 제공한다. 개발자는 클러스터 모듈을 사용해서 마스터 프로세스에서 코어 수만큼 워커 프로세스를 생성해서 모든 코어를 사용하게끔 로직을 짜면, 자원 낭비 없이 서버를 돌릴수 있다.
만일 노드의 멀티 프로세스/쓰레드 기법에 대해 알고 싶다면 다음 포스팅을 참고 바란다.
이것이 서버 프로그래밍의 본좌지만, 서비스를 배포하는데 있어 개발 주기가 늘어나게 된다는 단점이 있다.
예를 들어 워커 프로세스가 생성됐을 때 이벤트가 마스터 프로세스로 전달되면 어떻게 처리할지, 워커 프로세스와 마스터 프로세스에 수식 실행 로직을 어떻게 분리해서 구성해야 할지, 워커 프로세스가 메모리 제한선에 도달하거나 예상치 못한 오류로 종료되면서 종료(exit) 이벤트를 전달할 땐 어떻게 처리할지 등등 고민할 게 많다.
따라서 이런 최적화 작업들을 직접 구현하는 것 보다는 솔루션을 사용해 문제를 간편하게 해결하는 것이 옳은 방법이다.
다행히 노드 진영에는 PM2라는 Node.js의 프로세스 매니저가 존재한다.
이번 시간에는 PM2 사용법을 살펴보고, 노드 애플리케이션을 무중단으로 서비스 운영하는지에 대해서 알아보는 시간을 가져보겠다.
pm2 (Process Manager) 모듈
PM2는 P(Process) M(Manager) 2의 약자로 NodeJS 프로세서를 관리하는 원활한 서버 운영을 위한 패키지 이다.
PM2는 대표적으로 다음과 같은 기능을 제공해준다.
- 서비스를 제공하고 있는 도중 갑자기 서버가 중지되도 서버를 다시 켜준다
- Node.js는 싱글 스레드 기반이지만, 멀티 코어 혹은 하이퍼 스레딩을 사용할수 있게 해준다
- 클라이언트로부터 요청이 올 때 알아서 요청을 여러 노드 프로세스에 고르게 분배한다. (로드 밸런싱)
따라서 하나의 프로세스가 받는 부하가 적어지므로 서비스를 더 원활하게 운영할 수 있게 된다.
개발할 때 nodemon을 쓴다면, 배포할 때는 pm2를 쓴다.
물론 pm2에도 단점도 존재한다.
멀티 스레딩이 아니므로 서버의 메모리 같은 자원을 공유하지는 못한다.
예를들어 서버를 운영하는데 있어 가장 많이 사용하는 세션을 메모리에 저장하고 사용해왔는데, 메모리를 공유하지 못한다는 말은 서비스에 큰 지장을 주게 된다.
예를들어 웹서비스에 로그인을 하면 사용자의 정보가 세션에 들어가 있게 되는데, 그 후 새로고침을 반복할 때 세션 메모리가 있는 프로세스로 요청이 가면 로그인된 상태가 되고, 세션 메모리가 없는 프로세스로 요청이 가면 로그인되지 않은 상태가 되기 때문이다.
따라서 이 문제를 극복하기 위해서는 세션을 공유할 수 있게 해주는 무언가가 필요하다. 이를 위해 사람들은 주로 멤캐시드나 레디스(redis) 같은 서비스를 사용한다.
하나의 프로세스 내에서 돌아가는 멀티 쓰레드는 메모리를 공유할수 있지만, 멀티 프로세스는 프로세스간의 메모리 공유를 할수가 없다.
pm2 모듈 사용법
pm2 연습을 위해 우선 다음과 같이 서버 코드를 작성해준다.
/* app.js */
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.send('<h1>Hello World!</h1>');
});
app.listen(3000, () => {
console.log('pm2 서버 시작');
});
pm2 설치
$ npm install -g pm2 # pm2 설치
$ pm2 -version # pm2 버젼 확인
$ pm2 update # pm2 업데이트
pm2 실행 (start)
pm2를 이용해 앱을 실행한다면 fork 모드(자식 프로세스)로 설정되고, 서버 실행 즉시 Daemon화 되어, 종료하거나 에러가 발생하지 않는 이상 24시간 계속 유지된다.
백그라운드에서 데몬으로 돌아가서 nodemon과는 달리 콘솔에 다른 명령어를 입력할 수 있다.
$ pm2 start <파일명>
💡 리눅스나 맥에서 pm2 실행 시
리눅스나 맥에서 pm2를 실행할 때 1024번 이하의 포트를 사용하려면 관리자 권한이 필요하다.
따라서 sudo를 명령어 앞에 붙여 실행한다. 앞으로 나오는 다른 명령어도 sudo npm start, sudo npx pm2 list, sudo npx pm2 kill, sudo npx pm2 monit처럼 하면 된다.
pm2 실행 옵션 (클러스터)
- --watch : PM2가 실행된 프로젝트의 변경사항을 감지하여 서버를 자동 재시작(reload)
- nodemon과 유사하다, 주로 개발단계에서 즉시 반영되므로 매우 편리하게 사용 할 수 있다.
- 만일 watch옵션시에 특정 폴더 경로는 무시해야할 때 --watch --ignore-watch="[dir]/*"
- -i max(코어개수) : Node.js의 싱글 스레드를 보완하기 위한 클러스터(Cluster) 모드
- -i 뒤에 코어의 개수를 입력하거나 max를 쓰면 최대 코어 개수로 클러스터링(Clustering) 된다.
- --name : 앱 이름 지정
- --max-memory-restart <200MB> : 앱이 리로드 될때 최대의 메모리 지정
- --log <log_path> : 로그 파일 경로 지정
- -- arg1 arg2 arg3 : 스크립트에 추가 인수 전달
- --restart-delay <delay in ms> : 재시작할때의 딜레이 지정
- --time : 로그 남길때 프리픽스로 시간 지정
- --no-autorestart : 재시작 불가하도록 설정
- --cron <cron_pattern> : 주기적으로 강제 재시작이 필요할때 설정 (cron)
$ pm2 start app.js --watch -i 2 # 프로세스를 watching하고 멀티 코어로 서버 실행
클러스터 모드로 실행하면, mode 부분이 fork에서 cluster로 변하는 걸 볼 수 있다.
$ pm2 start app.js -i max # 최대 코어 갯수로 클러스터링
$ pm2 start app.js -i 0
$ pm2 start app.js -i -1 # max - 1 개 수만큼 클러스터 실행
pm2 프로세스 증설 (scale)
만약 프로세스 개수를 늘리거나(scale up) 줄여야(scale down) 한다면 pm2 scale 명령어를 사용해서 실시간으로 프로세스 수를 증가시키거나 감소시킬 수 있다.
$ pm2 scale app +2 # app 프로세스 갯수 +2
$ pm2 scale app 4 # app 프로세스 갯수 4개로 고정
pm2 상태 확인 (ls | status)
status와 ls를 이용해 현재 프로세스 리스트를 띄울 수 있다. (둘이 같은 명령)
프로세스 아이디(pid), CPU와 메모리 사용량(mem) 등이 보여 편리하다.
uptime과 status 사이에 ↺ 재시작된 횟수가 나오는데, 횟수가 많으면 아니라면 서버가 자꾸 에러나서 재부팅된 것이니 이 경우 서버 개발자는 프로세스가 왜 재시작되었는지 로그를 통해 확인해봐야 한다.
status 도 서버가 자꾸 에러가 나면 online이 아닌 경우도 있다.
$ pm2 status
$ pm2 ls
$ pm2 l
pm2 프로세스 중지 (stop)
클러스터링 된 여러개의 프로세스중에 특정 프로세스를 골라 중지 할 수 있다.
중지된 프로세스는 아직 pm2가 관리하고 있는 상태이다.
$ pm2 stop <app_name | namespace | id | 'all' | json_conf>
$ pm2 stop 2 # id 2번 프로세스를 멈춤
$ pm2 stop app # 이름을 줘도 됨. (전체 프로세스 멈춤)
pm2 프로세스 재시작 (restart)
$ pm2 restart <app_name | namespace | id | 'all' | json_conf>
$ pm2 restart 2 # 위에서 중지한 2번 프로세스를 재시작
$ pm2 restart app.js # 파일명을 써서 전체 서버를 재시작
pm2 프로세스 리로드 (reload)
$ pm2 reload <app_name | namespace | id | 'all' | json_conf>
$ pm2 reload app
$ pm2 reload all
pm2 reload vs restart 차이점
- pm2 restart는 모든 프로세스를 죽인다음 다시 시작하는 방식. 그래서 아주 잠깐 동안 서비스를 이용하지 못하는 상황이 생긴다.
- pm2 reload는 하나씩 프로세스를 죽여서, 최소한 1개 이상의 프로세스를 유지하며 하나씩 재시작 하는 방식이다. 그래서 잠깐 재시작 하는 동안 서비스를 이용못하는 상황을 미연에 방지할 수 있다.
이를 '0-second-downtime' 이라고 불리운다.
단, 이러한 방식 때문에 reload는 restart보다 재실행 속도가 느리다는 단점이 있다.
다운 타임 (0-second-downtime) : 서버가 중지되어 클라이언트가 접속할 수 없는 시간
pm2 프로세스 삭제 (delete | kill)
돌아가고 있는 특정 프로세스를 죽이고 싶다면 delete 명령어에 프로세스 id를 찍어주면 된다.
$ pm2 delete <app_name | namespace | id | 'all' | json_conf>
$ pm2 delete 2 # 특정 프로세스 삭제
$ pm2 delete app # 프로세스 name이 app인 거 모두 삭제
kill 명령어로 모든 프로세스를 한꺼번에 죽일수 있다.
$ pm2 kill # 프로세스 전체 삭제
pm2 로그 보기 (log)
- 기본 로그파일 위치 : /root/.pm2/pm2.log
만일 에러 로그만 보고 싶다면 뒤에 –err을 붙이면 된다.
출력 줄 수를 바꾸고 싶다면 --lines 숫자 옵션을 사용한다.
$ pm2 log # 전체 프로세스 로그 보기
$ pm2 log [process name | process id] # 특정 프로세스 로그 보기
$ pm2 log --lines 200 # 200줄 까지만 보기
$ pm2 log --err 200 # 에러 로그만 보기
빠져나올 때는 Ctrl+C로 빠져나오면 된다.
pm2 로그 관리 (logrotate)
pm2를 실행할때마다 로그 경로에 저장되는 로그들은 직접 삭제해주지 않는한 계속 쌓이게 된다.
이렇게 계속 쌓이게 된다면 서버 용량 부족으로 이어질 수도 있는데, 이를 해결하기 위해서 로그 파일을 관리해주는 pm2-logrotate라는 plugin을 사용한다.
$ pm2 install pm2-logrotate # npm으로 설치하는 것이 아닌 pm2로 설치
$ pm2 set pm2-logrotate:<option> <value> # 옵션 설정은 다음과 같이 설정
$ pm2 set pm2-logrotate:max_size 1K
$ pm2 set pm2-logrotate:retain 10
Options
- max_size: 로그 파일 사이즈 제한 크기 (기본값은 10M)
- retain: 로그 파일을 최대 몇개까지 가지고 있을 것인지 설정 (기본값은 30개)
- compress: 로그 파일을 gzip으로 압축할 것인지 여부 (기본값은 false)
- dateFormat: 로그 파일 날짜 폼새 (기본값은 YYYY-MM-DD_HH-mm-ss)
- workerInterval: 로그 파일 사이즈를 확인하는 1초마다의 회수 (기본값 초당 30회)
- rotateInterval: cron job (기본값은 '0 0 * * *')
pm2 프로세스 정보 (show | describe)
$ pm2 show <id> | <name>
$ pm2 describe <id> | <name>
pm2 모니터링 (monit)
pm2로 실행한 서버들의 상황을 한눈에 확인 할 수 있는 monit 명령어는 아래 할목을 실시간 상태를 확인할 수 있는 모니터링 창을 띄워준다
- 각 프로세스의 메모리, CPU 사용률, 현재 상태
- 선택된 프로세스의 로그
- 전체 프로세스의 Heap 사이즈, 사용률
- 어플리케이션 정보
$ pm2 monit
pm2 설정 파일로 관리하기
pm2 설정 파일 만들기 (ecosystem.config.js)
npm을 관리하기 위해 package.json 설정 파일을 만들었듯이, pm2를 보다 수월하게 관리하기 위해 ecosystem.config.js 라는 파일을 만들어 설정할 수 있다.
위에서 연습해봤던 pm2 CLI 옵션 명령어들을 이 파일에서 모아 관리할수 있다.
$ pm2 ecosystem # ecosystem.config.js 파일이 생성
module.exports = {
/* apps 항목은 우리가 pm2에 사용할 옵션을 기재 */
apps: [
{
name: 'projectName', // app이름
script: './index.js', // 실행할 스크립트 파일
instances: 2, // cpu 코어수 만큼 프로세스 생성 (instance 항목값을 ‘0’으로 설정하면 CPU 코어 수 만큼 프로세스를 생성)
exec_mode: 'cluster', // 클러스터 모드
max_memory_restart: '300M', // 프로세스의 메모리가 300MB에 도달하면 reload 실행
watch: ['bin', 'routes'], //bin폴더, routes폴더를 감시해서 변경사항 실행
ignore_watch: ['node_modules'], // 반대로 해당폴더의 파일변경은 무시
env: {
// 환경변수 지정
Server_PORT: 4000,
NODE_ENV: 'development',
Redis_HOST: 'localhost',
Redis_PORT: 6379,
},
output: '~/logs/pm2/console.log', // 로그 출력 경로 재설정
error: '~/logs/pm2/onsoleError.log', // 에러 로그 출력 경로 재설정
},
],
/* deploy는 원격 서버와 git을 연동해서 배포하는 방식 */
deploy: {
production: {
user: 'SSH_USERNAME',
host: 'SSH_HOSTMACHINE',
ref: 'origin/master',
repo: 'GIT_REPOSITORY',
path: 'DESTINATION_PATH',
'pre-deploy-local': '',
'post-deploy': 'npm install && pm2 reload ecosystem.config.js --env production',
'pre-setup': '',
},
},
};
Fields 설명
- Info
- name: 어플리케이션 이름
- interpreter: 인터프리터 경로 (기본값은 Node)
- interpreter_args: 인터프리터 옵션
- script: pm2 시작에 대한 시작 파일 경로 (interpreter으로 실행)
- cwd: 앱이 실행될 디렉토리
- args: CLI로 실행되는 인자
- Advanced
- instances: 인스턴스 수를 결정할 수 있으며, 0으로 작성하면 현재 가능한 CPU 코어 수만큼 실행
- exec_mode: fork모드로 실행할지, cluster모드로 실행할지 선택
- watch: 폴더 또는 특정 폴더를 감시하여 변경되면 Reload를 진행
- ignore_watch: watch를 제외할 경로
- max_memory_restart: 설정한 메모리를 초과할 시, 자동으로 재시작
- source_map_support: 소스맵 활성화 여부를 선택
- Logs
- log_date_format: 로그 날짜 형식 (기본값은 YYYY-MM-DD HH:mm Z 형식)
- output: 로그 출력 경로를 설정
- error: 에러 로그 출력 경로를 설정
- Control Flow ★
- min_uptime: 어플리케이션이 가동되었다고 생각하는 최소 시간
- kill_timeout: 최종 SIGKILL을 보내기 까지의 시간
- wait_ready: Reload 대기 이벤트 대신에 어플리케이션에서의 process.send('ready') 를 기다린다
- autorestart: 프로세스를 1회만 실행시키고 싶을 때, false로 설정
모든 설정을 맞췄으면 설정 파일을 실행해주면 된다.
$ pm2 start ecosystem.config.js # 설정 파일을 start하면, 세팅대로 pm2 명령어가 실행되게 된다
pm2 개발/배포 환경 실행 분리
위의 ecosystem.config.js 에서 pm2를 실행할때 개발환경으로 실행할것인지 배포환경에서 실행할 것인지 혹은 어느 프로젝트를 실행할 것인지 를 분리해서 설정 할 수 있다.
module.exports = {
apps: [
{
/* 개발 환경용 서버 */
name: 'projectName-dev',
script: './app.js',
instances: 1, // 단일 쓰레드
autorestart: false,
watch: false,
env: {
Server_PORT: 4000,
NODE_ENV: 'development',
},
},
{
/* 배포 환경용 서버 */
name: 'projectName-product',
script: './app.js',
instances: -1, // 클러스터 모드
autorestart: false,
watch: false,
env: {
Server_PORT: 1234,
NODE_ENV: 'production',
},
},
],
};
그리고 package.json 파일에서 scripts 영역에 다음과 같이 명령 구문을 넣으면 된다.
"scripts": {
"dev": "pm2 start ecosystem.config.js --only projectName-dev --env development",
"start": "pm2 start ecosystem.config.js --only projectName-product --env production",
"del": "pm2 del all"
},
위 명령어에서 중요한 옵션은 --only와 --env 이다.
- --only 옵션은 ecosystem.config.js 파일에서 name 값이을 지정해서 실행시키주는 옵션이다.
- --env 옵션은 실행되는 항목에 env 설정들을 실행시켜주는 옵션이다.
따라서 다음과 같이 개발 모드와 배포 모드에서 각각 따로 실행되게 구성할 수 있다.
$ npm start # 배포환경으로 pm2 실행
$ npm run dev # 개발환경으로 pm2 실행
pm2 무중단 서비스 운영하기
서비스를 오픈 이후에도 여러 상황 변화에 따라 지속적으로 업데이트를 해줘야 한다.
만약 애플리케이션에 새로운 기능을 추가했거나 발견된 버그를 수정했다면, 이를 서비스에 반영하기 위해선 다시 배포해야 할 것이다.
즉, 수정 사항을 반영하기 위해 기존 프로세스를 재시작 해야 한다는 소리이다.
이때 위에서 소개했던 down time 0 기능이 있는 reload 명령어를 활용하면 사용자 서비스 중단 없이 프로세스를 재시작할 수 있는데, 이때 무중단 서비스를 유지하려면 몇 가지 주의해야 할 사항이 존재한다.
가벼운 서비스 수준이라면 기본적으로 reload 명령어만 수행해도 PM2가 별다른 문제없이 알아서 프로세스를 재시작하여 무중단 서비스를 운영할 수 있다.
하지만 큰 프로젝트의 애플리케이션을 그저 reload 명령어만 신뢰하고 수행한다면, 배포 과정에서 사용자에게 종종 서비스에러 메시지를 보여주게 될 수도 있다. (접속이 안된다거나 기능 수행이 안된다거나)
pm2 프로세스 재시작 과정
왜 이런 문제가 발생하는지 프로세스 재시작 과정 원리를 알아보자
- 프로세스 10개가 실행되고 있다고 가정해보자.
이런 상태에서pm2 reload를 실행하면 PM2는 기존 0번 프로세스를 _old_0 프로세스로 옮겨두고, - 새로운 0번 프로세스를 만든다.
- 새로운 0번 프로세스는 파일을 로드하고 요청을 처리할 준비가 되면 마스터 프로세스에게 ready 이벤트를 보내고,
- 마스터 프로세스는 더 이상 필요없어진 _old_0 프로세스(기존 0번 프로세스)에게 SIGINT 시그널을 보내고 프로세스가 종료되기를 기다린다.
- 만약 일정 시간(1600ms)이 지났는데도 종료되지 않는다면, SIGKILL 시그널을 보내 프로세스를 강제로 종료시킨다.
- 이 과정을 총 프로세스 개수만큼 반복하면 모든 프로세스의 재시작이 완료된다.
SIGINT : 인터럽트 시그널로 실행을 중지시킴, Ctrl + c 입력시 보내지는 시그널
SIGKILL : 프로세스를 강제로 종료 시키는 시그널
pm2 프로세스 재시작 문제 (빠른 ready)
- PM2는 기존 0번 프로세스를 _old_0 프로세스로 옮겨두고,
- 새로운 0번 프로세스를 만든다.
- 새로운 0번 프로세스는 파일을 로드하는데 시간이 오래 걸려 아직 요청을 처리할 준비가 되지 않았음에 불구하고 마스터 프로세스에게 ready 이벤트를 보내버린다
- 마스터 프로세스는 _old_0 프로세스(기존 0번 프로세스)에게 SIGINT 시그널을 보내고 프로세스가 종료되기를 기다린다.
- 만약 일정 시간(1600ms)이 지났는데도 종료되지 않는다면, SIGKILL 시그널을 보내 프로세스를 강제로 종료시킨다.
- 그런데 아직 새로운 0번 프로세스 (App) 은 초기화가 덜 되어, 소비자로 부터 요청을 받으면 에러를 발생시키게 된다. (서비스 중단 현상)
해결책 (ready)
이 문제를 해결하기 위해서는 프로세스가 수행될 때 바로 ready 이벤트를 보내지 말고, 요청 받을 준비가 완료된 시점에 ready 이벤트를 보내도록 처리해야 한다.
[ ecosystem.config.js ]
ecosystem.config.js에서 wait_ready 옵션을 ‘true’로 설정하면 마스터 프로세스에게 ready 이벤트를 기다리라는 의미이고, listen_timeout 옵션은 ready 이벤트를 기다릴 시간값(ms)을 의미한다.
// ready 이벤트 설정 변경
module.exports = {
apps: [{
name: 'app',
script: './app.js',
instances: 0,
exec_mode: 'cluster',
wait_ready: true, // 마스터 프로세스에게 ready 이벤트를 기다려라
listen_timeout: 50000 // ready 이벤트를 기다릴 시간값(ms)
}]
}
[ app.js ]
그리고 app.js에는 app.listen이 완료되면 실행되는 콜백(Callback) 함수에서 마스터 프로세스로 ready 이벤트를 보내도록 코딩한다.
const express = require('express')
const app = express()
const port = 3000
app.get('/', function (req, res) {
res.send('Hello World!')
})
app.listen(port, function () {
process.send('ready') // 서버 구동이 완료되면 프로세스에 'ready'를 보냄
console.log(`application is listening on port ${port}...`)
})
pm2 프로세스 재시작 문제 (프로세스 중지 도중 요청)
- PM2는 기존 0번 프로세스를 _old_0 프로세스로 옮겨두고,
- 새로운 0번 프로세스를 만든다.
- 새로운 0번 프로세스는 파일을 로드하고 요청을 처리할 준비가 되면 마스터 프로세스에게 ready 이벤트를 보내고,
- 마스터 프로세스는 더 이상 필요없어진 _old_0 프로세스(기존 0번 프로세스)에게 SIGINT 시그널을 보내고 프로세스가 종료되기를 기다린다.
그런데 중지 도중, 이때 하필이면 사용자가 요청을 보냈다. 타이밍 기가막히게 ! (참고로 이 요청은 500ms 좀 길게 걸린다) - 만약 일정 시간(1600ms)이 지났는데도 종료되지 않는다면, SIGKILL 시그널을 보내 프로세스를 강제로 종료시킨다.
- 문제는 사용자는 아직 요청에 대한 응답을 받지도 않았는데 프로세스가 강제로 종료 되어 버렸다는 점이다. 그래서 사용자는 에러를 겪게 되버린다.
해결책 (시간 늘리기)
이 문제를 해결하기 위해서는 SIGINT 시그널이 전달되면 app.close명령어로 프로세스가 새로운 요청을 받는 것을 거절하고 기존 연결은 유지하게 처리해야한다.
그리고 사용자 요청을 처리하기에 충분한 시간을 kill_timeout에 설정하고, 기존에 유지되고 있던 연결이 종료되면 프로세스가 종료되도록 처리한다.
[ ecosystem.config.js ]
ecosystem.config.js에서 kill_timeout 옵션을 ‘5000’으로 설정하면, SIGKILL 시그널을 보내기까지의 대기 시간을 디폴트 값 1600ms에서 5000ms로 변경할 수 있다.
module.exports = {
apps: [{
name: 'app',
script: './app.js',
instances: 0,
exec_mode: ‘cluster’,
wait_ready: true,
listen_timeout: 50000,
kill_timeout: 5000 // SIGINT ↔ SIGKILL 대기시간을 5초로 설정
}]
}
[ app.js ]
app.js에 새로 추가된 코드는 해당 프로세스에 SIGINT 시그널이 전달되면, 새로운 요청을 더 이상 받지 않고 연결되어 있는 요청이 완료된 후 해당 프로세스를 종료하도록 한다.
const express = require('express')
const app = express()
const port = 3000
app.get('/', function (req, res) {
res.send('Hello World!')
})
app.listen(port, function () {
process.send('ready')
console.log(`application is listening on port ${port}...`)
})
// 만일 process로부터 SIGINT 이벤트를 받으면..
process.on('SIGINT', function () {
// 어플리케이션을 닫음
app.close(function () {
console.log('server closed')
process.exit(0) // 정상 종료
})
})
HTTP 1.1 Keep-Alive 경우
만일 일정시간동안 connection을 유지하는 keep-alive 상태일 경우에는 다시 문제가 생긴다.
응답을 주고 나서 프로세스를 종료해버렸는데, 사용자 입장에서는 여전히 연결상태라 서비스 에러를 받을수 있기 때문이다.
HTTP Keep-Alive 란?
HTTP 는 TCP 위에서(기반으로) 동작을 한다.
그래서 TCP가 전송이 끝나면 연결이 끊어지듯이 HTTP도 서로 전송이 끝나면 끊어지게 된다.
그런데 매번 이렇게 똑같은 주소로 요청을 할 때마다 새로운 연결을 설정하고 끊어야 한다면 자원이 낭비되게 된다.
이런 문제를 막고자 Keep-Alive가 생겼다. 말 그대로 '연결을 계속 유지해라' 라는 의미이다.
최소 특정 시간동안(timeout) 최대 요청 request(max)의 수에 따라 동작한다.
HTTP/1.1 200 OK
Connection: Keep-Alive
Keep-Alive: timeout=5, max=1000
...
위의 예시 응답 코드를 보면, 최소 5초동안 최대 1000번의 요청을 할 경우에는 http connection이 끊어지지 않을 것이라는 의미이다.
keep-alive는 HTTP 1.1 부터는 기본적으로 활성화 되어 동작된다.
그래서 서버 코드를 다음과 같이 추가 수정한다.
SIGINT 시그널을 받았을 때 특정 전역 플래그값에 따라 응답 헤더에 ‘Connection: close’를 설정해 클라이언트 요청을 종료하는 방법을 활용하면 타임아웃으로 서비스가 중단되는 문제를 해결할 수 있다.
[ app.js ]
const express = require('express');
const app = express();
const port = 3000;
let isDisableKeepAlive = false;
// keep-alive 해제용 미들웨어
app.use(function (req, res, next) {
if (isDisableKeepAlive) {
res.set('Connection', 'close'); // 만약 전역 변수가 true면 요청 오면 connection을 강제로 닫는다.
}
next();
});
app.get('/', function (req, res) {
res.send('Hello World!');
});
app.listen(port, function () {
process.send('ready');
console.log(`application is listening on port ${port}...`);
});
process.on('SIGINT', function () {
isDisableKeepAlive = true; // SIGINT 시그널을 받으면 전역변수를 true로 만들어 앞으로 요청오면 종료해 버리게 만든다.
app.close(function () {
console.log('server closed');
process.exit(0);
});
});
예상치 못한 시스템 재부팅 (리눅스용)
위와 같이 어플리케이션이 종료되는 것은 PM2가 알아서 관리해주지만, 하지만 컴퓨터 시스템에 문제가 생겨 자동으로 재부팅된다면 어떻게 될까?
당연히 PM2 자체가 종료되어 버리니 따로 수동으로 다시 켜주지 않는 이상 서버 관리는 물건너 간다고 볼수있다.
다행히 시스템을 재부팅해도 pm2를 자동으로 실행하게끔 할수 있는 명령어가 존재한다.
⚠️ 윈도우는 지원하지 않는 유닉스(리눅스)용 명령어 이다 !!
$ pm2 startup
[PM2] You have to run this command as root. Execute the following command:
sudo su -c "env PATH=$PATH:/home/unitech/.nvm/versions/node/v14.3/bin pm2 startup <distribution> -u <user> --hp <home-path>"
$ sudo su -c "env PATH=$PATH:/home/unitech/.nvm/versions/node/v14.3/bin pm2 startup <distribution> -u <user> --hp <home-path>"
$ pm2 save
이 명령은 pm2로 실행할 서버를 항상 자동으로 시작하게끔 부트 스크립트에 등록하는 명령이다. 단, 부트 스크립트에 등록하기 전 등록할 서버를 미리 실행해 놓아야 한다.
$ pm2 startup 명령을 사용하면 터미널에 sudo env로 시작하는 명령어가 자동으로 생성되어 출력될 것이다.
그렇게 출력되어 나온 명령어를 복사해서 그대로 붙여 넣은 뒤 실행하면 현재 실행 중인 pm2 서버가 부트 스크립트에 등록되게 된다.
그다음 pm2 save 명령으로 pm2 관련 변경사항을 저장해 주면 끝난다.
반대로 부트 스크립트를 해제하려면 다음 명령어를 이행하면 된다.
$ pm2 unstartup
$ pm2 save
# 참고자료
https://engineering.linecorp.com/ko/blog/pm2-nodejs/
https://any-ting.tistory.com/74
이 글이 좋으셨다면 구독 & 좋아요
여러분의 구독과 좋아요는
저자에게 큰 힘이 됩니다.